Home > Code, Graphics, Interactive, Tutorial > Remaking a Washington Post graph, part 3

Remaking a Washington Post graph, part 3

This is the third part of a tutorial for making a graph like this one on the Washington Post website, using RaphaëlJS.

Click here to go to the first part of the tutorial

Step 6: x-axis ticks and labels

First add this object:

var xaxis = {ticks: [], labels: []};

We’ll store references to the ticks and labels here. We probably won’t need to do anything with them, but it’s nice to have, just in case.

Notice that the first tick in the original graph is for March 2000. After that there are tick marks for each January and July, and labels for each January. So, when writing the JavaScript for making the ticks, we’ll make the first tick separately, then make a loop to hit all of the Julies and Januaries.

We have date labels through the date variable in the data.js file. These dates need to reformatted slightly, though. They’re of the form “Mar-00”, when we need them to be of the form “Mar\n’00” (where \n is the newline character). When we use the dates, then, we’ll use dates[i].replace("-","\n\'").

For the ticks, we’ll use SVG paths again, and, again, we’ll put all of the pieces into an array, use .join(" ") on the array, and give it to R.path. This time we’ll use one line to make the array and combine it into a string using .join(" "). In a second line, we give it to R.path, and then push it onto the xaxis.ticks array.

Here’s the function we’ll use:

function makeXaxis() {
	var x0 = scales.xintercept;
	var x1 = scales.xslope;
	var y0 = scales.yintercept;
	
	var path = ["M",String(x0),String(y0),"L",String(x0),String(y0 + 8)].join(" ");
	xaxis.ticks.push(R.path(path));  // first tick, March 2000
	xaxis.labels.push(R.text(x0,y0 + 25, dates[0].replace("-","\n\'")).attr({"font-size":12}));
	
	for(var i = 10; i < numMonths; i+=6) { // every January and July
		if ((i-10)%12 === 0) {              // if it's January, make a label and a longer tick
			xaxis.labels.push(R.text(x0 + x1*i, y0 + 25, dates[i].replace("-","\n\'")).attr({"font-size":12}));
			path = ["M",String(x0 + x1*i),String(y0),"L",String(x0 + x1*i),String(y0 + 8)].join(" ");
			xaxis.ticks.push(R.path(path));
		} else {
			path = ["M",String(x0 + x1*i),String(y0),"L",String(x0 + x1*i),String(y0 + 4)].join(" ");
			xaxis.ticks.push(R.path(path));
		}
	}
}

A little more explanation might be in order regarding the methods on R. As is probably obvious now, R.path takes a single argument, an SVG path. R.text takes three arguments, an x position, a y position, and the text to create. Either of these can have their attributes (things like color, size, etc., see the RaphaëlJS documentation) modified by appending .attr({name: value}). In the above function we used .attr({"font-size":12}) on the text. In step 5, we used .attr({stroke: colors[continent][0], fill: colors[continent][0]}) to change the stroke and fill color of the SVG shapes.

Step 7: Adding the next level of shapes

Before we start adding interactivity, let’s prepare the shapes for the countries. These are the shapes which appear when we click on a continent in the initial graph. Since this will have to be done eventually anyway, and since this additional level of shapes will complicate many functions, it’ll be easier in the long run if we add them now, even if we won’t see them for a few steps.

Ok, a bunch of small changes need to be made. First, the areas object, which holds references to the shapes, used to be

var areas = {
	level0: {}
};

Change this to

var areas = {
	level0: {},
	level1: {}
};

so that there’s a place for the country shapes. Next we have to make changes to the makeAreas function. First, recall how the shapes were constructed by keeping running totals of debt values using the prevTotal variable inside of makeAreas. We’ll now need to do this on two levels (total for all continents and for the second level the total for all countries in a given continent), and we don’t want the two running totals to interfere. Hence, we will change

var prevTotal;

to

var prevTotal = [];

and change

prevTotal = zeroes;

to

prevTotal[0] = zeroes;
prevTotal[1] = zeroes;

Next, for country shapes we’ll want to keep track of country name as well as continent. We’ll also want to keep track of the level of the graph we are on (initial level, level 0, vs the level we get after clicking on the graph, level 1). For these reasons, we’ll change the number of variables passed to the makeSingleArea function. The new version has

