Tutorial: Line interpolations in D3

In this tutorial we’re going to explore line interpolations in D3.

First we start with 2 scales that we will use to convert values to x- and y-coordinates on the screen:

var x = d3.scale.linear().domain([0,10]).range([0,400]),
y = d3.scale.linear().domain([0,1]).range([0,50]),
groupHeight = 60,
topMargin = 100

Next we generate some random data:

var data = []
d3.range(10).forEach(function(d) { data.push(Math.random()) })

We’re also creating an array which contains all the possible interpolations D3 supports. We’ll see the effects of every interpolation in a moment:

var interpolations = [
	"linear",
	"step-before",
	"step-after",
	"basis",
	"basis-closed",
	"cardinal",
	"cardinal-closed"]

In SVG there is a difference between a line and a path. A line is a straight line where you define the start and end position of the line: , whereas with a path you draw the outline of any arbitrary shape by specifying a series of connected lines, arcs, and curves. You do this by specifying the d attribute of the path. Every path must begin with a moveto command. The command letter is a capital M followed by an x- and y-coordinate, separated by commas or whitespace. This command sets the current location of the “pen” that’s drawing the outline. This is followed by one or more lineto commands, denoted by a capital L, also followed by x- and y-coordinates, and separated by commas or whitespace. You can see more of this specification here.

In our example we’re not actually creating an SVG line, but an SVG path, so we need to set the d attribute of the paht. Luckily D3 has a helper function to ease the burden to create this data: d3.svg.line. For this helper function you can set:

  • an accessor function for obtaining x values
  • an accessor function for obtaining y values
  • an interpolation type, which defaults to linear
  • a tension value which affects the cardinal interpolations only

In our example, we want to show the different kinds of interpolations for the same data, so we create a function that takes the name of an interpolation as an argument, and then returns the d3.svg.line function as a result. This is the code that does that (you can play with the out-commented tension property to see the effect):

function getLine(interpolation) {
    return d3.svg.line().x(function(d,i) {
        return x(i)
    }).y(function(d) {
        return y(d)
    }).interpolate(interpolation)
//.tension(0)
}

Note the following: the function for x has 2 arguments: d and i. The d is the current item in the dataset (which we will provide later), and i is the index of the current item in the dataset. Also note that we’re using the x-scale to convert i to an x-coordinate, and the y-scale to convert the data value to a y-coordinate.

Now we initialize the visualization:

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

Next up is greating a group for each of the lines we want to show:

var lg = vis.selectAll(".lineGroup")
	.data(interpolations)
	.enter().append("svg:g")
	.attr("class", "lineGroup")
	.attr("transform", function(d,i) {
	return "translate(100," + (topMargin + i * groupHeight) + ")"
}).each(drawLine)

We set the interpolations array as data for this group, so that svg:g elements for each of the interpolations will be added to the visualization. The svg:g element can be used to group other elements together, so that if you apply a transformation to the group for instance, it will be applied to all of its members. Note that we add the class lineGroup in our selection to select all these elements. Next we set the transform attribute, and we use the index to position the groups based on their position in the interpolation array. For each of the group, we want to draw a line. We do that by calling the drawLine function in the .each(drawLine) statement. The drawLine function itself looks like this:

function drawLine(p,j) {
	d3.select(this)
		.selectAll(".lineGroup")
		.data(data)
		.enter().append("svg:path")
		.attr("d", getLine(p)(data))
		.attr("fill", "none")
		.attr("stroke", "steelblue")
		.attr("stroke-width", 3)
		//.attr("stroke-dasharray", "15 5")
}

The drawLine function itself has to parameters: p and j where p is the parent data item (the current interpolation name), and j is the parent index. First we select the current element with d3.select(this), and next we select all the .lineGroup elements. We assign the line data to the data property, and append a path to each lineGroup element. The d attribute calls the getLine function and provides the current interpolation name as an argument. The result of that is the d3.svg.line function with the that uses the interpolation we just provided. Next we assign the line data to this function so that D3 will calculate the data string that will be used by the d attribute of the svg:path element. Finally we set some basic properties. The final out-commented is one of the stroke properties you can set, where 15 is the dash length and 5 is the gap length. Just play around with those properties to see what else is possible.

This concludes this tutorial. All the lines you see are using the same data, but they use different interpolations.

20 Comments

  • Pingback: Tutorial: Line chart in D3 | Jan Willem Tulp's blog

  • Brandon
    May 11, 2011 - 23:57 | Permalink

    Code didn’t work – needed to define topMargin and GroupHeight:

    var topMargin = 0
    var groupHeight = 50

  • May 16, 2011 - 21:31 | Permalink

    Too bad I missed those in the copy-paste action :( Thanks for the feedback. I fixed the code.

  • tomer
    November 10, 2011 - 20:56 | Permalink

    this is great, thanks! how do you transition an interpolation in case of a dynamic data source?

    • November 11, 2011 - 15:57 | Permalink

      Could you elaborate a bit more on what you’re trying to achieve? You can set the interpolation for a line and then apply that to a set of data points. The interpolation should still work if the data is dynamic or updated, since you’re not changing the interpolation being used. Does this answer your question?

  • December 2, 2011 - 15:56 | Permalink

    Your code for generating test data could be simplified. Rather than:

    var data = []
    d3.range(10).forEach(function(d) { data.push(Math.random()) })

    Why not simply do:

    var data = d3.range(10).map(function() { return Math.random() });

  • Steve Yang
    January 11, 2012 - 19:46 | Permalink

    Hi, thank you for a very thorough tutorial, When I tried this out, I can see only the first graph. I do see 7 lineGroups when I inspect the elements. Would you happen to know what might have gone wrong? Thank you very much.

    • Steve Yang
      January 11, 2012 - 20:13 | Permalink

      Actually, it is working. It just does not work when I run it from Aptana. Weird… Thank you for the tutorial again.

    • Steve Yang
      January 11, 2012 - 20:24 | Permalink

      After I test the code, it runs on IE9 and Chrome, but not Firefox 9, maybe some features are not compatible with Firefox 9?

      • January 11, 2012 - 22:32 | Permalink

        Hi Steve, great to hear that it works now. If you could provide some code examples, that would really help!

  • Nathan
    January 12, 2012 - 00:21 | Permalink

    This seems to be drawing each line 10 times one on top of the other. It looks like 6 lines on the screen. I don’t really know where the issue is, but I think the drawLine function with the enter(data) command creates 10 ‘svg:path’ elements. There’s a 90% chance I’m wrong. Am I entering the code wrong?

    • Nathan
      January 12, 2012 - 01:09 | Permalink

      Figured it out. Made some changes to drawLine function.

      First, I think that including both d3.select(this) and .selectAll(‘.lineGroup’) seems redundant. You can drop the .selectAll code and leave everything as-is and it seems to work fine.

      Not sure why, but you have to remove that line for the next part to work.

      Second, the .enter().append(‘svg:path’) causes 10 svg:path elements to enter the DOM (1 for each of the 10 data elements in .data(data). My next change is to get rid of the “.enter()” code. Now the code appears to only create 1 svg:path element in each of the svg:g elements.

      It also seems to smooth out the lines. At least in Chrome on a MacBook Air.

      Here’s my updated function code.

      function drawLine(p,j) {
      d3.select(this)
      .data(data)
      .append(‘svg:path’)
      .attr(‘d’, getLine(p)(data))
      .attr(‘fill’, ‘none’)
      .attr(‘stroke’, ‘steelblue’)
      .attr(‘stroke-width’, 3)
      }

      Thanks for these tutorials. I’ve literally gone from being mildly aware of what D3 can do to debugging code in two afternoons.

      • Juan Manuel
        January 21, 2012 - 16:22 | Permalink

        I had as many paths in the lineGroup as data points. I’ve changed drawLine to:

        function drawLine(p) {
        d3.select(this)
        .append(“path”)
        .attr(“d”, getLine(p)(data))
        .attr(“fill”, “none”)
        .attr(“stroke”, “steelblue”)
        .attr(“stroke-width”, 3);
        }

        and now there’s only one path per lineGroup.

        • Scott Cameron
          February 22, 2012 - 08:28 | Permalink

          Or even more in the spirit of D3 data binding, this seems to work also:

          function drawLine(p, j) {
          d3.select(this).selectAll(“path”)
          .data([data]).enter().append(“svg:path”)
          .attr(“d”, getLine(p))
          .attr(“fill”, “none”)
          .attr(“stroke”, “steelblue”)
          .attr(“stroke-width”, 3)
          }

          This binds the data array as a single array element the enter selection will only contain 1 new entry and then the function returned from getLine(p) will be invoked with that piece of data (the 10 element data array).

          • Sam Penrose
            April 3, 2012 - 19:18 | Permalink

            Many thanks to Jan Willem for the tutorial and to the others for the updated drawLine functions, which I needed for Firefox 10.0.1 on CentOS 5. I also needed to replace window.height with window.innerHeight for the value of the svg height attribute.

      • March 5, 2012 - 23:07 | Permalink

        I thought something was weird about the way it was rendering. Very pixelated. Thank you.

  • Kuan
    November 20, 2012 - 03:52 | Permalink

    Hi, Thanks for such a great tutorial!
    Because I am new to javascript and D3
    I have 2 questions about this example:
    1. Can you tell more about when to use forEach() and when to use each()?

    2. It seems that I have difficulty to understand the format in this line:
    .attr(ā€œdā€, getLine(p)(data))
    why there can be two parameters passed to getLine? Or,if the getline return a function(I guess it may be a interpolator, but where can I send the data parameter?)

    Thanks

    • December 6, 2012 - 00:24 | Permalink

      Hi,

      thanks for doing the tutorials and visiting my blog. To answer your questions:

      1. each() is a method in the D3 library (https://github.com/mbostock/d3/wiki/Selections#wiki-each) that you apply to a D3 selection. As arguments it passes you the current datum, the index and `this` is the current DOM element. Whereas forEach() is a standard method on a Javascript array (if supported by the browser) to loop over all the items in the array.

      2. Actually, only one parameter gets passed to getLine(), namely: p. The result of the getLine() function is again a function. When you call this returned function, you can provide an argument, which is data in this case. In Javascript functions can return functions, and that’s what’s happening here.

      Thanks again, and good luck!

  • 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>