5

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

  • f_i ( n_i ) is identically zero; and
  • f_i ( n_j ), where n_i != n_j, is the force on node n_i that is due to some other node n_j.

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.

kjo
  • 33,683
  • 52
  • 148
  • 265
  • You would need to create a new layout to use your custom force. – Lars Kotthoff Mar 08 '15 at 15:09
  • @LarsKotthoff: thanks for the pointer. I just scanned the d3.js docs, but I was not able to find any documentation on creating custom layouts. I imagine that this involves something like `d3.layout.myLayout = function () { ... }`, but it's not at all clear to me what exactly this function should return. IOW, I can't find any documentation on the "layout interface". Of course, I could try to reverse-engineer the d3.js source, but my previous attempts to do this have been pretty traumatic, so I'd really prefer not to do this if possible. – kjo Mar 08 '15 at 15:34
  • There's no documentation on that I'm afraid -- you'll have to go by the source. Starting with the force layout should be pretty straightforward though as, by the sound of it, the structure of your custom layout would be very similar to that. – Lars Kotthoff Mar 08 '15 at 15:47
  • @LarsKotthoff: thanks again; I'll accept your comments as the answer if you post them as such. – kjo Mar 08 '15 at 19:36

2 Answers2

3

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.

Ryder Brooks
  • 2,049
  • 2
  • 21
  • 29
2

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.

Lars Kotthoff
  • 107,425
  • 16
  • 204
  • 204