Recently I started a new job at the University of Louisville, and along with that I have had the opportunity to start the data visualization process of huge project at the school right now. One of the visualizations that were required by the lead of the project was a Radial Bar Chart (Polar Area Diagram). And with this being a web portal into the project, we needed something that would allow us to create visualizations on the web – enter d3.js.
You can find all of the source code for the tutorial in working HTML/JS files in the below link:
Polar Area Diagram Source Code
D3 provides a rich javascript library for data visualization. Unfortunately, as I began to do more research, I noticed that no one had created a Polar Area Diagram. Of course, this being a requirement, I have had to sit down and create a way of creating this chart. Fortunately, it wasn’t nearly as difficult as I had initially thought, it was just a culmination of various pie charts combined with different outer radii, but I thought I would write something up to help someone else get started on something similar if it is needed.
Here’s the data creation for the charts discussed here:
var randomFromTo = function randomFromTo(from, to){ return Math.random() * (to - from) + from; }; timeseries = []; sdat = []; keys = ["x", "y", "z", "w", "u", "t"]; for (j = 0; j < time; j++) { series = [[]]; for (i = 0; i < axis; i++) { series[0][i] = randomFromTo(minVal,maxVal); } for (i=0; i<=numticks; i++) { sdat[i] = (maxVal/numticks) * i; } timeseries[j] = series; } |
I’m going to assume that if you are reading this, you at least have a little bit of knowledge of the D3 libarary. As with all charts in D3, we start by creating an SVG tag (or there are ways of using the canvas object as well).
var w = 400; var h = 400; viz = d3.select("#radial") .append('svg:svg') .attr('width', w) .attr('height', h); viz.append("svg:rect") .attr('x', 0) .attr('y', 0) .attr('height', 0) .attr('width', 0) .attr('height', 0); vizBody = viz.append("svg:g") .attr('id', 'body'); |
From here we’ll have to do a little bit of mathematics to create the radial (circular) axis ticks. First we’ll create a padding for the image, and we’ll also, in this example, fix the maximum value to 6. We’ll also transform the image so that our centroid of the chart is located in the middle instead of the top-left corner of the image.
var vizPadding = { top: 25, right: 25, bottom: 25, left: 25 }; var maxVal = 6; //need a circle so find constraining dimension heightCircleConstraint = h - vizPadding.top - vizPadding.bottom; widthCircleConstraint = w - vizPadding.left - vizPadding.right; circleConstraint = d3.min([heightCircleConstraint, widthCircleConstraint]); // create a function for calculating the radius of a point from the center radius = d3.scale.linear().domain([0, maxVal]) .range([0, (circleConstraint / 2)]); radiusLength = radius(maxVal); //attach everything to the group that is centered around middle centerXPos = widthCircleConstraint / 2 + vizPadding.left; centerYPos = heightCircleConstraint / 2 + vizPadding.top; vizBody.attr("transform", "translate(" + centerXPos + ", " + centerYPos + ")"); |
Now we can start adding our bars to the charts, or if we want the axes to exist behind the bars, we could add those next. Up to now, I have assumed you have some knowledge of the d3.scale and creation of the svg object. I stay with some of those assumptions, but I’ll explain the bars in a little more depth here. First, the code:
pie = d3.layout.pie().value(function(d) { return d; }).sort(null); d = []; for(i = 0; i < timeseries[val][0].length; i++) { d.push(1); } groups = vizBody.selectAll('.series') .data([d]); groups.enter().append("svg:g") .attr('class', 'series') .style('fill', "blue") .style('stroke', "black"); groups.exit().remove(); bar = d3.svg.arc() .innerRadius( 0 ) .outerRadius( function(d,i) { return radius( timeseries[val][0][i] ); }); arcs = groups.selectAll(".series g.arc") .data(pie) .enter() .append("g") .attr("class", "attr"); arcs.append("path") .attr("fill", "blue") .attr("d", bar) .style("opacity", 0.4); |
Now here is where we use a pie chart in a slightly different way. You’ll notice we created our pie object near the top of this code. Of course, since we have six axes, each one will likely have a different height. The d variable does exactly this. We’ll use this variable – which is just an array length of 6 where each element contains the value 1. What this does is ensure that the angle of the “bars” that we are creating are equal.
The next bit of code for the group variable simply loads the data and creates a <g> tag in the svg that we can place our paths. Just like a d3 pie chart, we’re going to create an arc(). However, we are going to change the outer radius of the pie wedge to match with the radius of our data’s value. Since we created the radius() previously, we can use this function to calculate the outer radius of this wedge. Instead of using the d value that gets passed into the function that is calculating the radius, we’re going to pass this function the timeseries variable data for that wedge – i.
From there everything is the same as a standard pie chart. Simply pass the bar object to the path’s d attribute as shown and we get bars.
We can also add axes, text, and circular ticks if we’d like, as shown below:
var radialTicks = radius.ticks(numticks), lineAxes; vizBody.selectAll('.line-ticks').remove(); lineAxes = vizBody.selectAll('.line-ticks') .data(keys) .enter().append('svg:g') .attr("transform", function (d, i) { return "rotate(" + ((i / axis * 360) - 90) + ")translate(" + radius(maxVal) + ")"; }) .attr("class", "line-ticks"); lineAxes.append('svg:line') .attr("x2", -1 * radius(maxVal)) .style("stroke", ruleColor) .style("fill", "none"); lineAxes.append('svg:text') .text(function(d,i){ return keys[i]; }) .attr("text-anchor", "middle"); vizBody.selectAll('.circle-ticks').remove(); circleAxes = vizBody.selectAll('.circle-ticks') .data(sdat) .enter().append('svg:g') .attr("class", "circle-ticks"); circleAxes.append("svg:circle") .attr("r", function (d, i) { return radius(d); }) .attr("class", "circle") .style("stroke", ruleColor) .style("fill", "none"); circleAxes.append("svg:text") .attr("text-anchor", "left") .attr("dy", function (d) { return -1 * radius(d); }) .text(String); |
When everything is created, we should get a radial bar graph that looks like the chart below! Then you can just play around with the styling a bit more. You can add better looking headers for the text, align the axis text to be centered over the bar (with a little bit of trigonometry), and even build the bars backwards (inner radius is the “timeseries” value, and outer radius is the maximum Value) so that a gradient can be added to the “bars”. But hopefully this gets someone started on a graph like this.
Use these graphs sparingly! They don’t always show what you might want them to show, and comparing differences between bars is difficult in circular form. In most cases, I would recommend using bar charts. For more information on when to use various graphs, Stephen Few’s work in information visualization would be a good place to start.