function makeSingleArea(data, yscale, name, continent, level) {

The variable name will be the country’s name when the shape is for a country, or the continent name when for a continent (so a continent shape will have the same value for continent and name. Inside the makeSingleArea function, we’ll add

newArea.name = name;
newArea.level = level;

after defining newArea. Also inside of the makeSingleArea function there are three references to
prevTotal that now need to be to prevTotal[level] (make sure you get all three).

The last change we need to make is to how we call makeSingleArea. Because of the two levels of shapes it gets more complicated. I’m not going to go into all of the details here, but I will note that the second level of the running total needs to start over at zero each time we move to a new continent.

Here was the old version of the last lines of makeAreas.

for(var j = continents.length - 1; j >= 0; j--) {
	areas["level0"][continents[j]] = makeSingleArea(debt[continents[j]]["total"], scales["total"], continents[j]);
}

Here is the updated version (recall that we’re also passing more variables to makeSingleArea):

for(var j = continents.length - 1; j >= 0; j--) {
	debtData = debt[continents[j]]["total"];
	yscale = scales["total"];
	name = continents[j];
	areas["level0"][continents[j]] = makeSingleArea(debtData, yscale, name, continents[j], 0);
	
	areas["level1"][continents[j]] = {}; // initiate a place to store shapes for each continent
	prevTotal[1] = zeroes;  // restart the running total for each continent
	for(attr in debt[continents[j]]) {  // this loop adds countries within continents
		if (debt[continents[j]].hasOwnProperty(attr) && attr !== "total") {
			placeData = debt[continents[j]][attr];
			placeScale = scales[continents[j]];
			placeName = attr;
			areas["level1"][continents[j]][attr] = makeSingleArea(debtData, yscale, name, continents[j], 1);
			areas["level1"][continents[j]][attr].hide();
		} else if (attr === "total" && (continents[j] === "Australia" || continents[j] === "Other")) {
			placeData = debt[continents[j]][attr];
			placeScale = scales[continents[j]];
			placeName = continents[j];
			areas["level1"][continents[j]][attr] = makeSingleArea(debtData, yscale, name, continents[j], 1);
			areas["level1"][continents[j]][attr].hide();
		}
	}
}

I decided to add three variables here, just to give reminder what some of the positions of makeSingleArea are when calling it. These are placeData, placeScale, and placeName. These should be declared, probably at the beginning of the makeArea function.

I will say one other thing. We don’t want the new country shapes to show up yet, or if they did, they would overlap the continent shapes and make the whole thing look like a mess. That’s the reason for lines that end in .hide(), which is how you hide a RaphaëlJS object.

That concludes step 7 and this post. I had promised to add some interactivity in this post, but I thought it would be better to get these other shapes in here now, rather than do it later and then have to modify even more functions.

Here is what the html file should look like (with re-arrangement, possibly) after steps 6 and 7. Don’t forget to call makeXaxis. The only visible change from the previous part of the tutorial is the inclusion of the x-axis ticks and labels.

<html>
    <head>
        <title>Washington Post graph clone</title>
        <script src="raphael-min.js" type="text/javascript" charset="utf-8"></script>
        <script src="wapo-data.js" type="text/javascript" charset="utf-8"></script>
        <script type="text/javascript" charset="utf-8">
			window.onload = function () {
				var width = 900;
				var height = 800;
				var xintercept = 40;  // left side of graph, or, the x-position of the first month
				var yintercept = 750; // bottom of the graph, or, the y-position of zero debt
				var graphInnerHeight = 700;
				var graphInnerWidth = 800;
				var numMonths = 134;
				var scales = {
					xintercept: xintercept,
					yintercept: yintercept,
					xslope: graphInnerWidth/(numMonths - 1)
				}
				var totalDebt = [];
				var continents = ["Asia","Europe","Middle East","North America","Australia","South America","Other"];
				var areas = {
					level0: {},
					level1: {}
				};
				var colors = {
					"Asia": ["#e99d9d", "#efbaba"],
					"Europe": ["#95a0be", "#b4bcd1"],
					"Middle East": ["#ffe2a4", "#ccb483"],
					"North America": ["#b5b88a", "#cbcdad"],
					"Australia": ["#f0b780", "#f2c595"],
					"South America": ["#987bb7", "#b6a2cc"],
					"Other": ["#cccccc", "#a3a3a3"]
				};
				var xaxis = {ticks: [], labels: []};
				R = Raphael("graph",width,height);

				function rescaleData() {
					for (attr in debt) {
						if (debt.hasOwnProperty(attr) 
						  && attr !== "Asia" 
						  && attr !== "Europe" 
						  && attr !== "Australia" 
						  && attr !== "Other") {
							for (subattr in debt[attr]) {
								if (debt[attr].hasOwnProperty(subattr) && subattr !== "total") {
									for(var i = 0; i < numMonths; i++) {
										debt[attr][subattr][i] = debt[attr][subattr][i]/1000;
									}
								}
							}
						}
					}
				}
				
				function makeTotal() {
					var cont;

					for(var i = 0; i < numMonths; i++) {
						totalDebt[i] = 0;
					}
					for(var j = 0; j < continents.length; j++) {
						cont = debt[continents[j]];
						for(var i = 0; i < numMonths; i++) {
							totalDebt[i] += cont["total"][i];
						}
					}
				}
				
				function makeScales() {
					var attrMax;
					var yscale;

					scales["total"] = -graphInnerHeight/Math.max.apply(null,totalDebt);
	
					for (attr in debt) {
						if (debt.hasOwnProperty(attr)) {
							attrMax = Math.max.apply(null,debt[attr]["total"]);
							yscale = (attr === "Australia" || attr === "Other") ? 
								(-graphInnerHeight/attrMax + scales["total"])/2 : -graphInnerHeight/attrMax;
							scales[attr] = yscale;
						}
					}
				}
				
				function makeXaxis() {
					var x0 = scales.xintercept;
					var x1 = scales.xslope;
					var y0 = scales.yintercept;
					
					var path = ["M",String(x0),String(y0),"L",String(x0),String(y0 + 8)].join(" ");
					xaxis.ticks.push(R.path(path));  // first tick, March 2000
					xaxis.labels.push(R.text(x0,y0 + 25, dates[0].replace("-","\n\'")).attr({"font-size":12}));
					
					for(var i = 10; i < numMonths; i+=6) { // every January and July
						if ((i-10)%12 === 0) {              // if it's January, make a label and a longer tick
							xaxis.labels.push(R.text(x0 + x1*i, y0 + 25, dates[i].replace("-","\n\'")).attr({"font-size":12}));
							path = ["M",String(x0 + x1*i),String(y0),"L",String(x0 + x1*i),String(y0 + 8)].join(" ");
							xaxis.ticks.push(R.path(path));
						} else {
							path = ["M",String(x0 + x1*i),String(y0),"L",String(x0 + x1*i),String(y0 + 4)].join(" ");
							xaxis.ticks.push(R.path(path));
						}
					}
				}

				function makeAreas() {
					var placeData, placeScale, placeName;
					var prevTotal = [];
					var zeroes = [];
					for(var i = 0; i < numMonths; i++) {
						zeroes[i] = 0;
					}
					prevTotal[0] = zeroes;
					prevTotal[1] = zeroes;
					function makeSingleArea(data, yscale, name, continent, level) {
						var total = [];
						var top = [];
						var bottom = [];
						var topX, topY, bottomX, bottomY, letter, path, newArea;
						
						for(var i = 0; i < numMonths; i++) {
							total[i] = prevTotal[level][i] + data[i];
							topX = scales["xintercept"] + scales["xslope"]*i;
							topY = scales["yintercept"] + yscale*total[i];
							
							bottomX = scales["xintercept"] + scales["xslope"]*(numMonths - 1 - i);
							bottomY = scales["yintercept"] + yscale*prevTotal[level][numMonths - 1 - i];
							
							letter = (i === 0) ? "M" : "L"
							top.push(letter);
							top.push(String(topX));
							top.push(String(topY));
							
							bottom.push("L");
							bottom.push(String(bottomX));
							bottom.push(String(bottomY));
						}
						
						path = top.join(" ") + bottom.join(" ") + " Z";
						
						prevTotal[level] = total;
						
						newArea = R.path(path).attr({stroke: colors[continent][0], fill: colors[continent][0]});
						newArea.name = name;
						newArea.level = level;
						
						return newArea;
					}
					
					for(var j = continents.length - 1; j >= 0; j--) {
						placeData = debt[continents[j]]["total"];
						placeScale = scales["total"];
						placeName = continents[j];
						areas["level0"][continents[j]] = makeSingleArea(placeData, placeScale, placeName, continents[j], 0);
						
						areas["level1"][continents[j]] = {};
						prevTotal[1] = zeroes;
						for(attr in debt[continents[j]]) {
							if (debt[continents[j]].hasOwnProperty(attr) && attr !== "total") {
								placeData = debt[continents[j]][attr];
								placeScale = scales[continents[j]];
								placeName = attr;
								areas["level1"][continents[j]][attr] = makeSingleArea(placeData, placeScale, placeName, continents[j], 1);
								areas["level1"][continents[j]][attr].hide();
							} else if (attr === "total" && (continents[j] === "Australia" || continents[j] === "Other")) {
								placeData = debt[continents[j]][attr];
								placeScale = scales[continents[j]];
								placeName = continents[j];
								areas["level1"][continents[j]][attr] = makeSingleArea(placeData, placeScale, placeName, continents[j], 1);
								areas["level1"][continents[j]][attr].hide();
							}
						}
					}
				}
				
				rescaleData(); // do the rescaling
				makeTotal();
				makeScales();
				makeAreas();
				makeXaxis();
			};
        </script>
    </head>
    <body>
        <div id="graph"></div>
    </body>
</html>

You can see how it looks, here.

Next time

In the next post, we’ll add some interactivity, I promise.

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: