Skip to content

Instantly share code, notes, and snippets.

@mgold
Forked from mbostock/.block
Last active December 21, 2017 20:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mgold/6a32cec6380b6ce75c1e to your computer and use it in GitHub Desktop.
Save mgold/6a32cec6380b6ce75c1e to your computer and use it in GitHub Desktop.
Multi-Series Line Chart Demoing Selection Groups

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.

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
<!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="http://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>
@JerryWiltz
Copy link

Hello Max, thanks for this.
Question: Your example is done in d3 v3 where "j" is the index, how would you implement with d4?
Thanks
Jerry

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment