Search code examples
javascriptd3.jsforce-layoutd3-force-directed

Make two instances of d3.forceCollide() play nice together


I want two instances of d3.forceCollide(). In one, every node is pushed away from one another to prevent overlap. In the second, only a subset of nodes are pushed away from one another, with a much bigger radius.

To accomplish the second force, I tweak the initialize method to filter the incoming nodes, like so:

function selective(force,filter){
    var init = force.initialize;
    force.initialize = function(_){return init(_.filter(filter));};
    return force;
}

var dpi = 90;  // approximate pixels per inch in SVG
var size = dpi * (1/4); // quarter-inch unit size

var universally_applied = 
    d3.forceCollide()
    .radius(size)
    .strength(1);

var selectively_applied =
        selective(
            d3.forceCollide(),
            function(d){return d.id === color;}
        )
        .radius(size*5)
        .strength(1);
}

Now, this ALMOST works. I created a fiddle to see it in action: https://jsfiddle.net/jarrowwx/0dax43ue/38/ - every colored circle is supposed to repel every other circle of the same color, from a distance. Every other color, it just bumps into and pushes it out of the way.

If I do not change the order in which things are defined, then the selectively applied force is ONLY applied to the first color (red). If I shuffle the data array before applying forces, it is difficult to define exactly what happens, but the force is applied to some circles and not most of the others, even among the same color.

Any ideas what is going on here, or how to fix it?


Solution

  • The D3 team decided that this behavior was a bug (Isolating forces to a subset of nodes? #72), and fixed it. The fix was included in version 1.0.4 of d3-force, which is available as part of the full D3 build as of version 4.4.0.

    The problem is resolved using the solution suggested by "Partial forces on nodes in D3.js", and the code works as intended now.