Multiple Area Charts with D3.js

In this tutorial we will introduce some basics of D3.js and create an infographic with multiple area charts along with a context tool to zoom and pan the data.

Multiple Area Charts with D3.js

The D3.js website describes itself as “a JavaScript library for manipulating documents based on data.” It is but one of many in the ever growing list of visual data JavaScript libraries. D3 was created by Mike Bostock who was the lead developer on Protovis, which D3.js builds on.

The reason I focus on D3 is because it is one of the most robust frameworks available, while also remaining flexible and web standard agnostic. It’s ability to work in SVG, Canvas, or HTML (by taking data and generating HTML tables), allows you to decide what method the data should be presented in. SVG, Canvas and HTML all have their pros and cons and a framework shouldn’t force your hand into one or the other, which is what many frameworks do. This article won’t cover when to choose SVG or Canvas, as that would be an article unto its own. Instead, I’ll point you to a great article by Opera that discusses this question: http://dev.opera.com/articles/view/svg-or-canvas-choosing-between-the-two/.

So what kind of visualizations can you create with D3? Almost anything you can imagine. It handles simple information charts, such as line, bar, and area, as well as more complex charts that use cartograms or trees. I thoroughly encourage you to look at the examples provided on the D3 website for some inspiration.

MultipleAreaChartsD3_01

Since area charts are pretty common, it’s a great place to start. In this example we will go over the basics of how to create an infographic with multiple area charts along with a context tool to zoom and pan the data. The data for this chart was downloaded from Gapminder.org, which is a great resource if you need some data while learning D3.

The .CSV I downloaded from Gapminder contains electricity consumption per capita from 1960. I did modify this CSV slightly to make it easier to work with in the example. The CSV file contained data on 65 countries – not all of them had data for every year. I narrowed it down to five countries that had data for each year. I then switched the years and countries so that the columns were country based instead of year. This wasn’t necessary, however, it makes the example a little easier.

With this data we will create 5 area charts. The y-axis will show the range of consumption, while the x-axis will show the years.

First thing we need to do is to set up an HTML page.

<!DOCTYPE html>
<html>
<head>
	<script src="http://d3js.org/d3.v2.js"></script>
</head>
<body>
	<div id="chart-container">
		<h1>Electricity Consumption per capita</h1>
	</div>
</body>
</html>

The majority of the code will now be in JavaScript, which can be in an external file or directly on the page. The first thing we’ll want to do in JavaScript is to set up some variables to size and position our chart.

var margin = {top: 10, right: 40, bottom: 150, left: 60},
	width = 940 - margin.left - margin.right,
	height = 500 - margin.top - margin.bottom,
	contextHeight = 50;
	contextWidth = width * .5;

Next we will want to add an SVG tag to the page. D3’s select function conveniently uses CSS style string selectors.

In this example, I’m going to recommend using SVG for two reasons:

  1. SVG uses nodes in the markup that can be inspected with a browsers developer tools. This can make debugging a little easier.
  2. Because SVG uses nodes in the markup, it can also be styled with CSS.
							
var svg = d3.select("#chart-container")
			.append("svg")
			.attr("width", width + margin.left + margin.right)
			.attr("height", (height + margin.top + margin.bottom));

//d3.csv takes a file path and a callback function											
d3.csv('data.csv', createChart);
						
