« Kevin Ngo

Swan Dive! Into d3.js

26 Mar 2012

Today, I started experimenting with d3.js. A fortune-teller hinted to me that I would be doing a lot of data visualization work in the near-future and that d3.js would come in handy. Since my grades for last term just came out today, I figured graphing my GPA over the last three years would be a nice toy start. It would also be a nice visualization for me to reflect on the last three years since I’m done with school for the next six months.

What is d3.js?

d3.js is a small Javascript library for ‘manipulating documents based on data’. d3 stands for data-driven documents. More specifically, d3.js is used to bind data and apply data-driven transformations to DOM elements. However, it is not a visualization framework loaded with visualization functions. It is more of a tool that focuses on efficient manipulation of documents based on data. Because it’s simply a tool, it can be used to build whatever kind of data visualizations or graphs needed.


Let’s draw a useless circle

d3 makes it easy to add SVG elements and manipulate them with its jQuery-esque method chaining. We’ll go through the usual most basic stuff. Let’s draw a pretty blue and orange circle.

// create an SVG canvas, specifying its position and size
var demo = d3.select('#demo').
    append('svg:svg').
    attr('x', 500).
    attr('y', 500).
    attr('width', 120).
    attr('height', 120);

// draw a circle on the canvas specifying its color, radius, position
demo.append('svg:circle').
    style('stroke', 'blue').
    style('fill', 'orange').
    attr('r', 40).
    attr('cx', 50).
    attr('cy', 50);

And boom, a useless circle.

We can make it do some useless stuff like event-driven (hover) CSS3 transitions. We’ll have it expand and shrink on mouseover and mouseout respectively.

demo.append('svg:circle').
    style('stroke', 'blue').
    style('fill', 'orange').
    attr('r', 40).
    attr('cx', 50).
    attr('cy', 50).
    on('mouseover', function(){
        d3.select(this).
        transition().
            delay(100).
            duration(1000).
            attr('r', 10)
    }).
    on('mouseout', function() {
        d3.select(this).
            transition().
                delay(100).
                duration(1000).
                attr('r', 100)
    });

Try hovering over this baby.

Working with data, a simple GPA bar chart

d3.js without actual data is pretty pointless. The documentation explains everything in better detail, but for a ‘case study’ let’s build a simple GPA bar chart. First, we need to define our data.

var gpa_term_data = [
{'term': 'F09', 'gpa': 3.40},
{'term': 'W10', 'gpa': 3.32},
{'term': 'S10', 'gpa': 3.56},

{'term': 'F10', 'gpa': 3.32},
{'term': 'W11', 'gpa': 3.57},
{'term': 'S11', 'gpa': 3.71},

{'term': 'F11', 'gpa': 3.72},
{'term': 'W12', 'gpa': 3.92},
];

In this case, we want GPA by term. So we create a list of objects holding the data. Next, we’ll set up the chart itself, its size, position, and scales.

var barWidth = 70;
var chartWidth = (barWidth + 10) * gpa_term_data.length;
var chartHeight = 200;
var padding = 20;

// scales
var xScale = d3.scale.linear().
    domain([0, gpa_term_data.length]).
    range([0, chartWidth]);

var yScale = d3.scale.linear().
    domain([3, d3.max(gpa_term_data, function(datum) {
        return datum.gpa;
    })]).
    range([0, chartHeight]);

d3 has scale functions that return functions. In our definition, the domain is about the data, and the range is more about the number of pixels the data is mapped to. For X, the domain is are the terms and for Y, the domain is the GPA. We’ll only care about GPA over 3.0 so we set the minimum as such. We have the domain map to the range linearly, and we can use the returned function to help position data around the chart. Next we’ll create the actual bars.

// create rectangle for each datum
chart.selectAll('rect').
    data(gpa_term_data).
    enter().
    append('svg:rect').
    attr('x', function(datum, index) {
        // return x-offset pixels
        return xScale(index);
    }).
    attr('y', function(datum) {
        // return y-offset pixels, subtract due to origin being in top left
        return chartHeight - yScale(datum.gpa);
    }).
    attr('height', function(datum) {
        return yScale(datum.gpa);
    }).
    attr('width', barWidth).
    attr('fill', '#2d578b');

Here, we use a common pattern, selectAll.data.enter.append. selectAll selects all elements that match (like jQuery), here we select SVG rectangles, but we haven’t created any rectangles yet. Well if we use data() and pass in our data, we can create them on-the-fly. When we pass in data that don’t have any elements to bind to, the data is considered leftover and is stored in the enter selection. So the data isn’t binded yet, but if we call enter(), enter will take all the leftover data and automatically create elements out of them. We are thus effectively binding our data to rectangle elements, the heart of d3.js!. There is a function called exit() that does the opposite, remove elements that don’t have any data binded, but it isn’t used here. Finally, actually create and append the elements onto the chart.

While we’re creating the rectangles, we specify their size and position. We make use of Javascript anonymous functions which d3 expects to take a datum and an optional index argument. The datum is the data binded to the element and the index is the, well, index among the data set. For the x-position, we call our scale function we created earlier to get the pixel offset. We do similar for y, but since the coordinate system features the origin on the top-left, we subtract it from the chartHeight. The height of the bar is set to be the GPA, and that’s the meat of it.

We do similar things to position the axes and text (check the source of this page to see) except we have to pad the chart to make room for the axes. We eventually have the finished bar chart.


Over the next few days, I’ll be doing more things with d3.js. In the future, I hope to do data visualization work with Cyder and at Mozilla. It’s just so pretty and fun!