Show Buttons
Share On Facebook
Share On Twitter
Share On Google Plus
Share On Linkdin
Share On Reddit
Share On Stumbleupon
Contact us
Hide Buttons

Creating an Animated Ring or Pie chart in d3js

animated_ring_d3js

 

In our pre­vi­ous arti­cle, we learnt how to cre­ate a sim­ple ani­mated bar graph. In this arti­cle, we will tackle some­thing thats a lit­tle more inter­est­ing — cre­at­ing a an ani­mated ring. We will fol­low a step-by-step process to cre­ate a sim­ple ani­mated ring whose con­tents change based upon the options you select from a dropdown.

DEMO

TLDR: These are the major steps towards achiev­ing our goal

d3_animated_ring

Data and Dimentions

We begin by cre­at­ing some vari­ables that will be used by our drawAn­i­mat­edRingChart function.

// Data: Average age of employees in an organization
var employees = [
    {dept: 'A', count : 22},
    {dept: 'B', count : 66},
    {dept: 'C', count : 25},
    {dept: 'D', count : 50},
    {dept: 'E', count : 27}
];
var maxWidth = 200;
var maxHeight = 200;
var outerRadius = 100;
var ringWidth = 20;

D3.layout.pie

The main point to remem­ber here is that a ring in d3 is just a vari­a­tion of the pie chart with the fol­low­ing difference -


While a pie chart only requires an outer radius (the inner radius default­ing to 0), a ring has a non-zero value for both — outer radius and inner radius.


That said, lets start defin­ing our func­tion respon­si­ble for draw­ing the ring.

function drawAnimatedRingChart(config) {
    var pie = d3.layout.pie().value(function (d) {
        return d.count;
    });
}

In the above snip­pet we cre­ated a pie func­tion that can extracts the ‘count’ prop­erty of a data array and return a cor­re­spond­ing array of objects with each object hav­ing two new attrib­utes — the startAngle and the endAngle. This new array can then be con­sumed by another func­tion to draw a sec­tion of the pie — the arc function.

We need two more func­tions that will help us draw the actual pie — one to define the col­ors of our pie chart, the other is our arc function.

var color = d3.scale.category10();
var arc = d3.svg.arc();

d3.scale.catgory10 is a con­ve­nience func­tion that cre­ates an array of 10 colors.

d3.svg.arc is the meaty func­tion here. The arc func­tion under­stands the out­put array of d3.svg.pie and and can draw an arc shape based upon the star­tAn­gle and endAn­gle of the objects in that array.

Since we want to draw a ring, we will need to set both — the outer and inner radius pro­vided in the arguments.

arc.outerRadius(config.outerRadius || outerRadius)
        .innerRadius(config.innerRadius || innerRadius);

If you remem­ber the exam­ple above, since we we will draw dif­fer­ent rings based upon the item cho­sen in the drop­down, its impor­tant to clear the svg of its pre­vi­ous con­tents before attempt­ing a redraw.

// Remove the previous ring
d3.select(config.el).selectAll('g').remove();
var svg = d3.select(config.el)
    .attr({
        width : maxWidth,
        height: maxHeight
    });

Draw­ing the arc

With some ini­tial setup out of the way, its a great time to start draw­ing our ring.

We first cre­ate a group for each arc to be drawn.

// Add the groups that will hold the arcs
var groups = svg.selectAll('g.arc')
    .data(pie(config.data)) // Data binding
    .enter()
    .append('g')
    .attr({
        'class': 'arc',
        'transform': 'translate(' + outerRadius + ', ' + outerRadius + ')'
    });

Notice that we used pie(config.data) when bind­ing our data. We did not directly asso­ciate our raw data array employees with the groups, instead we trans­formed it such that each data point that is bound to the DOM will now posess a star­tAn­gle and an endAn­gle — thanks to the pie func­tion[ Tip: Learn how data bind­ing works in d3 ].

In the next step, we draw the pie and assign it some color.

// Create the actual slices of the pie
groups.append('path')
    .attr({
        'fill': function (d, i) {
            return color(i);
        }
        'd': arc // Use the arc to draw the ring sections
    });