function createChart(data){
	var countries = [],
		charts = [],
		maxDataPoint = 0;

After the CSV file loads, we’ll want to loop through the first row to grab each country. We’ll store it in an array to use later.

for (var prop in data[0]) {
	if (data[0].hasOwnProperty(prop)) {
		if (prop != 'Year') {
			countries.push(prop);
		}
	}
};
							
var countriesCount = countries.length,
	startYear = data[0].Year,
	endYear = data[data.length - 1].Year,
	chartHeight = height * (1 / countriesCount);

Lets also loop through the entire data set to ensure that every value is a number while converting the year value to a JavaScript Date object. We’ll also want to find the maximum data point. We’ll use this later to set the y-axis scale.

						
data.forEach(function(d) {
	for (var prop in d) {
		if (d.hasOwnProperty(prop)) {
			d[prop] = parseFloat(d[prop]);
										
			if (d[prop] > maxDataPoint) {
				maxDataPoint = d[prop];
			}
		}
	}
								
	// D3 needs a date object, let's convert it just one time
	d.Year = new Date(d.Year,0,1);
});
				
for(var i = 0; i < countriesCount; i++){
	charts.push(new Chart({
		data: data.slice(), // copy the array
		id: i,
		name: countries[i],
		width: width,
		height: height * (1 / countriesCount),
		maxDataPoint: maxDataPoint,
		svg: svg,
		margin: margin,
		showBottomAxis: (i == countries.length - 1)
	}));
}

We'll create the context brush here. This is the tool that will allow users to zoom and pan the charts.

							
var contextXScale = d3.time.scale()
				.range([0, contextWidth])
				.domain(charts[0].xScale.domain());	
							
var contextAxis = d3.svg.axis()
			.scale(contextXScale)
			.tickSize(contextHeight)
			.tickPadding(-10)
			.orient("bottom");
							
var contextArea = d3.svg.area()
			.interpolate("monotone")
			.x(function(d) { return contextXScale(d.date); })
			.y0(contextHeight)
			.y1(0);
				
var brush = d3.svg.brush()
			.x(contextXScale)
			.on("brush", onBrush);
				
var context = svg.append("g")
		.attr("class","context")
		.attr("transform", "translate(" + (margin.left + width * .25) + "," + (height + margin.top + chartHeight) + ")");

context.append("g")
		.attr("class", "x axis top")
		.attr("transform", "translate(0,0)")
		.call(contextAxis);

context.append("g")
		.attr("class", "x brush")
		.call(brush)
		.selectAll("rect")
		.attr("y", 0)
		.attr("height", contextHeight);

context.append("text")
		.attr("class","instructions")
		.attr("transform", "translate(0," + (contextHeight + 20) + ")")
		.text('Click and drag above to zoom / pan the data');
												
function onBrush(){
	/* 
	this will return a date range to pass into the chart object 
	*/

	var b = brush.empty() ? contextXScale.domain() : brush.extent();

	for(var i = 0; i < countriesCount; i++){
		charts[i].showOnly(b);
	}
}
}
						
function Chart(options){
this.chartData = options.data;
this.width = options.width;
this.height = options.height;
this.maxDataPoint = options.maxDataPoint;
this.svg = options.svg;
this.id = options.id;
this.name = options.name;
this.margin = options.margin;
this.showBottomAxis = options.showBottomAxis;
							
var localName = this.name;

Next we need to create the scales. Scales can be time-based or quantitative (linear, logarithmic, or power). Since our x-axis is going to represent the years, we will use a time-based scale. The y-axis represents the consumption, so we'll use a linear scale.

For scales to be useful, they need to have a range and a domain. Both range and domain accept an array of two or more numbers.

The domain represents the minimum and maximum input values. For instance, for our x-axis this will be 1960 to 2008. For our y-axis it will be 0 (the minimum value) and the max data point we found earlier. If every chart references this max data point, we'll ensure that each y-axis is the same.

The range is the minimum and maximum values for the output. We want the charts to be 840px (this.width). So if we set the range [0,this.width], D3 will use this as the out put for the domain. So 1960 will have an output of 0 while 2008 will have an output value of 840. If our domain was [200,400], then 2008 would have an output value of 400 and 1960 would have an output of 200.

/* XScale is time based */
this.xScale = d3.time.scale()
			.range([0, this.width])
			.domain(d3.extent(this.chartData.map(function(d) { return d.Year; })));
							
/* YScale is linear based on the maxData Point we found earlier */
this.yScale = d3.scale.linear()
			.range([this.height,0])
			.domain([0,this.maxDataPoint]);

The area call is what creates the chart. There are a number of interpolation options we can use. 'Basis' is the smoothest, however, when working with a lot of data it will also be the slowest.

var xS = this.xScale;
var yS = this.yScale;

this.area = d3.svg.area()
		.interpolate("basis")
		.x(function(d) { return xS(d.Year); })
		.y0(this.height)
		.y1(function(d) { return yS(d[localName]); });

We will create a definition with which to display the chart. In this example, it acts as a container for the chart. Without it, we would see the chart go off the left and under the y-axis when zooming and panning. It isn't a requirement, and this example would still work without it.

this.svg.append("defs").append("clipPath")
			.attr("id", "clip-" + this.id)
			.append("rect")
			.attr("width", this.width)
			.attr("height", this.height);

/*
Assign it a class so we can assign a fill color
And position it on the page
*/

this.chartContainer = svg.append("g")
			.attr('class',this.name.toLowerCase())
			.attr("transform", "translate(" + this.margin.left + "," + (this.margin.top + (this.height * this.id) + (10 * this.id)) + ")");
				
/* We've created everything, let's actually add it to the page */

this.chartContainer.append("path")
			.data([this.chartData])
			.attr("class", "chart")
			.attr("clip-path", "url(#clip-" + this.id + ")")
			.attr("d", this.area);
															
this.xAxisTop = d3.svg.axis().scale(this.xScale).orient("bottom");
this.xAxisBottom = d3.svg.axis().scale(this.xScale).orient("top");

/* We only want a top axis if it's the first country */

if(this.id == 0){
	this.chartContainer.append("g")
			.attr("class", "x axis top")
			.attr("transform", "translate(0,0)")
			.call(this.xAxisTop);
}
							
/* Only want a bottom axis on the last country */

if(this.showBottomAxis){
	this.chartContainer.append("g")
		.attr("class", "x axis bottom")
		.attr("transform", "translate(0," + this.height + ")")
		.call(this.xAxisBottom);
}  
								
this.yAxis = d3.svg.axis().scale(this.yScale).orient("left").ticks(5);
								
							this.chartContainer.append("g")
			.attr("class", "y axis")
			.attr("transform", "translate(-15,0)")
			.call(this.yAxis);
					this.chartContainer.append("text")
			.attr("class","country-title")
			.attr("transform", "translate(15,40)")
			.text(this.name);
							
}

