block by mgold 6a32cec6380b6ce75c1e

Multi-Series Line Chart Demoing Selection Groups

Full Screen

This is a fork of Mike Bostock’s Multi-Series Line Chart, which adds circles along the line for each data point. The circles help anchor us to the real datapoints (rather than extrapolations) when comparing across lines.

But this is really an exercise in nested selections, leveraging selections as groups. The data consists of multiple lines, which each have multiple data points. We call .data twice: first with the data for all cities, binding the array for each city as a single datum. This is good enough for D3’s line generators but not for placing circles. So we selectAll circles from the merged enter-update selection, and then bind the data for each line using a function, which is passed each datum from the prior join. This step produces a new data join, and you need to do it even if the function you pass is the identity. Since there are no circles, we skip straight to the enter selection and append them. Then we assign them x and y positions based on the accessors for the line generator.

But how do we get the color? Functions passed to .attr and .style as second arguments receive the datum d, the index i, and a little-known third argument j. It turns out that i is the index within the group (which point on the line) while j is the index of the group (which line of the three). We use j to index into our original data and determine the color of the point.

All of this happens in the last “paragraph” of the code. Selection groups are fully explained in How Selections Work, which is required reading for mastery of D3.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

</style>
<body>
<script src="//d3js.org/d3.v3.js"></script>
<script>

var margin = {top: 20, right: 80, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var parseDate = d3.time.format("%Y%m%d").parse;

var x = d3.time.scale()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var color = d3.scale.category10();

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var line = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.temperature); });

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 + ")");

d3.tsv("data.tsv", function(error, data) {
  color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));

  data.forEach(function(d) {
    d.date = parseDate(d.date);
  });

  var cities = color.domain().map(function(name) {
    return {
      name: name,
      values: data.map(function(d) {
        return {date: d.date, temperature: +d[name]};
      })
    };
  });

  x.domain(d3.extent(data, function(d) { return d.date; }));

  y.domain([
    d3.min(cities, function(c) { return d3.min(c.values, function(v) { return v.temperature; }); }),
    d3.max(cities, function(c) { return d3.max(c.values, function(v) { return v.temperature; }); })
  ]);

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  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("Temperature (ºF)");

  var city = svg.selectAll(".city")
      .data(cities)
    .enter().append("g")
      .attr("class", "city");

  city.append("path")
      .attr("class", "line")
      .attr("d", function(d) { return line(d.values); })
      .style("stroke", function(d) { return color(d.name); });

  city.append("text")
      .datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
      .attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
      .attr("x", 3)
      .attr("dy", ".35em")
      .text(function(d) { return d.name; });

  city.selectAll("circle")
    .data(function(d){return d.values})
    .enter()
    .append("circle")
    .attr("r", 3)
    .attr("cx", function(d) { return x(d.date); })
    .attr("cy", function(d) { return y(d.temperature); })
    .style("fill", function(d,i,j) { return color(cities[j].name); });

});

</script>

data.tsv

date	New York	San Francisco	Austin
20111001	63.4	62.7	72.2
20111002	58.0	59.9	67.7
20111003	53.3	59.1	69.4
20111004	55.7	58.8	68.0
20111005	64.2	58.7	72.4
20111006	58.8	57.0	77.0
20111007	57.9	56.7	82.3
20111008	61.8	56.8	78.9
20111009	69.3	56.7	68.8
20111010	71.2	60.1	68.7
20111011	68.7	61.1	70.3
20111012	61.8	61.5	75.3
20111013	63.0	64.3	76.6
20111014	66.9	67.1	66.6
20111015	61.7	64.6	68.0
20111016	61.8	61.6	70.6
20111017	62.8	61.1	71.1
20111018	60.8	59.2	70.0
20111019	62.1	58.9	61.6
20111020	65.1	57.2	57.4
20111021	55.6	56.4	64.3
20111022	54.4	60.7	72.4
20111023	54.4	65.1	72.4
20111024	54.8	60.9	72.5
20111025	57.9	56.1	72.7
20111026	54.6	54.6	73.4
20111027	54.4	56.1	70.7
20111028	42.5	58.1	56.8
20111029	40.9	57.5	51.0
20111030	38.6	57.7	54.9
20111031	44.2	55.1	58.8