Tutorial: Introduction to D3

Check out the final result here!

D3 is a brand new visualization framework created by Mike Bostock. It is the successor of the successful great visualization framework Protovis. There are a few differences and similarities with Protovis. One of the most important differences is that in D3 you work more directly with SVG which gives you much greater flexibility than Protovis. Also, the performance of D3 is much better than Protovis, especially with animation, because in D3 only the properties that are changing are updated, instead of re-rendering the entire visualization. And once you dive into D3, it’s really easy to pick up, so let’s get started!

The example we’re going to work on is really simple and will show you some of the basic concepts of D3. Basically we just plot hidden circles randomly on the screen, and then transition them to a portion of the screen. Then we add some interaction to it so that the circles will move once you move your mouse over them.

The first thing you need to do is to make a reference to D3 from your HTML page. You can download D3, or make a link to a stable version on GitHub. The reference I used for this tutorial is: https://github.com/mbostock/d3/raw/v1.8.2/d3.js

To get started we generate some data that we want to bind the circles to. All we do here is just generate a bunch of x and y values and put them in an array:

var data = []
for (i=0; i < 1000; i++) {
    data.push({"x": Math.random(), "y": Math.random()})
}

Now we need to add an SVG element to the body of the page. This is how you do that:

var h = 1000
var vis = d3.select("body")
    .append("svg:svg")
    .attr("width", screen.width)
    .attr("height", screen.innerHeight)

First the body is selected using d3.select("body") which is a similar selection you would do with jQuery selectors. The h variable is the height I want to use. I refer to it later in the code. Next we’re going to plot the invisible circles:

var x = d3.scale.linear().domain([0,1]).range([screen.width / 2 - 400,screen.width / 2 + 400]),
y = d3.scale.linear().domain([0,1]).range([0,h]),
r = d3.scale.linear().domain([0,1]).range([5,10]),
c = d3.scale.linear().domain([0,1]).range(["hsl(250, 50%, 50%)", "hsl(350, 100%, 50%)"]).interpolate(d3.interpolateHsl)

vis.selectAll("circle")
	.data(data)
	.enter().append("svg:circle")
	.attr("cx", function(d) { return x(d.x) })
	.attr("cy", function(d) { return y(d.y) })
	.attr("stroke-width", "none")
	.attr("fill", function() { return c(Math.random()) })
	.attr("fill-opacity", .5)
	.attr("visibility", "hidden")
	.attr("r", function() { return r(Math.random()) })

First we create some d3.scale variables. Since we’re working with random data, we need to convert the output of Math.random() outputs to screen positions, radii and colors. We use linear scales, and you see that the domain is [0,1] for all the scales. That’s because the output of Math.random() yields a number between 0 and 1. The range tells us to which range the domain should be converted. Each of these variables is actually a function, so you can use r for example as a function to convert a number of the domain to a number of the range, so for example: r(.5) will result in 7.5. The c variable will turn any value between 0 and 1 to a color.

Next we select all the circle elements, which is an empty collection at this time. We bind our data to this collection with the .data(data) statement, and then use the .enter().append("svg:circle") statement so that each element in our data array will be bound to a new svg:circle element. Here we set various properties: cx for the x-position of the circle (note that in SVG position 0,0 is the top left corner). The cy is used for the y-position of the circle, the r is used for the radius of the circle. The other properties speak for themselves. Note that a difference with Protovis is that you cannot use the shorthand way for using a function: function(d) x(d.x), but in D3 you have to write out the return keyword and the curly braces: function(d) { return x(d.x) }.

Now, when you view this page, you’ll see nothing, because we’ve set the .attr("visibility", "hidden"). If you want to see the results so far, just remove this line. Next we want to move all those hidden circles to their new random position, so that together they will form the colorful bar. This is the code that does just that:

var y2 = d3.scale.linear().domain([0,1]).range([h/2 - 20, h/2 + 20])
var del = d3.scale.linear().domain([0,1]).range([0,1])

d3.selectAll("circle").transition()
	.attr("cx", function() { return x(Math.random()) })
	.attr("cy", function() { return y2(Math.random()) })
	.attr("visibility", "visible")
	.delay(function(d,i) { return i * del(Math.random()) })
	.duration(1000)
	.ease("elastic", 10, .45)

First we need 2 extra scales to convert random numbers: y2 which is the scale that converts a random number to a new range. The del scale will be used for the delay of the transition. We now first select all the circle elements and we start a transition(). We set a few properties of the transition itself: delay, duration and ease, and also the properties of the selected circles elements that we want to animate. The cx, visibility and cy properties of the circle will all be animated. All you have to do is provide the end state for the properties you want to animate. The animation will last 1000 milliseconds, there will be some delay for each circle before starting the transition, and we apply an elastic easing function to create the nice elastic bouncing effect. Go see what you’ve got so far! Fun, isn’t it?

The last part will be a little extension of the piece of code we used to add the circles initially, so change your code so that it looks like this:

vis.selectAll("circle")
	.data(data)
	.enter().append("svg:circle")
	.attr("cx", function(d) { return x(d.x) })
	.attr("cy", function(d) { return y(d.y) })
	.attr("stroke-width", "none")
	.attr("fill", function() { return c(Math.random()) })
	.attr("fill-opacity", .5)
	.attr("visibility", "hidden")
	.attr("r", function() { return r(Math.random()) })
	.on("mouseover", function() {
		d3.select(this).transition()
		.attr("cy", function() { return y2(Math.random()) })
		.delay(0)
		.duration(2000)
		.ease("elastic", 10, .3)
	})

A mouseover event has been added to each of the circles. And basically we’re doing something similar again: just add a transition, set some transition properties and the properties of the selected elements. In this case d3.select(this) selects the current circle so that you apply the transition to the current selected circle.

That’s it!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>