The function showOnly is called when the chart is panned or zoomed. It's passed a start and end date. The chart will need to be redrawn to match these date limits. Therefore, we'll need to reset the xScale domain, reset the area path and update the axis.

Chart.prototype.showOnly = function(b){
	this.xScale.domain(b);
	this.chartContainer.select("path").data([this.chartData]).attr("d", this.area);
	this.chartContainer.select(".x.axis.top").call(this.xAxisTop);
	this.chartContainer.select(".x.axis.bottom").call(this.xAxisBottom);
}

Now we just need to style the chart a bit. As I mentioned above, SVG can be styled using CSS. While that is true, the css properties for SVG are a little different. For instance, instead of background-color, it's fill.

Let's set up the container and the chart title.

#chart-container {
	width: 1000px; 
	margin: 0 auto 50px auto;
	background: rgba(255,255,255,0.5);
	box-shadow: 1px 1px 4px rgba(0,0,0,0.2);
	padding: 20px 30px;
}

#chart-container h2 {
	color: #444;
	margin: 0 0 10px;
	font-weight: 300;
	padding: 10px;
	text-shadow: 0 1px 1px rgba(255,255,255,0.8);
	font-size: 24px;
}

Next, let's style the context tool by removing the border and making the axis lines almost transparent. Then we'll style what the tool looks like when you are interacting with it by giving it a semi transparent background along with white borders on the left and right. And finally, we'll give the whole tool a solid background color.

					
g.context g.axis path{ stroke-opacity: 0;}
g.context g.axis line{ stroke-opacity: .1;}

g.context g.brush rect.background{ fill: rgba(0,0,0,.1);}
.brush .extent {
	stroke: #fff;
	fill-opacity: .125;
	shape-rendering: crispEdges;
}

g.context rect.background{
	fill: rgb(200,200,255);
	visibility: visible !important;
}

Finally, lets give a color to each country and remove the fills on the axis.

			
g.france path.chart{ fill: rgba(127,201,127,0.5);}
g.germany path.chart{ fill: rgba(127,201,174,0.5);}
g.japan path.chart{ fill: rgba(127,183,201,0.5);}
g.uk path.chart{ fill: rgba(127,130,201,0.5);}
g.usa path.chart{ fill: rgba(171,127,201,0.5);}
			
.axis path, .axis line {
	fill: none;
	stroke: #aaa;
	shape-rendering: crispEdges;
}

If you’d like to learn more about D3, I encourage you to look at their website as it has a plethora of resources, including examples, tutorials, and documentation. There is also a d3.js tag on Stack Overflow.

Note that the demo should run in a web server environment.

Tagged with:

Tyler Craft

Tyler Craft is a web developer who is as comfortable server side as he is client side. He is passionate about making sites more user friendly, and occasionally stepping away from the computer to shoot some film.

Stay up to date with the latest web design and development news and relevant updates from Codrops.

Feedback 7

Comments are closed.
  1. Thanks, have been following this as an amatuer coder but finding it difficult to see where you put the final 3 sections on styling into the script. I can’t actually see this on the source code behind the demo.