D3.js Step by Step: Animating Interactivity
Adding the ability to filter the dataset and animate the transition
UPDATE (July 18, 2016): The code and API links in these tutorials have been updated to target D3 v4, which was a complete rewrite. The D3 wiki contains a breakdown of the changes from v3.
TL;DR
This post is part of a series that explores some key concepts in D3.js by building up an example, step by step, from a bare-bones pie chart to an interactive, animated donut chart that loads external data. For the enough-with-the-jibber-jabber-show-me-the-code types out there, here's a breakdown of the steps we'll be covering:
- Step 0: Intro
- Step 1: A Basic Pie Chart (Code | Demo)
- Step 2: A Basic Donut Chart (Code | Demo)
- Step 3: Adding a Legend (Code | Demo)
- Step 4: Loading External Data (Code | Demo)
- Step 5: Adding Tooltips (Code | Demo)
- Step 6: Animating Interactivity (Code | Demo) ← You Are Here
NOTE: Because we're building things up step by step, the source code contains NEW
, UPDATED
and REMOVED
comments to annotate the lines that have been added, altered or deleted relative to the previous step.
Here we are at Step 6; we've come a long ways. Now it's time to put the cherry on top . Try clicking on the coloured squares in the legend and you'll see what I mean:
We're going to allow days of the week to be toggled on and off, and have the chart adjust itself in an animated fashion. We'll also update the tooltip so that the percent value is the percent of the weekdays currently enabled.
First, though, we'll add a title because what good is a chart without a title?
<h1>Toronto Parking Tickets by Weekday in 2012</h1>
And we'll add some style to it:
h1 {
font-size: 14px;
text-align: center;
}
We'll also center the entire chart:
#chart {
height: 360px;
margin: 0 auto; /* NEW */
position: relative;
width: 360px;
}
While we're CSSing we'll add the following, which is needed for the toggle effect on the coloured squares:
rect {
cursor: pointer; /* NEW */
stroke-width: 2;
}
rect.disabled { /* NEW */
fill: transparent !important; /* NEW */
} /* NEW */
Enough with the tedium, let's get to some code. The first thing we need to do is add an enabled
property to each entry in the dataset so that we can track which ones are on and which ones are off. We'll do this right after we cast the count
property to a number since we're already iterating through the collection:
d3.csv('weekdays.csv', function(error, dataset) {
dataset.forEach(function(d) {
d.count = +d.count;
d.enabled = true; // NEW
});
// ...
// More code
// ...
There are a couple of other house-keeping tasks to do . The first is to add a _current
property to each path
. This will be necessary for a smooth animation:
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
}) // UPDATED (removed semicolon)
.each(function(d) { this._current = d; }); // NEW
We also need to change the way we calculate the total number of parking tickets in the mouseover
event handler:
path.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return (d.enabled) ? d.count : 0; // UPDATED
}));
// ...
// More code
// ...
Instead of just extracting the count
we're now checking to see if the entry is enabled. If it isn't we return 0 instead of its usual value. If you try turning off a few days of the week in the demo, you should see the percentages in the tooltip increase.
With the set up out of the way, we can get down to business. We're going to attach a click
event handler to the coloured squares:
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color) // UPDATED (removed semicolon)
.on('click', function(label) { // NEW
// ...
// A bunch of code
// ...
}); // NEW
Let's dive into that event handler. It's a bit of a doozy:
// ...
// More code
// ...
.on('click', function(label) {
var rect = d3.select(this);
var enabled = true;
var totalEnabled = d3.sum(dataset.map(function(d) {
return (d.enabled) ? 1 : 0;
}));
if (rect.attr('class') === 'disabled') {
rect.attr('class', '');
} else {
if (totalEnabled < 2) return;
rect.attr('class', 'disabled');
enabled = false;
}
pie.value(function(d) {
if (d.label === label) d.enabled = enabled;
return (d.enabled) ? d.count : 0;
});
path = path.data(pie(dataset));
path.transition()
.duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
});
});
There's a lot going on here, so let's take it one step at a time:
this
refers to the coloured square that was clicked. We wrap it using D3'sselect()
and assign it torect
. The wrapping is done to make it easier access its attributes.- We set
enabled
to betrue
by default. - We don't want to allow all of the weekdays to be disabled, so we're going to find out how many are currently enabled so that we can exit if the user tries to break things . We calculate
totalEnabled
by iterating over the dataset, returning 1 for each enabled entry, and then summing those up. - We'll use the class disabled to flag squares that have been, well, disabled. We check if the current square is disabled and if it is, we remove the disabled class and let
enabled
remaintrue
. - If the square is enabled, we first check how many squares we previously determined were enabled. If it's less than two, we stop dead in our tracks. Otherwise we flag the square as disabled and set
enabled
tofalse
. - The next step is similar to what we did earlier for the tooltip: We're going to redefine the way
pie
retrieves values from our dataset. We first check if the entry in question has a label that matches the label of the coloured square that was clicked. If it does we take this opportunity to update theenabled
property. Then we either return thecount
or 0 based on the entry's status. - We can then update
path
by providing itsdata()
method with the updatedpie
. - The last step is to animate the transition. The
duration()
method should be be clear enough. TheattrTween()
method needs some explaining, though. The 'd' parameter specifies that it's thed
attribute that we'll be animating. This should not be confused with thed
parameter passed to the callback. (There are a lot of d's in D3 code.) The parameterd
refers to the updated data point—the one resulting from the update topie
.this
refers to the currentpath
element. That allows us to know both the current value,this._current
, which we defined earlier, and the new valued
and interpolate between them. Like the.selectAll(el).data(data).enter().append(el)
pattern we first saw in Step 1, the code in thisattrTween
callback is a pattern you'll see in many examples that involve animation.
Animation is tricky . It's one of the most—if not the most—advanced topics in D3. We've just scratched the surface here but hopefully it's enough to get you started.
This series has endeavoured to shed light on some of the fundamental ideas in D3 and, in doing so, to make it more approachable. Whatever it is that you're hoping to achieve with D3, you'll find inspiration in the gallery. If you aren't impressed with what you can find there, either you're from the future or you may have suffered some sort of brain injury, in which case I suggest visiting a hospital ASAP.
Regardless, we have reached the end of our pie/donut chart journey. Here is the final code. Review it at your leisure: