Ashley Coolman Ashley Coolman - 5 months ago 69
Javascript Question

Occasional stuttering with "stacked" transitions on `translate()` (v4.0.0-alpha40)

Problem: Jerks



I have a D3 v4.0.0-alpha.40 graph, which periodically (not on every frame):


  1. Gets new data point

  2. Removes old Data point

  3. Reset transition and set
    translate(null)

  4. Starts new
    transition()
    new scrolls across as it get new values.





// Animation
function animateOnce(period) {
// Data: Add new, and remove old
data.unshift(Math.random());
data.pop();
// Do 2 transitions...
path
.attr("d", line)
.transition() /* ...reset */
.attr("transform", null)
.duration(0)
.transition()/* ...scroll across */
.duration(period)
.ease(d3.easeLinear)
.attr("transform", `translate(${x(1)})`)
};

// Animation looper
function animate() {
setTimeout(_=> {
animateOnce(PERIOD);
animate();
},
PERIOD);
}


However the transitions don't seem to execute cleanly - every few seconds there is a jerk.

I previously had this problem with D3 v3, but I believe I fixed it by adding in the reset transition (step 3 above). Unfortunately I'm not experiences with D3 and I'm not sure how to tackle this.

See it



This jsFiddle is an approximation of my graph, and you should be able to see the occasional jerk.

Note: the fiddle uses
setTimeout
while my actual graph is react component, updated with
componentDidUpdate()
.

Edit 1: Improved using using interrupt

Improved jsFiddle

While reading docs (as @Ashitaka suggested) - I found interrupt(). This kills the transition properly and may be the "v4 way" of achieving Step 3 above (reset transition).



// Animation
function animateOnce(period) {
// Data: Add new, and remove old
data.unshift(Math.random());
data.pop();
// Do 2 transitions...
path
.attr("d", line)
.interrupt() /* ...reset */
.attr("transform", null)
.transition()/* ...scroll across */
.duration(period)
.ease(d3.easeLinear)
.attr("transform", `translate(${x(1)})`)
};


This has improved the jerks (caused I assume by competing transitions), by turning them into small stutters.

I'd like to get understand where the (I assume 1 frame) stutter is being introduced. So I'll leave this open for now.

Answer

In Mike Bostock's post Working with Transitions, he writes that:

For a given element, transitions are exclusive: only one transition can be running on the element at the same time. Starting a new transition on the element stops any transition that is already running.

Now, I detect two problems in the presented code:

  1. The path transform reset is being animated (even with 0 duration). This new transition is cancelling the previous transition. This can be fixed by changing:

    path
      .attr("d", line)
      .transition()
        .attr("transform", null)
        .duration(0)
    

    to:

    path
      .attr("d", line)
      .attr("transform", null)
    
  2. The animateOnce function is being called with the same period as D3's transition and a transition tick lasts for ~17 ms. This new transition is cancelling the previous transition as well. This can be fixed by changing:

    function animate() {
      setTimeout(_=> {
        animateOnce(PERIOD);
        animate();
      },
      PERIOD);
    }
    

    to:

    function animate() {
      setTimeout(_=> {
        animateOnce(PERIOD);
        animate();
      },
      PERIOD + 20);
    }
    

    which can be further refactored with setInterval to:

    function animate() {
      setInterval(animateOnce, PERIOD + 20, PERIOD);
    }
    

These 2 changes should solve the jank issues. Still, updating that line chart every 80 ms will always be taxing on someone's computer or smartphone. I'd advise you to only update it every 200 ms or so.

EDIT: I did some experimenting and noticed that there was still some jank on Firefox. So, a few more points to take into account:

  1. Setting the transform property to null actually creates a new layer. This can be fixed by changing:

    .attr("transform", null)
    

    to:

    .attr("transform", "translate(0)")
    
  2. The transform strings are being recreated every time the animateOnce function is called. We can precompute them outside of animateOnce and then reuse them. This can be fixed by changing:

    .attr("transform", "translate(0)")
    .attr("transform", `translate(${x(1)})`)
    

    to:

    // outside the animateOnce function:
    let startStepAttrs = { transform: "translate(0)" },
        endStepAttrs   = { transform: `translate(${x(1)})` };
    
    // inside the animateOnce function:
    .attr(startStepAttrs)
    .attr(endStepAttrs)
    
Comments