Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jaketeyjake opened this issue Dec 6, 2020 · 6 comments

Comments

@jaketeyjake
Copy link

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
Copy link
Member

Fil commented Dec 7, 2020

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
Copy link
Author

jaketeyjake commented Dec 7, 2020

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
Copy link

lukasIO commented Feb 18, 2021

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 neesh-studio@7bad558

@Fil
Copy link
Member

Fil commented Feb 18, 2021

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
Copy link

lukasIO commented Feb 19, 2021

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
Copy link
Member

Fil commented Feb 19, 2021

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.

@Fil Fil closed this as completed Feb 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants