D3 in Depth covers versions 6 and 7 of D3

Home About Newsletter
NEWSLETTER

Get book discounts and receive D3.js related news and tips.

Subscribe

D3 Transitions

D3 transitions let you smoothly animate between different chart states. This article shows how to add transitions to selection updates, how to set transition duration, how to create staggered transitions, how to change the easing function, how to chain transitions and how to create custom tween functions.

For example, let's join an array of 5 numbers to SVG circles. When the 'Update data' button is clicked, the data is randomised and the circles jump to new positions:

If we add a transition to the circles the circles move smoothly to their new locations:

Furthermore, entering circles (circles that have just been created) and exiting circles (circles that are about to be removed) can be transitioned in specific ways. In this example new circles fade in and exiting circles drop down the page:

How to create a D3 transition

Creating a basic transition using D3 is fairly straightforward. Suppose you have the following code which joins an array of random data to circle elements:

let data = [];

function updateData() {
data = [];
for(let i=0; i<5; i++) {
data.push(Math.random() * 800);
}
}

function update() {
d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.attr('r', 40)
.attr('cx', function(d) {
return d;
});
}

function updateAll() {
updateData();
update();
}

updateAll();

If you're not familiar with the above code I suggest reading the data joins chapter.

The updateAll function is called when a button is clicked:

<button onClick="updateAll();">Update data</button>

updateAll calls updateData which randomises the data array. It also calls update which joins data to circle elements.

When the button is clicked the circles jump to new locations:

You can then add a .transition() call before the .attr and .style methods that you wish to transition:

function update() {
d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.attr('r', 40)
.transition()
.attr('cx', function(d) {
return d;
});
}

In this example, the .transition call is followed by a single .attr call. This results in the cx attribute of the circles transitioning into new positions:

The .transition method returns a 'transition selection'. This is basically the same as a normal D3 selection, except the .attr and .style methods animate attributes and style. A transition selection has additional methods such as .tween which we'll cover later.

Once the .transition method has been called, subsequent calls to .attr and .style will animate attributes and style, respectively.

For example, let's randomise the radius and colour of each circle:

...

function random(x) {return Math.floor(Math.random() * x);}

function updateData() {
data = [];
for(let i=0; i<5; i++) {
data.push({
x: random(800),
r: random(40),
fill: d3.rgb(random(255), random(255), random(255))
});
}
}

...

In the update function we update the radii and fills after the .transition call:

function update() {
d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.transition()
.attr('cx', function(d) {
return d.x;
})
.attr('r', function(d) {
return d.r;
})
.style('fill', function(d) {
return d.fill;
});
}

Now when the data updates, the position, radius and colour of each circle transitions:

Duration and delay

You can change the duration of a transition by calling .duration after the .transition call. The .duration method accepts an argument which specifies the duration in milliseconds:

d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.attr('r', 40)
.transition()
.duration(2000)
.attr('cx', function(d) {
return d;
});

In this example we've set the duration to 2000 milliseconds:

You can also specify a delay before which the transition starts:

...
.transition()
.delay(2000)
.attr('cx', function(d) {
return d;
});

The delay is usually used to delay each element in the selection by a different amount. For example, you can create staggered transitions by passing a function into .delay and setting the delay to a multiple of the element index i:

d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.attr('r', 40)
.transition()
.delay(function(d, i) {
return i * 75;
})
.attr('cx', function(d) {
return d;
});

Easing functions

An easing function defines the change in speed of an element during a transition. For example, some easing functions cause the element to start quickly and gradually slow down. Others do the opposite (start slowly and speed up) while others define special effects such as bouncing.

D3 has a number of built in easing functions which you can try out in this explorer:

In general 'in' refers to the start of the motion and 'out' refers to the end of the motion. Therefore, easeBounceOut causes the element to bounce at the end of the transition. easeBounceInOut causes the element to bounce at the start and end of the transition.

It's usually better to use easings where the element moves quickly at first and slows down. For example easeCubicOut is a commonly used easing function where the element moves quickly at first and slows down. D3's default easing function is easeCubic which is equivalent to easeCubicInOut. This causes the element to start slowly, accelerate, then finish slowly.

To select an easing function use the .ease method and pass in the easing function (e.g. d3.easeBounce):

d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.attr('r', 40)
.transition()
.ease(d3.easeBounceOut)
.attr('cx', function(d) {
return d;
});

Chained transitions

Transitions can be chained together by adding multiple calls to .transition. Each transition takes it in turn to proceed. (When the first transition ends, the second one will start, and so on.)

For example, let's chain two transitions. The first sets the cx attribute and the second sets the r attribute (and uses easeBounce):

function update() {
d3.select('svg')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cy', 50)
.transition()
.attr('cx', function(d) {
return d.x;
})
.transition()
.duration(750)
.ease(d3.easeBounce)
.attr('r', function(d) {
return d.r;
});
}

When the chart updates, the circles move to their new positions, then the radius changes (with a bounce):

Custom tweens

In most circumstances D3 is able to transition attributes and style in a sensible manner. However there are times where the default behaviour might not be suitable.

For example, suppose we join an array of angle values to circle elements. Each circle is placed on the circumference of a larger circle using the joined value to determine where on the circle it sits. An angle of zero corresponds to 3 o'clock on the large circle.

When the data updates, the circles travel in a straight line:

You can customise the path the elements take (such as along the circumference of the large circle) by using a tween function.

To use a tween function, call the .tween function at some point after .transition. You pass a name (which can be anything you like) and a function into .tween.

The function gets called once for each element in the selection. It must return a tween function which will get called at each step of the transition. A time value t between 0 and 1 is passed into the tween function. (t is 0 at the beginning of the transition and 1 at the end.)

