d3 / d3-transition

Animated transitions for D3 selections.
https://d3js.org/d3-transition
ISC License
224 stars 64 forks source link

Transition start gets delayed if execution is stalled before it is created (and d3-timer clock has started) #73

Closed magjac closed 7 years ago

magjac commented 7 years ago

See this block: https://bl.ocks.org/magjac/29911a2c697813463475c2f39c8d720a

The expected behavior is that the red box shall transition smoothly from the smaller black box to the larger during 6 seconds. However, it gets 3 seconds delayed because execution is stalled for 3 seconds before it is created and therefore jumps from start to half-done. If either the d3-timer has not been started (by calling d3.now()) or there is no stalling, the transition works as expected.

I'm not sure if this is a bug in d3-transition or in d3-timer. See also related issues https://github.com/d3/d3-timer/issues/26 and https://github.com/d3/d3-timer/issues/27 and the fix in https://github.com/d3/d3-timer/pull/28.

magjac commented 7 years ago

The reason for this behavior is that selections.transition() calls d3.now() here and that d3.now() here returns the clockNow (if it isn't the first time d3.now() is called) computed on the last wake, from which the current time has advanced substantially.

mbostock commented 7 years ago

This is the intended behavior.

If you schedule multiple timers (or transitions) within one pass of the “event loop” (i.e., before a queued promise microtask is invoked), those timers have the same reference time. We must capture the reference time to synchronize the timers.

The surprise here is that the reference time is captured lazily when you call d3.now or, by extension, d3.timer, selection.transition, etc. So by calling d3.now before waste, the reference time changes. (The same behavior would apply if you scheduled transitions both before and after waste.)

It would be better if D3 could somehow capture the reference time before your code runs. But it can’t, because it doesn’t control the browser event loop. Your code runs here on load, but in general your code could run for a variety of reasons, and D3 has no way of intercepting your code before it runs to capture the reference time.

An alternative would be for D3 to capture the reference time in the microtask, rather than when d3.now (and d3.timer, selection.transition, etc.) is called. If we did that, then in your example the transition would always run for the six seconds following the end of the event loop, rather than the six seconds following the start (or middle) of the event loop. But I’m not sure it’s desirable to do this, because in another common case, a reference time is set when timers are invoked (see the assignment to clockNow in wake)), so that you can schedule new timers during this invocation and the new timers are scheduled consistently relative to the old timers.