2

I'm using d3.js 3.5.6. How do we tick the force layout in our own render loop?

It seems that when I call force.start(), that automatically starts the force layout's own internal render loop (using requestAnimationFrame).

How do I prevent d3 from making a render loop, so that I can make my own render and call force.tick() myself?

trusktr
  • 44,284
  • 53
  • 191
  • 263
  • Don't call `force.start()`. – Lars Kotthoff Oct 24 '16 at 03:21
  • @LarsKotthoff I tried that, but then calling `force.tick()` seems to have no effect, and my `tick.on('tick', handler)` never fires. For example, I tried making an rAF loop, then just calling `force.tick()` inside the loop, but nothing happens. Note, I'm in d3.js v3.5.6. – trusktr Oct 24 '16 at 03:48
  • @trusktr As you can tell from my new answer and the old, now defaced one I was terribly wrong on this. To clean this mess up please consider unaccepting the old answer to enable deletion of the post. If it is also helpful you might still accept the new answer. – altocumulus Feb 04 '19 at 18:34

2 Answers2

3

This answer is plain wrong. Don't refer to it, don't use it.

I wrote a new one explaining how to do this correctly. I remember spending days digging into this as I though I had discovered an error. And, judging by the comments and the upvotes, I have managed to trick others—even including legends like Lars Kotthoff—to follow me down this wrong road. Anyways, I have learned a lot from my mistake. You only have to be ashamed of your errors if you do not take the chance to learn from them.

As soon as this answer is unaccepted I am going to delete it.


At first I was annoyed by the lack of code in the question and considered the answer to be rather easy and obvious. But, as it turned out, the problem has some unexpected implications and yields some interesting insights. If you are not interested in the details, you might want to have a look at my Final thoughts at the bottom for an executable solution.


I had seen code and documentation for doing the calculations of the force layout by explicitly calling force.tick.

# force.tick()

Runs the force layout simulation one step. This method can be used in conjunction with start and stop to compute a static layout. For example:

force.start();
for (var i = 0; i < n; ++i) force.tick();
force.stop();

