D3 Demo... D3MO? - part 1
My goal last week was to get a basic understanding of D3.js and to build a basic demo. A simple bar chart? a pie chart may be? But, the more I learned about it, I found out that it was way more powerful framework for data-binding & visualization, that to limit it to just charts felt like a crime :/ I didn’t want to build a simple chart anymore. I wanted to build a clock! Hopefully, these demos work on my blog :) (Also, this would be my first post with code. Entering new territory here on tumblr.)
Obviously D3 is a vast powerful framework, and I can’t cover all of it in a week. So, instead of claiming I learnt D3 in a week, I will instead post code & demos to showcase how far I have progressed. In this first part of the series, I’m gonna try and walk you through building the Clock Face, and along the way, introduce you to some core concepts in D3
Start with the Basics!
Let’s start by building a simple circle. Apparently, circular clocks have been all the rage with the kids since like… before the Grand Canyon was formed. Anyway, the simple way to draw a circle in HTML5 is to use SVG (Scalable Vector Graphics). I’m not gonna go into the details of SVG here, but here’s the HTML that would give us a circle.
<svg width="100" height="100">
<g transform="translate(50,50)">
<circle r="45"/>
</g>
</svg>
And here’s the D3 way of generating that. Let’s provide a simple target DIV element inside which we will generate the SVG.
DOM
<div id="vis"/>
Script 1:
var dim = 300;
var svg = d3.select("#vis")
.append("svg")
.attr("width",dim)
.attr("height",dim);
var circle = svg.append("g")
.attr("transform","translate("+[dim/2,dim/2]+")")
.append("circle")
.attr("r", (dim/2) * .9);
Demo 1:
Selectors
If you are familiar with JQuery or CSS Selectors, you already know how selectors work, and how powerful they can be in quickly applying a function on a list of elements. D3 works the same way, except the API is designed in such a way that every function returns a reference to the Selection it was applied on as well, so that you can chain calls gracefully like “attr()” above. The only exception is when the function creates a new selection, in which case that selection is returned.
So, in the above code, d3.select(“#d3vis1”) returns the selection for the div element, following which, append(“svg”) returns the selection containing the newly created svg element.
Okay, it’s kinda cool that I don’t have to do document.getElementById() all over the place, and the having every function return the selector it was applied on makes chaining calls easy… But I could do that with JQuery, and I don’t see what’s the hype about D3. Let’s explore something a bit more complex to understand D3’s most powerful feature…
Bindings.
Now instead of just one circle, let’s draw 12 circles.
First, the iterative way.
var svg = d3.select("#vis")
.append("svg")
.attr("width",dim)
.attr("height",dim);
for (i = 0; i < 12; i++) {
svg1.append("g")
.attr("transform","translate("+[i*dim/12 + dim/24,dim/2]+")")
.append("circle")
.attr("r", (dim/2) * .9);
}
and the D3 way…
var svg = d3.select("#vis")
.append("svg")
.attr("width",dim)
.attr("height",dim);
var circles = svg.selectAll("g")
.data(d3.range(12))
.enter().append("g")
.attr("transform",function(d){
return "translate("+[(d)*dim/12 + dim/24,dim/2]+")";
})
.append("circle")
.attr("r", (dim/2) * .9 / 12);
Demo 2:
It may be hard to see the benefits of this approach right away, but notice how we were able to bind specific values of “d” (which is a range going from 0..11), and create elements for each of those values automatically, and in the end, automatically get the selection of all “circles” that were created in one step, even though they are a bunch of second-level elements from the node where data binding was applied. It may take a bit of effort in the beginning, but once you start wrapping you mind around it, coding with D3 becomes a thing of beauty. (Nerdgasm!)
Let’s make things a bit more complicated and create a clock face. Let’s draw the 12 inside a circle for the hour markers. And instead of having a for loop, lets do a simple binding operation on an array containing the numbers 1 through 12 to generate our circles.
Script 3:
var svg = d3.select("#vis")
.append("svg")
.attr("width",dim)
.attr("height",dim);
var g = svg.append("g")
.attr("transform", "translate(" + [dim/2, dim/2] + ")");
var circles = g.each(function(d) {
d3.select(this).call(clockFace, 12, dim/2);
}).selectAll("circle");
function clockFace(selection, numDots, size) {
offset = 0;
radius = numDots * size / (numDots + 2);
deltaY = (numDots % 2 == 0)?0:(radius / 2) * (1 - Math.cos(Math.PI / numDots));
selection.selectAll("g")
.data(d3.range(numDots))
.enter().append("g")
.attr("transform",function(d) {
var angle = d * 360 / numDots + offset;
return "translate(0,"+deltaY+")rotate("+angle+")translate("+radius+")rotate("+ -angle + ")";
})
.append("circle").attr("r",0.9*(size)/(numDots));
}
Demo 3:
Notice how easy it was to get a reference to all the circles you just created. Anyway, to draw the second/minute markers, we just need to call the same clockFace function with 60… Right?
Script 4:
var hourMarkers = g.each(function(d) {
d3.select(this).call(clockFace, 12, dim/2);
}).selectAll("circle");
var secondMarkers = g.each(function(d) {
d3.select(this).call(clockFace, 60, dim/2);
}).selectAll("circle");
Demo 4:
Wait a minute! What happened there? Why are there only 48 second dots, instead of 60?
That’s because we are using the enter() data-binding function in our code, which defines how to create objects for additional data-points that don’t have backing DOM elements from the results of the selection. So, in our case, there were already 12 circles created which get skipped.
One simple way to fix this is to have two separate top-level graphics contexts created, and draw into each one, so you won’t find the 12 existing ones during the second call.
Script 5:
var gHours = svg.append("g")
.attr("transform", "translate(" + [dim/2, dim/2] + ")");
var gSeconds = svg.append("g")
.attr("transform", "translate(" + [dim/2, dim/2] + ")");
var hourMarkers = gHours.each(function(d) {
d3.select(this).call(clockFace, 12, dim/2);
}).selectAll("circle");
var secondMarkers = gSeconds.each(function(d) {
d3.select(this).call(clockFace, 60, dim/2);
}).selectAll("circle");
Demo 4:
Let me also introduce you to the function(d,i) signature for biding data to functions. So, function(d) signature can be used on attr() as well as call() functions to bind each array element in data d to each sekection element. You can also use the function(d,i) signature to get the array index passed in as well. And if there’s no data bound to the selection, you can still use function(d,i) to get the index of the element in your selection. (Basically, you have bound null data to the operation)
So, let’s try and make every 3rd hour marker a different color than the rest.
Script 6:
secondMarkers.attr("fill","#aaa");
hourMarkers.attr("fill",function(d,i){return (i == 0)?"#666":(i % 3 == 0)?"#666":"#aaa";});
Demo 6:
In the next part, we’ll explore how to apply transitions, and update elements by binding new data to them. i.e, build the clock hands and bind them to the current time.