d3 / d3-force

Force-directed graph layout using velocity Verlet integration.
https://d3js.org/d3-force
ISC License
1.82k stars 377 forks source link

Allow d3.forceManyBody().distanceMax() and .distanceMin() to accept functions? (feature request) #186

Closed jaketeyjake closed 3 years ago

jaketeyjake commented 3 years ago

d3.forceManyBody().strength() can accept a function as its parameter, allowing differentiation of strength based on some other node property. Why not implement the same for .distanceMax() and .distanceMin()?

One use case is setting up a ring fence of fixed nodes in an irregular polygon to contain a bunch of nodes that are free to move in the simulation. You'd want the ring fence nodes to have strong repulsive strength but only at a local distance, while the free nodes could interact with each other over longer distances.

Fil commented 3 years ago

Interesting. It's easy enough to plug in two arrays to store the values (as we do with strengths) in manyBody.js, and use them when evaluating the force; I'm happy to help you create the relevant force function (or patched version of d3-force) if you want to give it a try.

However, I'm not sure it would work in the particular use case you've described. Since the fence nodes are fixed, they are never, at the end of the iteration, subject to movement, so it doesn't matter what this force has computed for them. In fact, the only impact of fixed nodes in the calculation is to inform a "density field" (*), computed in a first stage. In the second (and final) stage, we evaluate for each "receiving" node the impact of the field. DistanceMax is used in this final stage, so it seems that only the distanceMax pertaining to free nodes would be used in a meaningful way.

What we might do in the case you're describing is to layer two forceManyBody. One would include only the free nodes, and would be parametrized with a long distanceMax; the other would include all nodes with a short distanceMax.

Note that there are other ways of fencing nodes inside a polygon: for one example see forceWalls, and the whole discussion at #163.

(*) In the Barnes–Hut algorithm, nodes interact with a "presence field", materialized in D3 as a quadtree that registers, for each quad, how much matter is contained in that quad. In other words, nodes don't interact with individual nodes, but with quads.

jaketeyjake commented 3 years ago

Ahh, thanks for the information. Perhaps I was misinterpreting the docs about .distanceMax():

# manyBody.distanceMax([distance]) · Source

If distance is specified, sets the maximum distance between nodes over which ***this*** force is considered. 
If distance is not specified, returns the current maximum distance, which defaults to infinity.
Specifying a finite maximum distance improves performance and produces a more localized layout.

(emphasis mine) - I interpreted that as the force belonging to the node itself, and therefore other nodes would only feel it if they were close by. E.g., even though the fixed node doesn't move, free nodes would only feel its force if they were within .distanceMax() of the fixed node. I guess if the feature request I submitted were implemented, it might be the case?

I will check out the forceWalls discussion as an alternative. Thanks!

lukasIO commented 3 years ago

Hi,

I came across this issue because I wanted to pass a function to distanceMax and it did not work. Motivated by your words, @Fil, I tried to naively implement the same array as used for strength also for the distanceMax2 - but it's not working. The code seems to be failing because I am trying to access the index of the quad in quad.data.index but the data is undefined.

here the relevant code snippet:

function apply(quad, x1, _, x2) {
    if (!quad.value) return true;

    var x = quad.x - node.x,
        y = quad.y - node.y,
        w = x2 - x1,
        l = x * x + y * y;

    // Apply the Barnes-Hut approximation if possible.
    // Limit forces for very close nodes; randomize direction if coincident.
    if (w * w / theta2 < l) {
      if (l < distanceMax2s[quad.data.index]) {
        if (x === 0) x = jiggle(random), l += x * x;
        if (y === 0) y = jiggle(random), l += y * y;
        if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
        node.vx += x * quad.value * alpha / l;
        node.vy += y * quad.value * alpha / l;
      }
      return true;
    }

what can I do if there is no data of the quad ?

edit: here is the commit of what I tried https://github.com/neesh-studio/d3-force/pull/1/commits/7bad558b9df9be98dbfd446dfb59f768ff20a0ef

Fil commented 3 years ago

quad.data is only present for leaf nodes, not for those which have 4 subquads. Test node.length to distinguish the two cases https://github.com/d3/d3-quadtree/#nodes

lukasIO commented 3 years ago

thanks for your explanation! Without quad.data I'm puzzled how I should replace stuff like if (l < distanceMax2) { if distanceMax2 is a function and I don't have an accessor (index) for the corresponding distanceMax2-Array. My attempt was:

- if (l < distanceMax2) {
+ if (l < distanceMax2s[quad.data.index]) {

Should I simply ignore distanceMax2 altogether if the node is not a leaf node?

Fil commented 3 years ago

Oh so in that case, you'd need to dig into the quad to see if any point contained has a distanceMax2 that says it must be counted…? Then it seems it would totally defeat the purpose of the quadtree, which is to eliminate points without looking at them. See my point about density field (*) in the first comment.

I'm going to close this issue, since it's more a topic for (fun) experimentations, than an issue with the current code. Feel free to continue the discussion here, or to open a thread in https://github.com/d3/d3/discussions instead.