Using a Data-Join to Make a Bar Chart
In the previous chapter, we remade our global age distribution bar chart in JavaScript, using D3 selections. We put the results in a file called pop2010-D3.html, which you should take a moment to review now. For the rest of the chapter, we’re going to create that same bar chart again, using a data-join.
To do this, we can use a lot of the same code from pop2010-D3.html. Create a new file called pop2010-data-joins.html and cut and paste the code in Listing 5.1 from pop2010-D3.html:
Listing 5.1 pop2010-data-joins.html
<!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 = [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], axisData = [0, 2.5, 5.0, 7.5], 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"]; var width = 400, leftMargin = 100, topMargin = 30, barHeight = 20, barGap = 5, tickGap = 5, tickHeight = 10, scaleFactor = width / popData[16], 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") .style("width", leftMargin + "px") .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"); </script> </body>
This includes everything leading up to the code we used for actually creating the bars of the bar chart, which we did using a for loop. Here’s what that code looks like:
for (var i = 0; i < popData.length; i++) { barGroup.append("rect") .attr("x", 0) .attr("y", i * barSpacing) .attr("height", barHeight) .attr("width", popData[i]); };
Recall that popData is an array of our age distribution data, where the first entry is the percentage of people who are 80 years old and older, followed by those between 75 years and 79, then 70 to 74, etc.
In the code above, the for loop goes through one iteration for each value in the popData array—17 in total. With every iteration, it appends a new SVG rectangle to the barGroup group element. To barGroup itself, we’ve applied a transform attribute designed to position and size the bars correctly: The entire group is shifted to the right 100 pixels to leave a margin for the bar labels and scaled horizontally so the widest bar will be exactly 400 pixels wide.
Good. But what if we wanted to draw those bars using a data-join instead? Let’s retrace the same steps we went through with our tabloid covers. Load pop2010-data-joins.html in your browser. Figure 5.6 presents what you should see.
Figure 5.6 Before the data-join
Now, we want to make some rectangles enter the page. And, specifically, we want them to be part of the group barGroup (which is shown in the DOM in Figure 5.6). So what we need to do is create a selection out of all of the rectangles in barGroup (of course, there aren’t any). Add this line to your code:
barGroup.selectAll("rect")
Then, we use that one-two punch of .data().enter():
barGroup.selectAll("rect") .data(popData) .enter()
Neither the page nor the DOM has changed yet, but we have a placeholder object for each of our data points. Let’s append some rectangles:
barGroup.selectAll("rect") .data(popData) .enter().append("rect")
We have 17 rectangles, and each one has a data point bound to it (see Figure 5.7). We now need to set the attributes of those rectangles. Some attributes will be the same for each rectangle—namely x, which determines where the bars start horizontally (remember, we want all of our bars to be aligned to the left, so x will be equal to 0), and height, because all of our bars will be the same height, a constant that we’ve called barHeight. Easy. We can go ahead and add those attributes:
barGroup.selectAll("rect") .data(popData) .enter().append("rect") .attr("x", 0) .attr("height", barHeight)
But the other two attributes—width and y—need to be different for each bar:
barGroup.selectAll("rect") .data(popData) .enter().append("rect") .attr("x", 0) .attr("height", barHeight) .attr("width", // What goes here?) .attr("y", // Or here?);
This is where the bound data comes into play. Let’s start with the width. When we were relying on a for loop, we iterated through a variable we called i, and set each rectangle’s width to popData[i]. We essentially want to do the same thing here—we want the width of each bar to be equal to the associated value in popData. But since popData is bound to our rectangles, we don’t need a loop. We can just tell D3, “hey, set the width of each rectangle equal to the data point that it is bound to.”
The way we do that is by using something called an anonymous function.