kjo
kjo

Reputation: 35331

Can one specify a custom force function for a force-directed layout?

I want to experiment with an alternative family force functions for force-directed graph layouts.

For each node n_i, I can define a "force function" f_i such that

The net force on node n_i should then be the vector sum of the forces f_i ( n_j ), where n_j ranges over all other nodes1.

Is there some way to tell d3.js to use these custom force functions in the layout algorithm?

[The documentation for d3.js's force-directed layout describes various ways in which its built-in force function can be tweaked, but I have not been able to find a way to specify an entirely different force function altogether, i.e. a force function that cannot be achieved by tweaking the parameters of the built-in force function.]


1IOW, no other/additional forces should act on node n_i besides those computed from its force function f_i.

Upvotes: 5

Views: 2378

Answers (2)

Ryder Brooks
Ryder Brooks

Reputation: 2119

Yes you can. Credit goes to Shan Carter and his bl.ocks example

let margin = {
  top: 100,
  right: 100,
  bottom: 100,
  left: 100
};

let width = 960,
  height = 500,
  padding = 1.5, // separation between same-color circles
  clusterPadding = 6, // separation between different-color circles
  maxRadius = 12;

let n = 200, // total number of nodes
  m = 10, // number of distinct clusters
  z = d3.scaleOrdinal(d3.schemeCategory20),
  clusters = new Array(m);

let svg = d3.select('body')
  .append('svg')
  .attr('height', height)
  .attr('width', width)
  .append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

let nodes = d3.range(200).map(() => {
  let i = Math.floor(Math.random() * m),
    radius = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
    d = {
      cluster: i,
      r: radius
    };
  if (!clusters[i] || (radius > clusters[i].r)) clusters[i] = d;
  return d;
});

let circles = svg.append('g')
  .datum(nodes)
  .selectAll('.circle')
  .data(d => d)
  .enter().append('circle')
  .attr('r', (d) => d.r)
  .attr('fill', (d) => z(d.cluster))
  .attr('stroke', 'black')
  .attr('stroke-width', 1);

let simulation = d3.forceSimulation(nodes)
  .velocityDecay(0.2)
  .force("x", d3.forceX().strength(.0005))
  .force("y", d3.forceY().strength(.0005))
  .force("collide", collide) // <<-------- CUSTOM FORCE
  .force("cluster", clustering)//<<------- CUSTOM FORCE 
  .on("tick", ticked);

function ticked() {
  circles
    .attr('cx', (d) => d.x)
    .attr('cy', (d) => d.y);
}

// Custom 'clustering' force implementation.
function clustering(alpha) {
  nodes.forEach(function(d) {
    var cluster = clusters[d.cluster];
    if (cluster === d) return;
    var x = d.x - cluster.x,
      y = d.y - cluster.y,
      l = Math.sqrt(x * x + y * y),
      r = d.r + cluster.r;
    if (l !== r) {
      l = (l - r) / l * alpha;
      d.x -= x *= l;
      d.y -= y *= l;
      cluster.x += x;
      cluster.y += y;
    }
  });
}
// Custom 'collide' force implementation.
function collide(alpha) {
  var quadtree = d3.quadtree()
    .x((d) => d.x)
    .y((d) => d.y)
    .addAll(nodes);

  nodes.forEach(function(d) {
    var r = d.r + maxRadius + Math.max(padding, clusterPadding),
      nx1 = d.x - r,
      nx2 = d.x + r,
      ny1 = d.y - r,
      ny2 = d.y + r;
    quadtree.visit(function(quad, x1, y1, x2, y2) {

      if (quad.data && (quad.data !== d)) {
        var x = d.x - quad.data.x,
          y = d.y - quad.data.y,
          l = Math.sqrt(x * x + y * y),
          r = d.r + quad.data.r + (d.cluster === quad.data.cluster ? padding : clusterPadding);
        if (l < r) {
          l = (l - r) / l * alpha;
          d.x -= x *= l;
          d.y -= y *= l;
          quad.data.x += x;
          quad.data.y += y;
        }
      }
      return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
    });
  });
}
<!doctype html>
<meta charset="utf-8">

<body>
  <script src="//d3js.org/d3.v4.min.js"></script>

Also here is a more in-depth look at the subject.

Upvotes: 3

Lars Kotthoff
Lars Kotthoff

Reputation: 109282

To achieve this, you'll need to create your own custom layout. There's no tutorial for this that I'm aware of, but the source code for the existing force layout should be a good starting point as, by the sound of it, the structure of your custom layout would be very similar to that.

Upvotes: 2

Related Questions