Working with Transitions

D3’s selection.transition method makes it easy to animate transitions when changing the DOM. For example, to change the text color to red instantaneously, you can select the body element and set the color style:

d3.select("body").style("color", "red");

To instead animate the change over time, derive a transition:

d3.select("body").transition().style("color", "red");

This ease-of-use comes from D3’s transition interface mirroring the selection interface: nearly everything you can do to a selection of elements, you can also do to a transition, causing the change to be animated over time rather than instantaneous.

Yet looks can be deceiving because selections and transitions are not perfectly equivalent: transitions provide only a subset of selection functionality. These nuances are due to the long-running, asynchronous nature of transitions. Before you attempt more complicated uses of transitions, it is helpful to understand the system’s underlying design.

Note: This article discusses transitions as implemented in version 3.0, which was released in December 2012. You can download D3 3.0 from d3js.org or GitHub.

#Transitions Are a Form of Animation

Transitions are a limited form of key frame animation with only two key frames: start and end. The starting key frame is typically the current state of the DOM, and the ending key frame is a set of attributes, styles and other properties you specify. Transitions are thus well-suited for transitioning to a new view without complicated code that depends on the starting view.

Look again at this example transition:

d3.select("body").transition().style("color", "red");

Although only a single attribute value is specified—the color red—a second value is implied. This starting value is computed from the DOM via getComputedStyle or getAttribute.

In some cases, the computed starting value may not be what you want. For example, it can be different than what was set previously due to browsers converting values to canonical representations, say converting the color string "red" to "rgb(255,0,0)". Depending on the interpolator you use (more on that in a bit), this may cause the transition to stutter. As another example, transparent colors are reported as "rgba(0,0,0,0)" (transparent black), which is treated as opaque black when using RGB interpolation.

One way to make the starting value more explicit is to set it in the DOM before creating the transition. This won’t fix problems related to value conversion, but it can be useful if the inherited or default value isn’t what you want.

d3.select("body")
    .style("color", "green") // make the body green
  .transition()
    .style("color", "red"); // then transition to red

If the transition has a delay, then the starting value should be set only when the transition starts. You can do this by listening for the start event:

d3.select("body").transition()
    .delay(750)
    .each("start", function() { d3.select(this).style("color", "green"); })
    .style("color", "red");

The most explicit approach to set the starting value is to use transition.styleTween. This skips computing the starting value from the DOM by specifying the both values and an interpolator.

d3.select("body").transition()
    .styleTween("color", function() { return d3.interpolate("green", "red"); });

This last variation gives a hint as to how transitions work under the hood. When you use transition.style, D3 creates a style tween for you automatically by retrieving the starting value from the DOM and constructing an interpolator to the specified ending value. If you want to override the start value or the interpolator, do this using styleTween, attrTween or tween.

#Transitions Interpolate Values over Time

Given start and end key frames, how do we get from here to there? To perform a smooth animated transition, D3 needs to know how to interpolate—or blend—from a given starting value to its corresponding ending value. The d3.interpolate method determines an appropriate interpolator by inferring a type for each pair of starting and ending values. D3 supports several common types, including:

The string interpolator is particularly useful: it finds numbers embedded within strings, pairing up numbers in the starting string with the corresponding number in the ending string. Those numbers are interpolated separately, and the resulting string is then reconstructed. String interpolators have myriad applications, such as interpolating path data (e.g., "M0,0L20,30") and CSS font specifications (e.g., "300 12px/100% Helvetica").

String interpolation isn’t always appropriate. If the starting and ending path data have a different number of control points, for example, it’s no longer meaningful to pair up numbers. Instead, you need to resample the path prior to interpolation (or apply more advanced shape blending algorithms). Likewise, arcs require interpolation in polar coordinates so that the angles are interpolated, rather than the positions.

If you want to implement your own interpolator, you need a function that takes a single argument t ranging from 0 to 1. For t = 0, the interpolator returns the starting value; for t = 1, the interpolator returns the ending value; and for values in-between, the interpolator returns a blending value. For example, to interpolate two numbers:

function interpolateNumber(a, b) {
  return function(t) {
    return a + t * (b - a);
  };
}

When interpolating to or from zero, some interpolated values may be very small. JavaScript formats small numbers in exponential notation, which unfortunately is not supported by CSS. For example, when transitioning opacity to fade in or out, the number 0.0000001 is converted to the string "1e-7" and then ignored, giving the default value of 1! To avoid distracting flicker, start or end the transition at 1e-6 rather than 0; this is the smallest value not formatted in exponential notation.

Some Things Cannot Be Interpolated

When modifying the DOM, use selections for any changes that cannot be interpolated; only use transitions for animation. For example, it is impossible to interpolate the creation of an element: it either exists or it doesn’t. Furthermore, in order to schedule a transition, the element must exist and so element creation cannot be deferred to start. Selection methods related to data joins (data, enter, exit) and creating elements (append, insert) can therefore only be performed on selections.