And with the above snip­pet, we are finally able to draw a ring, albeit with­out ani­ma­tion. Notice that we cre­ated a path with an attribute called d. Thats because in order to draw the actual sec­tion of a ring, the star­tAn­gle and endAn­gle attrib­utes need to be trans­formed into svg coor­di­nates in a way that a path can be drawn around it and that task is per­formed by the pie function.

JSFIDDLE DEMO

Ani­mat­ing the ring

In order to cre­ate the ani­ma­tion, we will need tweak our code a bit.

First we need to cre­ate a func­tion that knows how to gen­er­ate dif­fer­ent val­ues of the end angle for each arc over a period of time. In d3, such func­tions that gen­er­ate time depen­dent val­ues are called interpolators.

function tweenPie(finish) {
    var start = {
        startAngle: 0,
        endAngle: 0
    };
    var interpolator = d3.interpolate(start, finish);
    return function(d) { return arc(interpolator(d)); };
}

In the above code, we cre­ated an inter­po­la­tor func­tion i that can gen­er­ate inter­me­di­ate val­ues between a start and fin­ish object. The arc func­tion then con­sumes these inter­me­di­ate val­ues to ren­der the state of the arc over a period of time, thereby caus­ing an ani­mated effect.

Here’s how you’d use the tweenPie function.

groups.append('path')
    .attr({
        'fill': function (d, i) {
            return color(i);
        }
    })
    .transition()
    .duration(config.duration || 1000)
    .attrTween('d', tweenPie);

There are three impor­tant points to notice here.

  • We invoked a tran­si­tion func­tion on our selector.
  • We spec­i­fied a dura­tion of the transition.
  • Instead of using attr we now use attrTween and pass it our tweenPie func­tion defined earlier

JSFIDDLE DEMO

That pretty much achieves what we set out to do. Now lets give it a final touch.

Adding labels

Adding our label to a chart is pretty easy. All you would need to do is add the fol­low­ing snippet.

groups.append('text')
    .attr({
        'text-anchor': 'middle',
        'transform': function (d) {
            return 'translate(' + arc.centroid(d) + ')';
        }
    })
    .text(function (d) {
        // Notice the usage of d.data to access the raw data item
        return d.data.dept;
    });

We just trans­lated the text to the cen­ter of each ring sec­tion and then set the text. Notice that we had to use d.data because if you remem­ber from ear­lier d in our case was the out­put of the pie function.

There is how­ever one prob­lem with the above snip­pet. The labels dont seem to show up. The rea­son is that due to the ani­ma­tion in the ring, it gets drawn on top of the labels. To get this work­ing, we will need to draw the lables after the entire ani­ma­tion is complete.

In D3, As of this writ­ing the end event for a tran­si­tion is fired for each ani­mated item in the selec­tion. So the best way to know when the entire ani­ma­tion is com­plete is through ref­er­ence count­ing. So, we are going to define a lit­tle call­back func­tion out­side of our ring func­tion that we will then bind to the end event of every func­tion to do the count­ing for us.

// This function helps you figure out when all
// the elements have finished transitioning
// Reference: https://groups.google.com/d/msg/d3-js/WC_7Xi6VV50/j1HK0vIWI-EJ
function checkEndAll(transition, callback) {
    var n = 0;
    transition
    .each(function() { ++n; })
    .each("end", function() {
        if (!--n) callback.apply(this, arguments);
    });
}

function drawAnimatedRingChart(config) {
    ...
    ...
    groups.append('path')
    .attr({
        'fill': function (d, i) {
            return color(i);
        }
    })
    .transition()
    .duration(config.duration || 1000)
    .attrTween('d', tweenPie)
    .call(checkEndAll, function () {
        // Finally append the title of the text to the node
        groups.append('text')
        .attr({
            'text-anchor': 'middle',
            'transform': function (d) {
                return 'translate(' + arc.centroid(d) + ')';
            }
        })
        .text(function (d) {
            // Notice the usage of d.data to access the raw data item
            return d.data.dept;
        });
    });
}

And thats how you get the labels to appear on top of the UI after the ani­ma­tion ends.

FINAL JSFIDDLE DEMO


And thats all there is to it. Found some­thing incor­rect or want to see some­thing improved? Let us know in
the comments.


  • Brian

    A func­tion called i?.….“we cre­ated an inter­po­la­tor func­tion i that”.… you could really do with using more suit­able function/variable names. This is confusing

    • tuto­ri­al­hori­zon

      Fixed! thank you.