Path Transitions

When implementing realtime displays of time-series data, we often use the x-axis to encode time as position: as time progresses, new data comes in from the right, and old data slides out to the left. If you use D3’s built-in path interpolators, however, you may see some surprising behavior:

Why the distracting wiggle? There are multiple valid interpretations when interpolating two paths. Here’s the relevant code from the above chart:

// push a new data point onto the back data.push(random()); // pop the old data point off the front data.shift(); // transition the line path.transition().attr("d", line);

One interpretation (the one shown above) is that the y-values are changing in-place; for example, you might use this when filtering the data or transitioning between metrics. Another interpretation (the one we want) is that the change represents a sliding window in x. But how do you tell D3 to interpolate in x rather than in y?

To start, you need to understand a bit about how paths are represented in SVG. Consider this path element, which draws a polyline (a piecewise linear curve) of three points:

<path d="M0,0L1,6L2,4"></path>

The path data, stored in the d attribute, is a string which contains various commands such as moveto (M) and lineto (L). This path starts at the origin ⟨0,0⟩, draws a line segment to ⟨1,6⟩, and finally another line segment to ⟨2,4⟩; these positions are called the control points. Now say you wanted to shift the old points left and add a new point, resulting in a new path:

<path d="M0,6L1,4L2,5"></path>

The old path had three control points, and the new path has three control points, so the naïve approach is to interpolate each control point from the old to the new:

⟨0,0⟩ ↦ ⟨0,6⟩

⟨1,6⟩ ↦ ⟨1,4⟩

⟨2,4⟩ ↦ ⟨2,5⟩

Since only the y-values change, this interpretation results in a vertical wiggle. When you tell D3 to transition between two paths, it takes exactly this simple approach: it finds numbers embedded in the associated path data strings, pairs them in order, and interpolates. Thus, the transition interpolates six numbers (for the three control points) and produces the same wiggle.

To eliminate the wiggle, interpolate the transform rather than the path. This makes sense if you think of the chart as visualizing a function—its value isn’t changing, we’re just showing a different part of the domain. By sliding the visible window at the same rate that new data arrives, we can seamlessly display realtime data:

The relevant code is only slightly changed from the original excerpt:

// push a new data point onto the back data.push(random()); // redraw the line, and then slide it to the left path .attr("d", line) .attr("transform", null) .transition() .attr("transform", "translate(" + x(-1) + ")"); // pop the old data point off the front data.shift();

Here x is a quantitative scale that encodes the x-position. The value of x(-1) is about -24, which is the distance between control points in x.

When a new data point arrives, we redraw the line instantaneously and remove the previous transform (if any). The new data point is thus initially invisible off the right edge of the chart. Then, we animate the x-offset of the path element from 0 to some negative value, causing it to slide left.

While conceptually simple, there are some nuances of this approach:

First, you should use linear easing so that the speed of the continuously-changing transform remains constant. If you use the default cubic-in-out easing, then the transition velocity will oscillate and again be distracting.

Second, since the entering data point is drawn off the right edge, you’ll need a clip path. In the above example, we use:

<defs> <clipPath id="clip"> <rect width="950" height="90"></rect> </clipPath> </defs>

Lastly, if you’re using spline interpolation for the path data, then note that adding a control data point changes the tangents of the previous control point, and thus the shape of the associated segments. To avoid another wiggle when the control points are changed, further restrict the visible region (the x-domain) so that the extra control point is hidden:

If you like, you can also combine this technique with D3’s built-in axes and time scales. This chart, for example, shows your scrolling activity while reading this document over the last three minutes:

Notice that the exiting tick marks smoothly fade-out, while the entering tick marks smoothly fade-in; this is handled automatically by the axis component. The process for transitioning the axis is the same as for the transform: update the scale’s domain, then apply linear easing.