Storing Data in Objects
In the tabloid cover example, I mentioned that it helps to structure your data arrays so that each element contains one and only one data point. For our tabloid data for the month of January we have:
var janData = [ {name:"Angelina Jolie", covers:20, rank:1}, {name:"Brad Pitt", covers:18, rank:2}, {name:"Jennifer Aniston", covers:10, rank:3}, {name:"Brittney Spears", covers:8, rank:4} ];
Now, let’s take a look at our data array for our population bar chart:
var popData = [1.6, 1.5, 2.1, 2.6, 3.4, 4.5, 5.1, 6.0, 6.6, 7.1, 7.3, 8.1, 8.9, 8.8, 8.6, 8.8, 9.3];
It also has one and only one data point for each entry. But do you notice a difference between the two? In the case of janData, each entry has three pieces of information associated with it: a name, a cover count, and a rank. In popData, on the other hand, each entry only contains a single number. The names of the age groups associated with these values are stored in a separate array:
var barLabels = ["80 and up", "75-79", "70-74", "65-69", "60-64", "55-59", "50- 54","45-49", "40-44", "35-39", "30-34", "25-29", "20-24", "15-19", "10-14", "5-9", "0-4"];
There’s something a little dicey about this. We’re relying on ourselves to remember that the data starts with the 80 and up age group and gets progressively younger. But what if we were looking at this code months from now and couldn’t remember that? Or what if we decided to change the order of the bars and put the 0-4 bar on top? We would either have to f lip the arrays around (something we could easily make a mistake doing) or we would have to change the way we set the y attribute of the bars so that the final entry went on top and the first entry went on the bottom. Neither of these methods is ideal.
Instead, we can mimic what we did with janData. We can create an array of objects, where each object has descriptive information about each data point as well as the value:
var popData = [ {age:"80 and up", value:1.6, position:0}, {age:"75 - 79", value:1.5, position:1}, {age:"70 - 74", value:2.1, position:2}, {age:"65 - 69", value:2.6, position:3}, {age:"60 - 64", value:3.4, position:4}, {age:"55 - 59", value:4.5, position:5}, {age:"50 - 54", value:5.1, position:6}, {age:"45 - 49", value:6.0, position:7}, {age:"40 - 44", value:6.6, position:8}, {age:"35 - 39", value:7.1, position:9}, {age:"30 - 34", value:7.3, position:10}, {age:"25 - 29", value:8.1, position:11}, {age:"20 - 24", value:8.9, position:12}, {age:"15 - 19", value:8.8, position:13}, {age:"10 - 14", value:8.6, position:14}, {age:"5 - 9", value:8.8, position:15}, {age:"0 - 4", value:9.3, position:16} ]
I’ve included a property called position to indicate where in the chart I want each of the final bars to be displayed (see Figure 5.9).
Figure 5.9 Joining with objects
Here’s how it works when we create our rectangles using this new array. We start the same way as we did before:
barGroup.selectAll("rect") .data(popData) .enter().append("rect")
and the x and height attributes still stay the same:
barGroup.selectAll("rect") .data(popData) .enter().append("rect") .attr("x", 0) .attr("height", barHeight)
But when we set the width and y attributes, it works a little differently. Why? Because d is different. Instead of just being equal to a number for each rectangle, d is now equal to an entire object. So, for the first rectangle, d is equal to {age:"80 and up", value:1.6, position:0}, and for the second rectangle, d is equal to {age:"75 - 79", value:1.5, position:1}, and so on.
If you set the width equal to d, D3 doesn’t know what to do. But the good news is we can treat d just as we would any other object in JavaScript. If we want to access any of its properties, we just type d followed by a period followed by the property name. So, d.age, or d.value.
That in mind, here’s how we can set the width attribute for the rectangles in our bar chart:
.attr("width", function(d){ return d.value })
And here is how we can set the y attribute:
.attr("y", function(d){ return d.position * barSpacing })
And here is the whole chain, reordered:
barGroup.selectAll("rect") .data(popData) .enter().append("rect") .attr("x", 0) .attr("y", function(d){ return d.position * barSpacing }) .attr("height", barHeight) .attr("width", function(d){ return d.value });
Now, we can change the order of bars however we like by changing the position property in popData. And we can be sure that the right values will always stay with the right age groups.
Again, I encourage you to try to build the rest of the chart with popData as an array of objects. The entire code is shown in Listing 5.3 in case you get stuck.
Listing 5.3 Building the chart with our data stored in an array of objects
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { font-family: Helvetica; } svg { width:500px; height:500px; } .top-label { font-size: 13px; font-style: italic; text-transform: uppercase; float: left; } .age-label { text-align: right; font-weight: bold; width: 90px; padding-right: 10px; } .clearfix { clear: both; } .bar { fill: DarkSlateBlue; } .bar-label { text-anchor: end; } .axis-label { text-anchor: middle; font-size: 13px; } </style> </head> <body> <!-- --> <script src="http://d3js.org/d3.v3.min.js"></script> <script> var popData = [ {age:"80 and up", value:1.6, position:0}, {age:"75 - 79", value:1.5, position:1}, {age:"70 - 74", value:2.1, position:2}, {age:"65 - 69", value:2.6, position:3}, {age:"60 - 64", value:3.4, position:4}, {age:"55 - 59", value:4.5, position:5}, {age:"50 - 54", value:5.1, position:6}, {age:"45 - 49", value:6.0, position:7}, {age:"40 - 44", value:6.6, position:8}, {age:"35 - 39", value:7.1, position:9}, {age:"30 - 34", value:7.3, position:10}, {age:"25 - 29", value:8.1, position:11}, {age:"20 - 24", value:8.9, position:12}, {age:"15 - 19", value:8.8, position:13}, {age:"10 - 14", value:8.6, position:14}, {age:"5 - 9", value:8.8, position:15}, {age:"0 - 4", value:9.3, position:16} ]; var axisData = [0, 2.5, 5.0, 7.5]; var width = 400, leftMargin = 100, topMargin = 30, barHeight = 20, barGap = 5, tickGap = 5, tickHeight = 10, scaleFactor = width / popData[16].value, barSpacing = barHeight + barGap, translateText = "translate(" + leftMargin + "," + topMargin + ")", scaleText = "scale(" + scaleFactor + ",1)"; var body = d3.select("body"); body.append("h2") .text("Age distribution of the world, 2010"); body.append("div") .attr("class", "top-label age-label") .append("p") .text("age group"); body.append("div") .attr("class", "top-label") .append("p") .text("portion of the population"); body.append("div") .attr("class", "clearfix") var svg = body.append("svg"); var barGroup = svg.append("g") .attr("transform", translateText + " " + scaleText) .attr("class", "bar"); barGroup.selectAll("rect") .data(popData) .enter().append("rect") .attr("x", 0) .attr("y", function(d) {return d.position * barSpacing}) .attr("width", function(d) {return d.value}) .attr("height", barHeight); var barLabelGroup = svg.append("g") .attr("transform", translateText) .attr("class","bar-label"); barLabelGroup.selectAll("text") .data(popData) .enter().append("text") .attr("x",-10) .attr("y", function(d) {return d.position * barSpacing + barHeight*(2/3)}) .text(function(d) {return d.age}); var axisTickGroup = svg.append("g") .attr("transform", translateText) .attr("stroke", "black"); axisTickGroup.selectAll("line") .data(axisData) .enter().append("line") .attr("x1", function(d) {return d*scaleFactor}) .attr("x2", function(d) {return d*scaleFactor}) .attr("y1", 0) .attr("y2", -tickHeight); var axisLabelGroup = svg.append("g") .attr("transform", translateText) .attr("class", "axis-label"); axisLabelGroup.selectAll("text") .data(axisData) .enter().append("text") .attr("x", function(d) {return d*scaleFactor}) .attr("y", -tickHeight - tickGap) .text(function(d) {return d + "%"}); </script> </body> </html>