d3.select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.transition()
.tween('my-tween', function(d) {
...

// Return a tween function
return function(t) {
// Do stuff here such as updating your HTML/SVG elements
}
});

Typically you'll set up interpolator function(s) in the outer function. An interpolator function takes a parameter between 0 and 1 and interpolates between two given values. The interpolator function can be used when updating HTML/SVG elements in the tween function.

For example:

let data = [], majorRadius = 100;

function updateData() {
data = [Math.random() * 2 * Math.PI];
}

function getCurrentAngle(el) {
// Compute the current angle from the current values of cx and cy
let x = d3.select(el).attr('cx');
let y = d3.select(el).attr('cy');
return Math.atan2(y, x);
}

function update() {
d3.select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('r', 7)
.transition()
.tween('circumference', function(d) {
let currentAngle = getCurrentAngle(this);
let targetAngle = d;

// Create an interpolator function
let i = d3.interpolate(currentAngle, targetAngle);

return function(t) {
let angle = i(t);

d3.select(this)
.attr('cx', majorRadius * Math.cos(angle))
.attr('cy', majorRadius * Math.sin(angle));
};
});
}

In the function that's passed into .tween we start by computing currentAngle.

We then set up an interpolator function i. This is a function that interpolates between two values (currentAngle and targetAngle in our case). The function i accepts a number between 0 and 1 and interpolates between the two values.

Next we create a tween function and return it. The tween function uses the interpolator function i to compute angle for the given time value t. It then updates the cx and cy attributes using angle. The tween function is called for each step of the transition.

Now when the button is clicked, the circle moves along the circumference of the large circle. Notice also that when the button is rapidly clicked, the transition starts from the circle's current (mid-transition) position:

Entering and exiting elements

You can define specific transitions for entering and exiting elements. (Entering elements are newly created elements and exiting elements are ones that are about to be removed.)

For example you might want entering circles to fade in and exiting circles to fall:

To define enter and exit transitions you need to pass three functions into the .join method. This was covered in the Enter, exit and update chapter so if you're not familiar with these concepts I suggest you read up on them there.

As a brief reminder, you can pass three functions (instead of an element name) into the .join method:

.join(
function(enter) {
...
},
function(update) {
...
},
function(exit) {
...
}
)

The first, second and third functions are named the enter, update and exit functions, respectively.

Entering elements

As covered in Enter, exit and update chapter the enter function should append an element to each element of the enter selection.

You can also set the style and attributes of entering elements and this has the effect of initialising elements before any transitions take place.

Let's initialise cy, cx, r and opacity on entering circles:

.join(
function(enter) {
return enter
.append('circle')
.attr('cy', 50)
.attr('cx', function(d) {
return d;
})
.attr('r', 40)
.style('opacity', 0);
},
function(update) {
return update;
},
function(exit) {
return exit.remove();
}
)

The initial x and y coordinates of entering circles are initialised so that the circles appear in place (rather than sliding in from the origin).

The initial radius of entering circles is initialised so that the circles appear with a radius of 40.

Finally the opacity of entering circles is initialised to 0 so that entering circles fade in. (We'll set the final opacity later on.)

Exiting elements

The exit function usually removes the elements in the exit selection:

.join(
...
function(exit) {
return exit.remove();
}
)

If you don't call .remove on the exit selection, the elements will remain on the page.

You can add a transition to exiting elements by calling .transition on the exit selection. For example, to make exiting elements fall off the page, use .transition and set the cy attribute to a large value:

.join(
function(enter) { ... },
function(update) { ... },
function(exit) {
return exit
.transition()
.attr('cy', 500)
.remove();
}
)

The .remove method of a transition selection removes the selection's element(s) when the transition ends.

Updating elements

You can apply a transition to entering and updating elements by adding a call to .transition after the .join call. This will cause all elements (except exiting elements) to transition.

Let's add a .transition call and set the cx attribute and the opacity:

d3.select('svg')
.selectAll('circle')
.data(data)
.join(
function(enter) { ... },
function(update) { ... },
function(exit) { ... }
)
.transition()
.attr('cx', function(d) {
return d;
})
.style('opacity', 0.75);

This has the following effect:

  • when the data changes, the circles transition to new positions (because of .attr('cx'))
  • when new circles are created, they fade in

The circles fade in because their initial opacity is set to zero in the enter function and the .style call animates the opacity to 0.75.

Putting it all together

Here's the complete example where:

  • circles fade in (without sliding or expanding)
  • circles transition to new positions
  • circles fall off the page when they exit
d3.select('svg')
.selectAll('circle')
.data(data)
.join(
function(enter) {
return enter
.append('circle')
.attr('cy', 50)
.attr('cx', function(d) { return d; })
.attr('r', 40)
.style('opacity', 0);
},
function(update) {
return update;
},
function(exit) {
return exit
.transition()
.attr('cy', 500)
.remove();
}
)
.transition()
.attr('cx', function(d) { return d; })
.style('opacity', 0.75);

To summarise:

  • entering elements are initialised in the enter function. This is first function passed into .join. This allows you to initialise style and attributes of entering elements (before the transition starts)
  • a transition is applied to exiting elements in the exit function. This is the third function passed into .join.
  • entering and existing elements are transitioned by adding a call to .transition after .join

The update function (the second argument of .join) only updates existing elements (and not entering elements) and I rarely add any special code here.

BOOKS & COURSES
D3 Start to Finish book cover

Visualising Data with JavaScript teaches you how to build charts, dashboards and data stories using Chart.js, Leaflet, D3 and React.

Find out more

"One of the best D3 books I've read. The contents are very clear, it is easy to follow and the concepts are very solid."

Javier García Fernández

Learn how to make a custom data visualisation using D3.js.

Find out more

Learn the fundamentals of HTML, SVG, CSS and JavaScript for building data visualisations on the web.

Find out more