When using transitions in conjunction with the general update pattern, first perform the data-join on selections using enter and exit. Then derive transitions on each subselection as desired.

var bar = svg.selectAll(".bar")
    .data(data, function(d) { return d.key; });

bar.enter().append("rect")
    .attr("class", "bar")
    … // initialize entering bars

bar.transition()
    … // transition entering + updating bars

bar.exit().transition()
    … // transition exiting bars
    .remove();

For convenience, there are a few exceptions to this rule. You can transition.remove to remove an element at the end of the transition; likewise, transition.text sets the text content at the start of the transition, without interpolating. In the future, transitions may support additional non-interpolatable operations, such as classed and html.

#The Life of a Transition

As with concurrent programming, perhaps the trickiest aspect of transitions is that they happen over time rather than instantaneously. The code does not proceed in a single straightforward path, as when the page loads, but as a complex sequence of recurring callbacks. While you can safely ignore this complexity in many cases, you must understand the rules which govern the evaluation of transitions if you want to harness their full power.

Transitions have a four-phase life cycle:

  1. The transition is scheduled.
  2. The transition starts.
  3. The transition runs.
  4. The transition ends.

A transition is scheduled when it is created: when you call selection.transition, you are scheduling a transition. This is also when you call attr, style, and other transition methods to define the ending key frame. Scheduling happens in your code (for example, in response to the user clicking a button), meaning that the code so far is fully synchronous. This makes it easier to debug, and easier to use ending values that depend on changing global state, such as a scale’s domain.

A transition starts based on its delay, which was specified when the transition was scheduled. If no delay was specified, then the transition starts as soon as possible, which is typically after a few milliseconds. The start event is then dispatched, and the transition initializes its tweens, which may involve retrieving starting values from the DOM and constructing interpolators. Deferring the initialization of tweens to start is necessary because starting values aren’t known until the transition starts. Therefore, if you use attrTween, styleTween and other tween methods, keep in mind that your code will be evaluated asynchronously when the transition starts!

While the transition runs, its tweens are repeatedly invoked with values of t ranging from 0 to 1. In addition to delay and duration, transitions have easing to control timing. Easing distorts time, such as for slow-in and slow-out. Some easing functions may temporarily give values of t greater than 1 or less than 0; however, the ending time is always exactly 1 so that the ending value is set exactly when the transition ends. A transition ends based on the sum of its delay and duration. When a transition ends, the tweens are invoked a final time with t = 1, and then the end event is dispatched.

#Transitions Are per-Element and Exclusive

Each element transitions independently. When you create a transition from a selection, think of it as a set of transitions, one per element, rather than a single mega-transition running on multiple elements. Different elements can have different delays and duration, and even different easing and tweens. Additionally, transition events are dispatched separately for each element. When you receive an end event for a given element, its transition has ended, but other transitions may still be running on other elements.

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. Interrupting a transition on the element has no effect on other elements, and multiple transitions can run concurrently on different elements. While only one transition can be running simultaneously on the element, multiple transitions can be scheduled. For example, you can schedule successive transitions using transition.transition, which creates a new transition whose delay immediately follows the existing transition.

For each element, sequences of transitions only advance forward in time. Transitions are assigned a monotonically-increasing identifier (id) when they are scheduled; each new transition id is greater than the last. When a transition starts on the element, it can only run if the transition is newer than whatever previously ran on the same element. Thus, starting a new transition implicitly cancels any previous transitions—even if those old transitions have not yet started. This design eliminates the need to cancel transitions explicitly. An excellent illustration of this behavior is the stacked-to-grouped bar transition. This uses two chained transitions: the bars first slide right and narrow; then the bars drop to the baseline. The first transition also has a staggered delay. If you quickly toggle between stacked and grouped, notice the old transition is only interrupted when the new one starts, not when the new one is scheduled.

Similar to how data is bound to an element’s __data__ property, transitions are bound to a __transition__ property. When a transition is first scheduled on an element, this property is created; when the last scheduled transition ends, this property is likewise deleted. Inspecting this property in the console can be useful to debug which transitions are scheduled to run on which elements, as well as to inspect computed tweens and transition timing parameters. Because transitions are bound to elements, you can also reselect elements within a transition and modify tweens or timing. This is most common in conjunction with component-driven transitions, such as the axis component; use post-selection to customize the appearance of elements after invoking the component.

#Additional Reading

This tutorial covered most of the important details in the mechanics of transitions. I omitted an explanation of transition inheritance using transition.each; perhaps I’ll cover that in the future, though see #400 for an example. For more on how transitions should be used (and not just implemented), see my earlier post on object constancy and Heer & Robertson’s excellent paper, “Animated Transitions in Statistical Data Graphics”.