I’ve been trying to get to grips with d3 – the Data Driven Documents data visualisation library for javascript. The best way to learn a thing is to write code, so that’s just what I did. Online examples can only get you so far and eventually you have to make a break, go out on your own, and copy someone else’s ideas.
Last Sunday I went to go see The Martian – the science-heavy jokeathon from Matt Damon and Ridley Scott. I’m a Brit in New York so right now it’s all movie theater instead of cinema, sidewalk instead of pavement. Brown slime instead of coffee – that kind of thing.
So I went to the movie theater and thoroughly enjoyed Damon’s performance except for the entire dialog, which will age worse than a geriatric in winter. “I’m going to science the shit out of this” says 2015 like “I’m SO over you” says 90’s Friends.
Anyway, so one cool thing was the heads-up display inside the Mars surface suits the team wore – displaying wearer biometrics and a bunch of other cool stuff that definitely did not distract me from the awesome plot and crummy dialog. There’s a couple screeners on hudsandguis.com, but not much in the way of content. Just go see the movie, I guess.
Long story short: I built the circular percentage readout for Damon’s suit pressure in d3. It was fun. This is what it looks like, and here’s the code.
function CircularProgress(element, settings){ var duration = settings.duration || 500; var w = settings.width || 200; var h = settings.height || w; var outerRadius = settings.outerRadius || w/2; var innerRadius = settings.innerRadius || (w/2) * (80/100); var range = settings.range || {min: 0, max: 100}; var fill = settings.fill || "#F20100"; var format = settings.format || function(num){return num + "%";} var svg = d3.select(element) .append("svg") .attr("width", w) .attr("height", h); var arc = d3.svg.arc() .innerRadius(innerRadius) .outerRadius(outerRadius);
We’re using javascript’s comical this-function-is-really-an-object-definition syntax to declare a CircularProgress
type with an implicit constructor that takes two arguments – a DOM
element and a settings collection – and then declares a bunch of variables that represent some facts about the visual representation of the class.
The d3.select
invocation is a special spell that creates an svg element that d3 knows about inside our HTML DOM, with the specified width and height.
arc
is actually a function, which converts radians data into an svg arc. It’s where the real wizardry happens, and we’ll come back to it later. More code!
var paths = function(numerators) { return numerators.map(function(numerator){ var degrees = ((numerator - range.min) / (range.max - range.min)) * 360.0; var radians = degrees * (Math.PI / 180); var data = {value: numerator, startAngle: 0, endAngle: radians}; return data; }); }
paths is a function that takes a number (in the example we’re using it’s a percentage), and figures out the angle in radians, from zero, that the arc should cover. This is the beans of the code – given a number and a range (0-100 for a percentage, or 35-40 for body temperature in celcius, or whatever), this function outputs the correct dimensions of the arc.
var g = svg.append('g').attr('transform', 'translate(' + w / 2 + ',' + h / 2 + ')'); //initialise the control g.datum([0]).selectAll("path") .data(paths) .enter() .append("path") .attr("fill", fill) .attr("d", arc) .each(function(d){ this._current = d; }); svg.datum([0]).selectAll("text") .data(paths) .enter() .append("text") .attr("transform", "translate(" + w/2 + ", " + h/1.6 + ")") .attr("text-anchor", "middle") .text(function(d){return format(d.value)});
An SVG g
element is just a named group – meaning you can modify disparate elements together. In this case we’re moving the drawn elements into the centre of the SVG element.
Then we initialise the control to 0, or whatever the range minimum is. In d3
we bind the data ‘0’ to an array of SVG path
elements. You might notice that currently our document doesn’t contain any paths – but that’s not a problem for d3, no sir! One is created for us and then – because it’s a newly-created element, the enter()
function is bound to the new path. Inside enter
, we set a bunch of attributes on the path, including the arc created by the arc
function. Phew!
Finally, we perform a similar operation for the text. We want the text to be right in the middle of the circle, and a little lower than halfway – this means the centre of the text is roughly in the centre of the circle. It’s a little crumby, but 1.6 is where it’s at. We’re actually setting the body of the text element to the return value of an anonymous function. The function takes the struct returned from the call to paths and returns the value:
var data = {value: numerator, startAngle: 0, endAngle: radians}
Then what? Well, we want a live control that (a) accepts updates and (b) transitions the progress arc nicely between data items, rather than jerking from one to the next. The update function, declared as an object member using this
, achieves this:
this.update = function(percent) { g.datum(percent).selectAll("path") .data(paths) .transition() .duration(duration) .attrTween("d", arcTween); svg.datum(percent).selectAll("text") .data(paths) .text(function(d){return format(d.value);}); }; var arcTween = function(initial) { var interpolate = d3.interpolate(this._current, initial); this._current = interpolate(0); return function(next) { return arc(interpolate(next)); }; } };
The arcTween
function is called once for each new data value. It creates a new function, bound to a value called interpolate
, which is the linear interpolation of numbers from the previous value to the new value. Then, we persist the new value in the DOM, so it can represent the previous value the next time we call update. Finally, we return a function which allows d3
to interpolate values of next
from 0-1 as the transition is played through.
The code, along with a bunch of examples, are available on http://bl.ocks.org/sammoorhouse/