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:

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:

  1. this refers to the coloured square that was clicked. We wrap it using D3's select() and assign it to rect. The wrapping is done to make it easier access its attributes.
  2. We set enabled to be true by default.
  3. 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.
  4. 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 remain true.
  5. 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 to false.
  6. 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 the enabled property. Then we either return the count or 0 based on the entry's status.
  7. We can then update path by providing its data() method with the updated pie.
  8. The last step is to animate the transition. The duration() method should be be clear enough. The attrTween() method needs some explaining, though. The 'd' parameter specifies that it's the d attribute that we'll be animating. This should not be confused with the d parameter passed to the callback. (There are a lot of d's in D3 code.) The parameter d refers to the updated data point—the one resulting from the update to pie. this refers to the current path element. That allows us to know both the current value, this._current, which we defined earlier, and the new value d and interpolate between them. Like the .selectAll(el).data(data).enter().append(el) pattern we first saw in Step 1, the code in this attrTween 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:

comments powered by Disqus