🏠 back to Observable

Help - d3 force simulation is not being triggered when nodes change.

This is my first foray into d3 forces and transitions. I’ve cobbled together something that allows me to add or remove nodes (circles with text) based on a slider, but I’ve not been able to get the force simulation to trigger automatically when the set of nodes change. That’s the main problem for which I’m requesting help.

Secondly, when I manually refresh the simulation cell, the resulting positions of the nodes are not at all what I expect, based on the forces that I think I am appplying. It may be a matter of experimenting with the strengths or other parameters of the forces, but I really need to get the simulation to trigger automatically first, so that I can see the results of the forces as I experiment with them.

I’ve shared the notebook entitled Radial Force that has the problem. Near the bottom of the page, I’ve described the problems in more detail.

Although I’m mainly seeking help to get the simulation running, I would also welcome any suggestions about how to choose appropriate forces to achieve the desired effect that I describe in the “Plan” section of the notebook.

Thanks

I don’t recommend using this (the previous value of the cell). You can make it work, but I find that it’s very difficult to do it correctly, because anything can have changed and likely some things you may not have expected. In retrospect we probably should not have included this feature in the runtime, and perhaps if we version the runtime in the future we can remove this footgun.

What I recommend instead is that you use a side-effect cell: a cell that reacts to the input (the slider) and then mutates the dependent state (such as the simulation or the visualization). Here’s an example of this technique:

I’ll try to cook up an example of a dynamic force graph to give a better idea of what I mean, but I’m flying to Chicago tonight for Visfest and I’m not sure if I’ll have time before leaving.

Thanks for the quick reply Mike. I’ll see if I can glean any ideas from the Sortable Bar Chart notebook. I’m really a newbie here. I’m using “this” just because that’s the way it was done in a page where I stole some code. I definitely don’t understand the implications of some of this code the way you do. Thanks for the suggestion though. I’ll look forward to any examples you get a chance to put together when you return.
Safe travels.

In general, the rule of thumb is to avoid side-effects whenever you can, and rely on Observable’s reactivity to throw away the old cells and create new ones.

But for something like a transition (or a smooth change to a force-directed graph), you need side-effects (mutation) to make the change less disruptive. Hence the side-effect cell (calling chart.update) in the sortable bar chart as a way to minimize and isolate where the mutation happens inside the chart cell.

Is the following line from the “order” cell…

    select.dispatchEvent(new CustomEvent("input"));

…the thing that causes the “side effect” cell to be re-executed, thus calling chart.update?

I saw similar code in https://observablehq.com/@tmcw/sample-notebook-testing-calling-a-function-defined-in-one-c but didn’t understand why.

Indirectly, yes. That dispatches an input event, which is how a view reports that its value has changed. Any cell that references the view’s value then runs automatically (triggering the call to chart.update which does the mutation and transition).

You can read about views here:

I used a custom view in that notebook so that I could define order as an arbitrary comparator function. But you can use vanilla HTML input elements as views, too, and then you don’t have to dispatch the input events manually.

1 Like

I added the following cell near the bottom of the notebook as a hack untiI I work out a cleaner solution using the mutation pattern Mike suggested. The comment is self-explanatory.

{
  // This cell is a hack which fixes the issue of restarting 
  // the simulation when nSides changes.
  // I really need to clean things up and use the mutation pattern 
  // that Mike suggested, but this will do for now.
  // The reference to nPoints will trigger this cell 
  // to be updated when nPoints changes,
  // which in turn allows the simulation to be restarted.
  simulation.restart();
  return nPoints; 
}

Now I need to figure out why the combination of forces are not behaving the way I expect to move the nodes so they end up equally spaced around a circle. I’m probably over-complicating it. Any suggestions?

Thanks Mike.

2 Likes

Are you sure you need to use d3-force? Here’s something rough that I think is along the lines of your “Plan” that just sends the nodes to precomputed positions around a circle:

4 Likes

Thanks @bgchen. I think you’re pretty much right on the mark. I had a feeling that I was over complicating things. If I had done it from scratch it would have been much more like what you’ve done with manually calculating the coordinates for the nodes rather than depending on d3-forces to position the nodes. I realize now, that what I really wanted was just the transitions in response to the slider.

When I came across Mike’s original Radial Force example using d3.forceRadial, I tried to port that model into Observable. In trying to make the number of nodes varable with the slider, I added in snippets from another Observable notebook which really just brought some transitions into the mix. I didn’t initially understand that d3-transitions could be used independently from d3-forces.

When the only tool you have is a hammer, everything looks like a nail.

Thanks for helping me distinguish between d3-force and just using generic transitions. Hopefullly, I can now use the right tool for the job at hand.

4 Likes

Hello David,
I too am new to d3 forces. A few weeks ago I made these trying to understand how it worked:

Maybe they will help you too better grasp what’s happening.

1 Like