This code always seemed dubious to me, but I took it for granted because the documentation had it and Mike Bostock himself made a "Static Force Layout" Block using the code from the docs. As it turns out, my intuition was right and both the Block as well as the documentation are wrong or at least widely off the track:

  1. Calling start will do a lot of initialization of your nodes and links data (see documentation of nodes() and links(). You cannot just dismiss the call as you have experienced yourself. The force layout won't run without it.

    Another thing start will eventually do is to fire up the processing loop by calling requestAnimationFrame or setTimeout, whatever is available, and provide force.tick as the callback. This results in an asynchronous processing which will repeatedly call force.tick, whereby doing the calculations and calling your tick handler if provided. The only non-hacky way to break this loop is to set alpha to below the hard-coded freezing point of 0.005 by calling force.alpha(0.005) or force.stop(). This will stop the loop on the next call to tick. Unless the timer is stopped this way, it will continue looping log0.99 (0.005 / 0.1) ≈ 298 times until alpha has dropped below the freezing point.

    One should note, that this is not the case for the documentation or the Block. Hence, the tick-loop started by force.start() will continue running asynchronously and do its calculations.

  2. The subsequent for-loop might or might not have any effect on the result of the force layout. If the timer happens to be still running in the background, this means concurrent calls to force.tick from the timer as well as from the for-loop. In any case will the calculations be stopped once alpha has dropped low enough when reaching a total of 298 calls to tick. This can be seen from the following lines:

    force.tick = function() {
      // simulated annealing, basically
      if ((alpha *= 0.99) < 0.005) {
        timer = null;
        event.end({type: "end", alpha: alpha = 0});
        return true;
      }
    
      // ...
    }
    

    From that point on you can call tick as often as you like without any change to the layout's outcome. The method is entered, but, because of the low value of alpha, will return immediately. All you will see is a repeated firing of end events.

    To affect the number of iterations you have to control alpha.

The fact that the layout in the Block seems static is due to the fact that no callback for the "tick" event is registered which could update the SVG on every tick. The final result is only drawn once. And this result is ready after just 298 iterations, it won't be changed by subsequent, explicit calls to tick. The final call to force.stop() won't change anything either, it just sets alpha to 0. This does not have any effect on the result because the force layout has long come to an implicit halt.

Conclusion

Item 1. could be circumvented by a clever combination of starting and stopping the layout as in Stephen A. Thomas's great series "Understanding D3.js Force Layout" where from example 3 on he uses button controls to step through the calculations. This, however, will also come to a halt after 298 steps. To take full control of the iterations you need to

  • Provide a tick handler and immediately stop the timer by calling force.stop() therein. All calculations of this step will have been completed by then.

  • In your own loop calculate the new value for alpha. Setting this value by force.alpha() will restart the layout. Once the calculations of this next step are done, the tick handler will be executed resulting in an immediate stop as seen above. For this to work you will have to keep track of your alpha within your loop.

Final thoughts

The least invasive solution might be to call force.start() as normal and instead alter the force.tick function to immediately halt the timer. Since the timer in use is a normal d3.timer it may be interrupted by returning true from the callback, i.e. from the tick method. This could be achieved by putting a lightweight wrapper around the method. The wrapper will delegate to the original tick method, which is closed over, and will return true immediately afterwards, whereby stopping the timer.

force.tick = (function(forceTick) {
  return function() {  // This will be the wrapper around tick which returns true.
    forceTick();       // Delegate to the original tick method.
    return true;       // Truth hurts. This will end the timer.
  }
}(force.tick));        // Pass in the original method to be closed over.

As mentioned above you are now on your own managing the decreasing value of alpha to control the slowing of your layout's movements. This, however, will only require simple calculus and a loop to set alpha and call force.tick as you like. There are many ways this could be done; for a simple showcase I chose a rather verbose approach:

// To run the computing steps in our own loop we need
// to manage the cooling by ourselves.
var alphaStart = 0.1;
var alphaEnd   = 0.005;
var alpha      = alphaStart;
var steps      = n * n;
var cooling = Math.pow(alphaEnd / alphaStart, 1 / steps);  

// Calling start will initialize our layout and start the timer
// doing the actual calculations. This timer will halt, however,
// on the first call to .tick.
force.start();

// The loop will execute tick() a fixed number of times.
// Throughout the loop the cooling of the system is controlled
// by decreasing alpha to reach the freezing point once
// the desired number of steps is performed.
for (var i = 0; i < steps; i++) {
  force.alpha(alpha*=cooling).tick();
}
force.stop();

To wrap this up, I forked Mike Bostock's Block to build an executable example myself.

altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • Alternatively you can also take the relevant parts from `force.start()` and do it all by yourself. – Lars Kotthoff Oct 24 '16 at 16:32
  • @LarsKotthoff I doubt that would be worth it. You need a complete custom-built force layout, because you won't have access to variables of layout's inner scope which are initialized in `start`. – altocumulus Oct 24 '16 at 16:46
  • It sounds like OP wants a slimmed-down version of the force layout and it this case it may be feasible. You're certainly correct that in general this wouldn't be worth it though. – Lars Kotthoff Oct 24 '16 at 16:49
  • @LarsKotthoff After pondering over it for some more hours I developed a slightly different approach, which I think, is the possibly least intrusive one. I have added a section "Final Thoughts" for this. I'd very much appreciate your thoughts about this. – altocumulus Oct 24 '16 at 22:58
  • That sounds good, yes! I guess without more detail from OP we don't know whether it's the most suitable thing in this case though. – Lars Kotthoff Oct 24 '16 at 22:59
  • This is exactly what I was looking for. Thanks! This makes it easier to, for example, use the force layout in an existing game engine's loop. In my case, I have something like `force.on('tick', function() {node.property('position', function(d) {this.position.x = d.x}) })` where the setting of `this.position.x` triggers a frame in an external loop, and so what was happening was that `force.tick` fired in d3's loop, then the `this.position.x` setter called requestAnimationFrame which caused the graphical update to run in the *subsequent* frame, one frame behind. – trusktr Oct 29 '16 at 09:18
  • Adding to my previous comment, setting `this.position.x` in the desired loop (not d3.timer's loop) does not cause a new frame to be requested, so the graphical update will happen in the same frame as `force.tick` now that I can call force.tick in this no-d3 loop. Hopefully that makes sense how I described it? I think you get the idea: better to have all logic (force.tick + graphical updating) run in a single loop, not two separate loops that may be out of sync with each other. – trusktr Oct 29 '16 at 09:25
  • @altocumulus Just wanted to double check: `force.stop` is required in the tick handler in my render loop? I followed your example, but myloop isn't a for loop, it's an rAF loop. When I don't call force.stop, there seem to be both d3's loop and my loop. Calling force.stop eliminates d3's loop. Can you check your static layout example? It might be ticking double as much as you think (it's easier to see in an rAF loop and timeline, but with your example timeline always stops before the for loop and I can't observe it). – trusktr Oct 31 '16 at 04:42
  • @altocumulus Another problem is that `node.call(force.drag)` to make the nodes draggable doesn't work any more because as soon as I let go of a node the layout freezes (due to `return true` in overriden `force.tick`?). TLDR, this is a pain in d3 v3.x. Any suggestions on that? – trusktr Oct 31 '16 at 05:19
  • @trusktr Glad you asked ;-) You should really opt for v4 if possible! The force layout got sorted out in that release with a much more distinct separation of various functions! There is no need to override `tick` any more; just call `.stop()` and you are done. The initialization will nevertheless be done as expected. Also, the drag behaviour is bundle much more loosely as of this version. I thought about this for a while but hesitated posting yet another update to my answer. But, now that you asked, please, stay tuned for another addendum covering v4. – altocumulus Oct 31 '16 at 08:45
  • Hehe, yeah, I wanted to move to v4, but the funny thing is the requirement for the project I'm on is to use v3.5.6. – trusktr Nov 03 '16 at 06:34
0

You want a Static Force Layout as demonstrated by Mike Bostock in his Block. The documentation on force.tick() has the details:

# force.tick()

Runs the force layout simulation one step. This method can be used in conjunction with start and stop to compute a static layout. For example:

force.start();
for (var i = 0; i < n; ++i) force.tick();
force.stop();

As you have experienced yourself you cannot just dismiss the call to force.start() . Calling .start() will do a lot of initialization of your nodes and links data (see documentation of nodes() and links()). The force layout won't run without it. However, this will not start the force right away. Instead, it will schedule the timer to repeatedly call the .tick() method for asynchronous execution. It is important to notice that the first execution of the tick handler will not take place before all your current code has finished. For that reason, you can safely create your own render loop by calling force.tick().


For anyone interested in the gory details of why the scheduled timer won't run before the current code has finished I suggest thoroughly reading through:

altocumulus
  • 21,179
  • 13
  • 61
  • 84
  • Based on your answer, it seems that making a render loop with `requestAnimationFrame` won't work then, if `force.tick` is scheduled regardless, then it will interfere with my own async loop. I'd like my loop to be async rather than sync. – trusktr Feb 07 '19 at 23:48
  • Or did I misunderstand what you were saying? Can I run my own async loop? (I don't remember the details of your previous answer, it was too long ago xD ) – trusktr Feb 07 '19 at 23:49
  • @trusktr You are right, doing it via `requestAnimationFrame` won't work. For that approach you can refer to my original answer which is still around (just use the DevTools to remove the `` elements to make it readable again). My analysis of the static force layout was wrong, the proposed solution will work, however. I am going to merge this answer into my previous one to clean up the mess I brought about. This might take a few days, though. – altocumulus Feb 08 '19 at 00:18
  • Cool. Thanks! It will help others out for sure. :) – trusktr Feb 08 '19 at 00:22
  • @trusktr I am afraid, the gain of knowledge here is rather slim as things are much easier now using d3 v4+, but for the sake of my own soul I will put this into a reasonable and consistent answer. – altocumulus Feb 08 '19 at 00:26
  • If you want to update the answer for v4, I wouldn't mind, if it makes it easier for you. Plus next time I try d3 I'll use the latest anyways. – trusktr Feb 08 '19 at 00:29