SVG Path animations with D3.js

Here we will tween an arc from one radius to another

SVG, D3.js, Javascript, Canvas, HTML5

03/27/2012

I recently stumbled on an incredible HTML5 graphics library called d3. It's well documented, powerful, smooth, and pretty. The problem is, most of the folks that use it well are apparently geniuses, so support for learning from the ground up can be a bit limited. There are plenty of examples out there on how to build a bar chart, but not so many on how to do generic things like "apply a transition to property X of my SVG path."

I spent half a day trying to learn how to tween the start endAngle of an arc, and I usually find that writing out an explanation of something difficult helps me to understand it better, so I figured I'd use that as an example of how to do a basic drawing in d3, and animate it. Here goes:

All you need to get started in d3 is to include the library (download it or use the github hosted version), include a div where you can insert your svg elements, and include the script with the code you want to write. Here's my setup:


<!DOCTYPE html>
<html>
  <head>
    <title>Arc Tween</title>
    <script type="text/javascript" src="d3.js"></script>
  </head>
  <body>
    <div id="chart"></div>
    <script type="text/javascript" src="arctween.js"></script>
  </body>
</html>

Now, for my arctween.js file:

The first thing I do is declare a few variables about the screen size and the radius of my arc:


var w = 960, //canvas width
    h = 700, //canvas height
    r = Math.min(w, h) / 1.8; //radius of the circle my arc will follow
    s = .09; //arc spacing

Next, I declare a variable for the arc itself:


var arc = d3.svg.arc()
    .startAngle(0)
    .endAngle(function(d) { return d.value * 2 * Math.PI; })
    .innerRadius(function(d) { return r; })
    .outerRadius(function(d) { return (d.index + s) * r; });

Here, I'm calling d3's arc function, which needs a little info: start angle, end angle, inner, and outer radius. You'll see my start angle is 0, but my end angle references something called d.value. My project will eventually be set up to draw multiple arcs of different radii and end angles, so I want to store all that info somewhere. For the simplicity of this example, my data will only include information for a single arc. I'll show you that code in a second, but first, let's write the code to actually add the arc to the screen:


var vis = d3.select("#chart").append("svg")
    .attr("width", w)
    .attr("height", h)
    .append("g")
    .attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");

var g;

g = vis.selectAll("g")
    .data(buildPercBars)
    .enter().append("g")
    .attr("class", "arc");

g.append("path")
    .style("fill", "93cfeb")
    .attr("d", arc)

I create a variable called "vis" which will contain my data visualization. I tell vis to be appended to the svg element of my chart div, and set its width and height. Then I append an element "g". G is the standard dom selector for svg elements which need to be grouped. I will be creating a grouping for each arc I build, and within that grouping, there will be a path. The data for this grouping will coming from a function, in this case called buildPercBars. I will give that grouping a class name of "arc". Within that grouping, I add the path, and give the path a color. D3 allows you to specify colors in many ways - in this case I'll just use the hex code for a light blue.

Now, I can show you the function buildPercBars, where my data is contained:


// Generate the percentage bars
function buildPercBars() {

    return [
        {value: .45,  index: .5},
    ];

}

All I'm doing here is building a data set for my arcs, in this case just 1 arc. The arc has a value of .45 - which in this example means that I will be drawing an arc that is 45% of a full circle. It has an index of .5, which means its radius will be 50% of the total available radius / width of my canvas.

So that's all you need to do to draw an arc in d3! Animating that arc is a little trickier though. I come from a flash background, where all we'd have to do is call some tweening library's tween function, and pass it the properties we want to tween. Not so (yet) with SVG. First, let's add something that will trigger the animation. In this case, a key press:


window.addEventListener("keypress", selectArcs, false);

Now, to execute the animation:



function selectArcs() {
    d3.selectAll("g.arc > path")
        .each(arcTween);
}

function arcTween(){
    d3.select(this)
        .transition().duration(1000)
        .attrTween("d", tweenArc({ value : 0 }));
}

function tweenArc(b) {
    return function(a) {
        var i = d3.interpolate(a, b);
        for (var key in b) a[key] = b[key]; // update data
        return function(t) {
            return arc(i(t));
        };
    };
}


First, I get a reference to all of my arcs, and for each of them, I call the arcTween function. d3 can get a reference to DOM elements very similarly to jQuery - just pass the selector name (all g elements with a class of "arc") and optionally specify a child element ("path").

In my arcTween function, I call d3's transition function, and give it a length of 1000 milliseconds to complete the transition. Then I call d3's attrTween function, a powerful function which can tween pretty much any property of your path you wish to control. In this case, I'll be just tweening the data value from .45 (or 45% of the circle), back to zero, so I call a function I've entitled tweenArc, and pass it a value of zero.

In the tweenArc function, I pass a parameter, b, which will represent the value to tween to. This will return a value of "a" - my new arc end angle. I declare a variable i which will be the interpolated value as a function of time that d3 will use while animating my arc from its current endAngle (.45) to zero. So each time the interpolation is called, we'll be taking the old end angle, animating to the new end angle, and updating the arc on the screen, until we reach our final value of zero.

And that's what you do to animate in d3. Sounds tricky, but keep in mind for simpler shapes, d3 has a lot of built in functions that are much simpler to animate, such as style properties, like color, height, width, etc, or attribute properties, like radius. Good luck!