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 iseaseCubic
which is equivalent toeaseCubicInOut
. 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.