Skip to content

Instantly share code, notes, and snippets.

@ritchieking
Last active January 3, 2016 18:39
Show Gist options
  • Save ritchieking/8503838 to your computer and use it in GitHub Desktop.
Save ritchieking/8503838 to your computer and use it in GitHub Desktop.
Line chart, with comments

Howdy, and welcome to this block! Its purpose is to help you learn to make a line chart using D3. The curve above shows the life expectancy for the entire world over time. The data come from the World Bank.

The code itself is ripped almost entirely from Mike Bostock's Line Chart block. But the data is different (just to mix things up a bit) and the code itself is heavily commented with explanations of what it's actually doing. I hope that it helps. You can also find a clean version here.

I also hope to do a series of these, so stay tuned!

year lifeExpectancy
1/1/1961 53.00618814
1/1/1962 53.49544588
1/1/1963 54.14244036
1/1/1964 54.97746907
1/1/1965 55.82684751
1/1/1966 56.74868644
1/1/1967 57.61738778
1/1/1968 58.33389775
1/1/1969 59.00507154
1/1/1970 59.59217127
1/1/1971 60.08082426
1/1/1972 60.49296656
1/1/1973 60.85638163
1/1/1974 61.26343093
1/1/1975 61.62668426
1/1/1976 61.97408697
1/1/1977 62.33021711
1/1/1978 62.63088187
1/1/1979 62.94032196
1/1/1980 63.18719679
1/1/1981 63.49452384
1/1/1982 63.79853101
1/1/1983 64.03326825
1/1/1984 64.29128534
1/1/1985 64.54567853
1/1/1986 64.84644025
1/1/1987 65.10007882
1/1/1988 65.3044991
1/1/1989 65.51592402
1/1/1990 65.69645159
1/1/1991 65.86228667
1/1/1992 65.99723977
1/1/1993 66.08334808
1/1/1994 66.25878373
1/1/1995 66.43599275
1/1/1996 66.69034593
1/1/1997 66.95916797
1/1/1998 67.18587271
1/1/1999 67.41704789
1/1/2000 67.69058418
1/1/2001 67.98333357
1/1/2002 68.23767908
1/1/2003 68.49809111
1/1/2004 68.80300304
1/1/2005 69.03462379
1/1/2006 69.33145142
1/1/2007 69.59024029
1/1/2008 69.81932781
1/1/2009 70.07339995
1/1/2010 70.31041664
1/1/2011 70.53933561
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* The "lighter" font-weight just makes the Helvetica thin and stylish */
body {
font: 14px Helvetica;
font-weight: lighter;
}
/* The svg lines here are the tick marks, and the svg paths are the long horizontal and vertical
lines that run parallel to the axes and have a little nub on the end. D3 axes include both by
default. Also, by default, SVG shapes come with a fill but not a stroke. You want to set the
fill to "none" here because otherwise the paths will be filled in (they aren't one dimensional
because of the nub, so they actually have an inner area that can be filled). You need to
specificy a stroke color, otherwise there won't be a stroke at all (in other words, the paths
and lines won't show up). Setting the shape-rendering to crispEdges turns off anti-aliasing, so
the lines and paths will be black pixels surrounded by white pixels (instead of a gradation of
grey). CrispEdges are great for straight, horiztonal and vertical lines, but not for diagonal
lines or curves */
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
/* This turns off the path for the y-axis. This is just a personal preference. */
.y.axis path {
display: none;
}
/* This bit is referring to everything with the class "line," which is different from above,
where we applied style rules to svg line objects with the class "axis." As you'll see below, the
object with the class, "line" is the line of the line chart itself. Again, you need to add a
stroke color. I've changed the width to 2 pixels from the default 1 pixel. Also, notice that the
shape-rendering is not set to crispEdges because this is a curve. */
.line {
fill: none;
stroke: darkmagenta;
stroke-width: 2px;
}
</style>
<body>
<!-- You always want to put your script (or your reference to it) inside the body of your HTML. -->
<script src="http://d3js.org/d3.v3.js"></script>
<script>
// Set up your margins the Bostock way! Bostock always creates a JavaScript object called margin
// with the properties top, right, bottom, and left (pretty self-explanatory). He then defines
// the variables width and the height based on the dimensions inside of those margins. The 960px
// and 500px here are the overall width and height, respectively — this is the standard size of
// a block. These variables will be used to define the size of the SVG element (as you'll see
// below).
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// This is a helper function that turns a string representing a date into an actual date object
// that JavaScript can understand. The "%d/%m/%Y" bit tells D3 how your raw date data is
// formatted. %m is the month (as a number), %d is the day of the month, and %Y is the four-
// digit year. The D3 API reference has a list of more of these so-called directives.
var parseDate = d3.time.format("%m/%d/%Y").parse;
// Create a time scale for the horiztonal or x-axis. Time scales take date objects as inputs and
// map them linearly to an output range. Here the output range is set to go from 0 to width.
// Remember that the width variable includes everything inside of the left and right margins.
// That means the x-axis will span that entire distance and but up against both of the margins.
// Because the input (domain) depends on the data, it is defined below, inside of the d3.tsv()
// function, where the data is called in.
var x = d3.time.scale()
.range([0, width]);
// Create a linear scale for the y-axis. This takes regular old numbers and maps them linearly
// to an output range. Note that the range goes from height to 0, not the other way around. This
// is because, on the web, the upper left-hand corner of the page is 0,0 and as you move down, y
// increases. In charts, however, you want y to increase as you move up the page. So the range
// has to be reversed.
var y = d3.scale.linear()
.range([height, 0]);
// xAxis and yAxis are super handy axis generators. They depend on their respective scales are
// will be used below to automatically create default axes. Orient tells the axis where to put
// the labels relative to the tick marks — bottom means the labels go below the tick marks and
// left means they go to the left.
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// This is a helper function for creating the life expectancy curve. In general, the curves that
// you make in D3 will be SVG paths. Paths are pretty complex. Their shape is described by an
// attribute called d — a string written in a code that is like a mini-language unto itself.
// This helper function takes x and y inputs and then translates them into a string to set d
// equal to. The x input here is the year, and the y input is the life expectancy. You have to
// use an anonymous function to access each of them.
var line = d3.svg.line()
.x(function(d) { return x(d.year); })
.y(function(d) { return y(d.lifeExpectancy); });
// Here is where the margin convention above really comes into play. What Bostock does is he
// creates an SVG element that is the size of the entire block - 960 pixels by 500 pixels (
// notice that he adds the margins back to the width and height variables). Then he appends an
// SVG group and translates it over to the left by a distance equal to the left margin and down
// by a distance equal to the top margin. The variable svg actually refers to the group, not to
// the parent svg element. Everything will be based around that group. One thing this does is
// change the frame of reference. Now, 0,0 is in the upper left-hand corner inside of the
// margins, instead of outside of them. Conveniently, however, if svg objects spill over into
// the margins, they will still be displayed because it is still within the bounds of the
// granddaddy SVG element. In fact, the axes will be placed in the margins.
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// This function pulls in the data. Data in D3 is loaded asynchronously, which means the rest of
// the page is loaded first, just in case the data takes a while to load. As a result,
// everything that your code needs to do that involves data has to happen within this d3.tsv()
// function. Also, in case you were wondering, tsv is a file format and it stands for tab-
// separated values. You can see in the data.tsv file below that the values are indeed separated
// by tabs.
d3.tsv("data.tsv", function(error, data) {
// Note that when D3 pulls in a tsv file, it creates an array of objects. Each object has
// property names that correspond to the headers in the file - in this case "year" and
// "lifeExpectancy." The array is assigned to a variable, called data in this case.
// This self-contained loop iterates through each object in the data array and uses our helper
// function above to parse the raw data strings into actual JavaScript date objects. It also
// ensures that the life expectancy values are numbers and not strings.
data.forEach(function(d) {
d.year = parseDate(d.year);
d.lifeExpectancy = +d.lifeExpectancy;
});
// The domain of the x scale is defined using the extent of the year data. That function d3.
// extent() returns an array of length 2, where the first entry is the minimum value and the
// second entry is the maximum value.
x.domain(d3.extent(data, function(d) { return d.year; }));
// For the y domain, instead of using the extent, I'm setting the lower bound at a nice, even
// 50, and then computing the maximum for the upper bound.
y.domain([50, d3.max(data, function(d) { return d.lifeExpectancy; })]);
// Append the x axis, and assign two classes - "x" and "axis." Note that all you have to do is
// append a group, then you can use the call operator to call the x-axis generator and it will
// take care of the rest (by putting objects inside of that group). The default for the x-axis
// is to put it at the top, so you have to use transform to move the group to the bottom.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Create y axis and add label. When you append a text label to the axis, the point of
// reference is the axis group itself.
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("years");
// Create the path for your curve. In this case, we can use .datum() instead of .data(). The
// difference is subtle. Both bind data to objects (or more precisely to selections). But when
// .data() binds data to a selection, it computes a data-join, which, among other things,
// allows you to easily create a new SVG object for every data point in your array. .datum()
// does not do this - it only binds data. But it works in this case, because you only need a
// single path for your line. Every data point in your data set, instead of getting turned
// into a separate line, will be translated into SVG path-speak by the helper function above, (
// called line). This SVG path speak is used to set the "d" attribute for the path and
// generate the curve.
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment