Tutorial: Line chart in D3

One of the most common visualizations is a line chart. D3 is not a charting framework, but instead allows you to manipulate the document based on data. That’s what you’re actually doing with D3: adding elements to a document, removing them, updating them, etc. The advantage is that you are much more flexible in creating the visualization that you want. Some may consider it a slight disadvantage that you may do have to do some extra work to get things done.

For this tutorial we’re going to create a basic line chart with an x-axis and y-axis, tickmarks and labels.

First, we defined some variables:

var data = [3, 6, 2, 7, 5, 2, 1, 3, 8, 9, 2, 5, 7],
w = 400,
h = 200,
margin = 20,
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin, h - margin]),
x = d3.scale.linear().domain([0, data.length]).range([0 + margin, w - margin])

The data variable contains our dataset we want to display as a line chart. The w and h variables are used for the width and height of our chart and the margin will be used to create some room to display our labels. The y and x variables are our linear scale functions, and remember that you can use x and y as functions. The scales are used to convert our data values (which are defined in the domain of the scale) to x- and y-positions on the screen (which are defined in the range of the scale). Note the use of margin in the range.

Next we append an svg element to our document with the proper width and height, and then we append a g element to this svg element so that all the elements that will be appended to this g element will be grouped together. The transformation that we apply on the g element makes sure that our coordinate grid moves down 200 pixels.

var vis = d3.select("body")
    .append("svg:svg")
    .attr("width", w)
    .attr("height", h)

var g = vis.append("svg:g")
    .attr("transform", "translate(0, 200)");

In order to create a line chart, we’re going to add an path element to our visualization. This path element needs a d attribute that contains the data for the path. Now, you could write that yourself, but that may be a little hard to do, so we use the helper function d3.svg.line to do that for us (see Tutorial: Line interpolations in D3 for more details on this helper function). Now we add the line variable which is, just like the scales earlier, a function that you can call. So when we bind our data to the path, this d3.svg.line function will be called so that the value of the d attribute will be created:

var line = d3.svg.line()
    .x(function(d,i) { return x(i); })
    .y(function(d) { return -1 * y(d); })

The i parameter is the index of the current item, and the d is the current item itself. Note the usage of the scale functions (x() and y() to convert from i and d to x- and y-coordinates (note the -1. This is needed because right now the coordinate have a positive y-axis down, and since we have translated our g down 200 pixels, we need to use the negative values of the y-axis now). To use our line function we add a path element to our g element like this:

g.append("svg:path").attr("d", line(data));

Next up is adding the x-axis and y-axis. The following snippet adds them by appending a line to our g element.

g.append("svg:line")
    .attr("x1", x(0))
    .attr("y1", -1 * y(0))
    .attr("x2", x(w))
    .attr("y2", -1 * y(0))

g.append("svg:line")
    .attr("x1", x(0))
    .attr("y1", -1 * y(0))
    .attr("x2", x(0))
    .attr("y2", -1 * y(d3.max(data)))

Next are the labels. We can use a convenient function of the scales here: x.ticks() and y.ticks(). These functions will return the proper tickmarks, and you can provide how many you want. D3 does return nicely rounded numbers though. Also note the .text(String) usage for obtaining the string value of the current data item. The labels are aligned in the center by using .attr("text-anchor", "middle"). And for the y-labels I’ve used dy in order to vertically position the labels correctly.

g.selectAll(".xLabel")
	.data(x.ticks(5))
	.enter().append("svg:text")
	.attr("class", "xLabel")
	.text(String)
	.attr("x", function(d) { return x(d) })
	.attr("y", 0)
	.attr("text-anchor", "middle")

g.selectAll(".yLabel")
	.data(y.ticks(4))
	.enter().append("svg:text")
	.attr("class", "yLabel")
	.text(String)
	.attr("x", 0)
	.attr("y", function(d) { return -1 * y(d) })
	.attr("text-anchor", "right")
	.attr("dy", 4)

The last thing to do is to add the ticks themselves. These are actually just very short lines, so all you have to do is to provide the start x- and y position (x1 and y1) and the end x- and y-position (x2 and y2).

g.selectAll(".xTicks")
	.data(x.ticks(5))
	.enter().append("svg:line")
	.attr("class", "xTicks")
	.attr("x1", function(d) { return x(d); })
	.attr("y1", -1 * y(0))
	.attr("x2", function(d) { return x(d); })
	.attr("y2", -1 * y(-0.3))

g.selectAll(".yTicks")
	.data(y.ticks(4))
	.enter().append("svg:line")
	.attr("class", "yTicks")
	.attr("y1", function(d) { return -1 * y(d); })
	.attr("x1", x(-0.3))
	.attr("y2", function(d) { return -1 * y(d); })
	.attr("x2", x(0))

And that’s it. Finally, some of the styling of the objects was moved to a style section at the top of the page in order to get the final result:

path {
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
}

line {
    stroke: black;
}

text {
    font-family: Arial;
    font-size: 9pt;
}

View a live version here.

19 Comments

  • Dan Evans
    June 6, 2011 - 18:00 | Permalink

    The g.selectAll(“path”)… call causes a path element to be added for each of the data array members; all but the first are redundant. A g.append(“svg:path”).attr(“d”, line(data)) call
    solves this problem.

    • June 7, 2011 - 09:59 | Permalink

      You’re absolutely right. That is indeed a great improvement. Thank you!!

    • June 23, 2011 - 10:17 | Permalink

      Hi Dan,

      thank you very much for your reply. I didn’t change the code because mine wasn’t necessarily wrong, and Mike just offered a suggestion for an alternative, and indeed better, approach. But, now that you also asked for it, I updated the code anyway!

      Thanks again!

  • Mike
    June 23, 2011 - 06:44 | Permalink

    Hi Jan,
    i think what Evan was referring to was to replace this line:
    g.selectAll("path")
    .data(data)
    .enter().append("svg:path")
    .attr("d", line(data))

    to
    g.append("svg:path").attr("d", line(data));

    If the “selectAll(“path”) is there, it will cause many redundant child path elements to be appended to svg/g element. I don’t see that you have updated your excellent tutorial to reflect this change Evan suggested.

  • July 9, 2011 - 01:12 | Permalink

    Hi!

    Thanks so much for this tutorial – it’s very helpful! One quick thing – I think you can get rid of the awkward -1 * y(d) by simply messing with the y scale, like this:

    y = d3.scale.linear().domain([d3.max(data), 0]).range([0, h])

    I think this just tells d3 that the data should be mapped backwards onto 0 -> h. Would be glad to find out if there is a better way!

    • August 27, 2011 - 07:04 | Permalink

      And then don’t forget remove the transformation on the coordinate ( .attr(“transform”, “translate(0, 200)”);).

      Otherwise the chart won’t be visible.

      • August 29, 2011 - 08:56 | Permalink

        Hi Kazuya, thanks for your comment. Do you mean removing line 7 in the second code snippet? Are you sure your code looks like the live example? In other words, the live example doesn’t work on your computer?

        • October 13, 2011 - 12:46 | Permalink

          Hi Jan,

          I think Kazuya is correct.

          If you follow Mike’s tip to modify the y-axis (so you can remove all the “-1 *” for the y calculations). You will also have to remove the transform at line 7.

          Thanks for a great post!

  • July 9, 2011 - 23:52 | Permalink

    If that works now, that would be great!! I have been experimenting with this, but at the time of writing this tutorial, that didn’t work yet. Thank you for pointing this out!

  • Jonathan Birkholz
    October 28, 2011 - 21:30 | Permalink

    Great post. Thanks!

  • Scott Bronson
    November 14, 2011 - 00:49 | Permalink

    Hi Jan. In the picture on this blog post you’ve drawn the X and Y axes, but you don’t do that in the code. Here’s how I solved that.

    g.append(“svg:line”)
    .attr(“x1″, x(0))
    .attr(“y1″, -1 * y(0))
    .attr(“x2″, x(data.length))
    .attr(“y2″, -1 * y(0))
    .attr(“stroke”, “black”)

    g.append(“svg:line”)
    .attr(“x1″, x.range()[0])
    .attr(“y1″, -1 * y.range()[0])
    .attr(“x2″, x.range()[0])
    .attr(“y2″, -1 * y.range()[1])
    .attr(“stroke”, “black”)

    The first block computes the ranges, the second block accesses them.

  • Pingback: D3.js Data Visualizations « Nate's Code Vault

  • January 20, 2012 - 03:21 | Permalink

    Thanks for this, it’s very useful! Here’s a neat trick.

    If you create your y range like this:

    d3.scale.linear().domain(…).range([y_max, y_min])

    instead of what you have now:

    d3.scale.linear().domain(…).range([y_min, y_max])

    then you can get rid of the translate and all the -1*y(…) in the code. It’s a bit cleaner that way.

    • January 20, 2012 - 03:22 | Permalink

      Oh look, there’s the exact same comment up there. Sorry for the spam!

  • April 7, 2012 - 22:15 | Permalink

    According to http://www.w3.org/TR/SVG/text.html#TextAnchorProperty, text-anchor only takes “start”, “middle” or “end”. on your labeling attributes for the y ticks, you have (“text-anchor”, “right”). That should be (“text-anchor”, “end”) (at least on Chrome where I tried it..)

  • Pingback: d3.js | Pearltrees

  • Ray
    August 11, 2012 - 12:44 | Permalink

    Hi Guys,

    But what if chart has more than one data serie?
    What then? You just add more of these:

    var line1 = d3.svg.line()
    .x(function(d,i) { return x(i); })
    .y(function(d) { return -1 * y(d.serie1); })

    var line2 = d3.svg.line()
    .x(function(d,i) { return x(i); })
    .y(function(d) { return -1 * y(d.serie2); })

    and then in a loop creating new paths?

  • Andy
    December 19, 2012 - 20:32 | Permalink

    Would anyone know how I could animate the line from one side of the graph to the other?

    My graph is pulling data from a .csv file.

  • April 9, 2013 - 15:14 | Permalink

    In all honesty this became a wonderful indepth write-up nevertheless as with most excellent writers there are some items that could be worked after. But never ever the significantly less it turned out interesting.

  • Leave a Reply

    Your email address will not be published. Required fields are marked *

    *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>