D3, Conceptually

Lesson 4: Changing Data

originally published 8 march 2013

index

4.1 Two nodes leave, no nodes enter
In previous lessons, we've only added data or modified data. Never have we removed data from a loving home. Our introduction of the enter selection left a question unspoken - what if one had less data? Would there be a corresponding exit selection to match?
<div class='captain'>Kirk</div> <div class='captain'>Picard</div> <div class='captain'>Sisko</div> <div class='captain'>Janeway</div> <div class='captain'>Archer</div> var captainNames = [ 'Kirk', 'Picard', 'Sisko', 'Janeway', // I never liked Enterprise. // 'Archer', ]; var captains = d3.selectAll('.captain').data(captainNames); captains.exit() .remove(); captains .text(String);
The remove function, rather obviously, removes the selection from the DOM. Its use isn't limited to exit selections, but it is most commonly used there. You also need not remove the exiting nodes - there is nothing inherently special about the selection and it functions identically to the update selection. Except, of course, it has no data:
<div class='captain'>Kirk</div> <div class='captain'>Picard</div> <div class='captain'>Sisko</div> <div class='captain'>Janeway</div> <div class='captain'>Archer</div> var captainNames = [ 'Kirk', 'Picard', 'Sisko', 'Janeway', // I never liked Enterprise. // 'Archer', ]; var captains = d3.selectAll('.captain').data(captainNames); captains.exit() .style('color', 'grey') .text(function(d, i) { return 'data: [' + d + '], index: [' + i + ']'; }); captains .text(String);
But how, exactly, does d3 decide which nodes are in the exit selection? If the data started as [0, 2, 4] and was update-d to [2, 4, 6], would any nodes exit? With the tools presented so far, no. Just as enter is defined as all the new indices with data, exit is all the indices that no longer have data. Consequently, we can't have an enter and an exit from the same data binding. At least, not yet. Consider the following example:
<div id='chart'></div> var chart = d3.select('#chart'); chart.selectAll('div') .data([0, 2, 4]).enter() .append('div') .text(String); var divs = chart.selectAll('div') .data([2, 4]); divs.exit() .style('color', 'grey') .text('Poor, unloved datum'); divs .text(function(d) { return d + ' - still kicking'; });
As expected, the last node is the exit-ing node. The mapping from pre-update node to post-update node is by index. The node formerly known as 0 is now known as 2 after the update.
4.2 A new dawn for data - the key
Data binding needn't be by index; we may also bind data by key. But how to specify it? As you may have noticed, many of the functions we've seen in D3 have additional, optional params. data is no exception. In addition to the first argument (to specify the data), it accepts a key function as a second argument. What's a key for, huh? To unlock doors, of course. Doors to data-binding fun!
Or, you know, something else. The key function allows one to specify a data mapping from pre-update to post-update instead of the default by-index behavior. In other words, it defines if a datum is exiting, entering, or updating. Consider our previous example, but with a key function that defines an identity mapping:
<div id='chart'></div> var chart = d3.select('#chart'); var keyFn = function(d) { return d; }; chart.selectAll('div') .data([0, 2, 4], keyFn).enter() .append('div') .text(String); var divs = chart.selectAll('div') .data([2, 4], keyFn); divs.exit() .style('color', 'grey') .text('Poor, unloved datum'); divs .text(function(d) { return d + ' - still kicking'; });
The function, like most D3 functions, is passed two params: data and index along with the this bound to the appropriate Element. To pair nodes up with data, D3 calls the key function on each selected node and on the fresh data. The exit selection is all nodes selected with a key not present in the fresh data (in set terms, {key(node): node ∈ selected} - {key(datum): datum ∈ data}). Likewise, enter is all nodes with keys in fresh data, but not in selected. The base selection is all selected nodes with a matching key in data ( {key(node): node ∈ selected} ∩ {key(datum): datum ∈ data}).
What does this mean for us? Well, the first node in the list is now grey, not the last. That is about it. We'll find this useful in the next section. One last thing to note: the returned key is string-ified, so using an Array or Object will lead to all data having the same key - not a good thing.
If you are the kind that likes to cause trouble, find flaws in things, or worry about corner cases - have you checked for backdoors in your compiler yet? Also, let me head off two possible questions. First, the key function only matters at the time of calling data, and only for that single call. Previous and future calls could use a different function. Second, if your key function does not provide a unique mapping you are in for a world of hurt - the enter and exit sets will be mystifying.
4.3 Enter-Update-Exit
Example code, up until this point, has been a single-run affair - we select some nodes, we update attributes on them, and we go on our way. What if we wish for our method to be more than a fling? What if we want our callers to call us (maybe)?
<div id='root'></div> var root = d3.select('#root') var renderData = function() { var data = d3.range(5).map(Math.random); // 5 random numbers [0, 1) var numbers = root.selectAll('div.datum').data(data); numbers.enter().append('div') .attr('class', 'datum') .text(d3.format('.2f')); }; root.append('button') .text('make it rain data!') .on('click', renderData);
What happened? Why doesn't the text update? Shouldn't we get a fresh set of five numbers each time renderData is called? No, not with this code. Now that our function is called multiple times, we must consider when attributes can change and when they can't. Should the class of a div in this example ever change? Should the text of a div change? To answer these questions, consider how the attribute is set. If your attribute depends on data, then it can change. And attributes that can change should be set on the entire selection:
<div id='root'></div> var root = d3.select('#root') var renderData = function() { var data = d3.range(5).map(Math.random); // 5 random numbers [0, 1) var numbers = root.selectAll('div.datum').data(data); numbers.enter().append('div') .attr('class', 'datum'); // The class never changes. // The number changes each time; ensure the text does as well. numbers .text(d3.format('.2f')); }; root.append('button') .text('make it rain data!') .on('click', renderData);
Far out! Now, I've written this code in a very specific order - the order from the section title. enter before update (our fancy name for operating on the original selection). What if we write it in the reverse order? With update before enter? Not the nicest:
<div id='root'></div>
var root = d3.select('#root') var renderData = function() { var data = d3.range(5).map(Math.random); // 5 random numbers [0, 1) var numbers = root.selectAll('div.datum').data(data);
numbers .text(d3.format('.2f')); numbers.enter().append('div') .attr('class', 'datum');
}; root.append('button') .text('make it rain data!') .on('click', renderData);
You may have noticed the first time you made it rain data it did not, in fact, rain data. We instead found ourselves parched for data. But, if you look ever more closely, you'll notice it had still rained divs. This should be obvious in retrospect - the code first set properties on divs, then added new ones. If we have no divs to begin with, nothing will get updated and then our new divs will pop into existence, naked.
d3 does something to make the enter-update pattern more effective - any nodes appended to an enter are immediately filled in on the original selection. By writing your enter first, you automatically get any operations from your update applied.
So, what of our third musketeer - the exit? An example, of course:
<div id='root'></div> var root = d3.select('#root') var renderData = function() { var numData = Math.floor(5 * Math.random()); var data = d3.range(numData).map(Math.random); // [0-4] random numbers [0, 1) var numbers = root.selectAll('div.datum').data(data); numbers.enter().append('div') .attr('class', 'datum'); numbers .text(d3.format('.2f')); numbers.exit() .remove(); };
root.append('button') .text('make it rain data!') .on('click', renderData);
4.4 Animate
Drumroll - animation time! Animations in D3 are very simple to initiate. And, as is wont for easy tasks, easy to mess up. A transition is just another function called on a selection. It takes no arguments and returns a d3.transition object. You could read all about them by clicking on the link, or you could keep reading my tutorial. Maybe both? Wouldn't want to hurt mine nor mbostock's feelings, now would you?
A d3.transition acts just like a selection with a few extra methods tacked on, and a few other methods removed. The three most important methods are retained - attr, style, and text. These three will continue as your bread-and-butter of D3. As you may have guessed from the identical names, attr, style, and text accomplish the same ends, with the same arguments, as the d3.selection methods.
With one difference, of course. Two of the setters (attr/style) will not immediately take effect, but instead gradually change from the current value to one specified. Consider our previous example, but with animating opacities:
<div id='root'></div> var root = d3.select('#root') var renderData = function() { var numData = Math.floor(5 * Math.random()); var data = d3.range(numData).map(Math.random); // [0-4] random numbers [0, 1) var numbers = root.selectAll('div.datum').data(data); numbers.enter().append('div') .attr('class', 'datum'); numbers.transition() .style('opacity', Number) .text(d3.format('.2f')); numbers.exit() .remove(); }; root.append('button') .text('make it rain data!') .on('click', renderData);
Nitpicker that I am, two qualms with the example get me. For one, watch what happens when a new div is added. They fade from 100% opacity to whatever their target value is! That isn't a very good visual; why not have them start at 0% and fade in, instead of out? The second is the inverse - our exiting nodes simply go poof. Why not fade them out into the dark of the night? It sounds like we want two changes: new nodes should start with an opacity of 0, and exiting nodes should animate to 0 before going away. We can turn that directly into two new lines of code (to set new properties), and one modified one (to cause an animation). Ignore the incorrect sizing, it is a defect in my page's code, not of the example:
<div id='root'></div> var root = d3.select('#root') var renderData = function() { var numData = Math.floor(5 * Math.random()); var data = d3.range(numData).map(Math.random); // [0-4] random numbers [0, 1) var numbers = root.selectAll('div.datum').data(data); numbers.enter().append('div') .style('opacity', 0) .attr('class', 'datum'); numbers.transition() .style('opacity', Number) .text(d3.format('.2f')); numbers.exit().transition() .style('opacity', 0) .remove(); }; root.append('button') .text('make it rain data!') .on('click', renderData);
Nifty! By giving the entering nodes an initial opacity of 0, now they fade in to whatever their target may be. And by turning our exit call into a transition we make the nodes fade out. Of note is the behaviour of a call to remove on a transition - it is delayed until the transition completes, allowing all to see the majesty of your animation before unceremoniously severing the nodes from the document. A sad life they lead, sad indeed.
4.5 An Annotated Animated Example
Let's one more example, which could accurately be described as a graph. This section is a bit different from the previous - it consists entirely of a large block of code, replete with inline comments. All the important bits are called out, and the rest if for you to grok. So, grok:
<style type='text/css'> rect.bar { fill: #ccf; stroke: #000; } </style> <div id='root'></div> var root = d3.select('#root'); var w = 300; var h = 100; var svg = root.append('svg') .style('display', 'block') .attr('width', w) .attr('height', h);
/* We will draw 3-5 bars for random numbers in the range [0, 1), with a small text label afterwards. 30 is, sadly, a magic number - you can expect a few of these in svg. */
var xScale = d3.scale.linear() .range([0, w - 30]); // Allow 30 pixels of space for the labels. var yScale = d3.scale.linear()
/* Note the domain is constant; this way, the size of a bar is constant whether we have 3, 4, or 5. */
.domain([0, 5]) .range([0, h]); var renderBars = function() { var numData = 3 + Math.floor(Math.random() * 3); // 3-5 numbers var data = d3.range(numData).map(Math.random); // between [0, 1)
/* enter of the enter-update-exit triumvirate. Keep in mind this selection will have initial attrs we specify here, as well as potentially updated attrs specified later on the root selection's transition. If an attr never changes - like the class of a bar - set it on newly created enter nodes. */
var bars = svg.selectAll('rect.bar').data(data); var newBars = bars.enter().append('rect') .attr('class', 'bar')
/* The enter animation is a fade in from 0 opacity combined with a grow to the target width.
Using xScale(0) for the x position makes our code more flexible. If we changed the bars to have an indent of 10px, this code need not change.
The same effect could, of course, be accomplished using a <g> element and modifying its transform property. */
.attr('opacity', 0) .attr('x', xScale(0))
/* Each bar should get some padding. I arbitrarily decided 20% the height of a bar between each bar - or, 10% from the top and 10% from the bottom.
Or, in other words, each bar should go from yScale(i + 0.1) to yScale(i + 0.9). The height could also be written as, simply, yScale(0.8) or with constants. It comes down to taste, though I chose this value to call out the duality of the y and height properties. As with the xScale(0) call above, make sure whichever solution you think looks best uses the yScale. */
.attr('y', function(d, i) { return yScale(i + 0.1); }) .attr('height', function(d, i) { return yScale(i + 0.9) - yScale(i + 0.1);}); bars.transition()
/* update of the enter-update-exit triumvirate. One gotcha with the pattern is the behavior of repeated calls to transition on a node - the last one wins.
Even if it looks cleaner to handle the transition from 0 opacity to 1 on the new nodes using the newBars selection, the subsequent transition on bars would override it. Sometimes, an enter animation is distinct enough from the update that separating the two makes sense - in this case, either set up your update transition before appending new nodes, or place the enter transition after the update transition. */
.attr('width', xScale) .attr('opacity', 1); bars.exit().transition()
/* exit of the enter-update-exit triumvirate. We are initiating a transition, so the remove call won't happen until the animation has ended. */
.attr('opacity', 0) .attr('width', 0) .remove(); var labels = svg.selectAll('text.label').data(data); var newLabels = labels.enter().append('text') .attr('class', 'label')
/* We go through the same opacity hoops as on the bars with our text. In fact, much of the animation is the same so that text and bars appear a single unit. */
.attr('opacity', 0) .attr('x', xScale(0)) .attr('y', function(d, i) { return yScale(i + 0.5); })
/* 0.3em is a magic number that looks good, giving just enough padding. Pixels would be a fine measure as well - five of them or so. */
.attr('dx', '0.3em')
/* 0.35em is SVG for vertical-align: center - not technically true, but true enough. */
.attr('dy', '0.35em'); labels.transition() .attr('x', xScale) .attr('opacity', 1)
/* The d3.format mini-language is very rich and familiar to users of Python, and ever-so-slightly confusing to those accustomed to printf. You can just yell at me to get off your lawn, really. */
.text(d3.format('.2f')); labels.exit().transition() .attr('opacity', 0) .attr('x', xScale(0)) .remove(); }; root.append('button') .text('run renderBars') .on('click', renderBars);
Now go off and animate some bars! But one more thing, and it is dreadfully important: object constancy. A mouthful, truly.
4.6 Constancy, my dear, would you fetch my keys?
So: what is object constancy and why should you care? Pretend, for a moment, you are playing catch with a parental figure. You are a kid. You like catch. Your parental figure tosses a ball your way; you see it pause as it leaves their hand. Then, it disappears and a ball materializes in your glove. That ball? No constancy. It jumped from parental figure's windup to your waiting glove, and you are confused. Is it even the same ball? You didn't see it travel, so you can't be sure. You go and lie down, never to play catch again.
Maybe a graph will help (the code is a good example to read, but also a mouthful. I recommend you read it after running the demos):
<style type='text/css'> #chart { background-color: #fff; } svg { display: block; margin-bottom: 10px; } .highs line { stroke-width: 2px; stroke: #f88; } .rains line { stroke-width: 2px; stroke: #88f; } text { font-size: 13px; font-family: sans-serif; } text.small { font-size: 10px; fill: #666; } </style> <div id='chart'></div> var w = 300; var h = 220; var data = [ {'i': 0,'month': 'January', 'high': 47, 'rain': 5.6}, {'i': 1,'month': 'February', 'high': 50, 'rain': 3.5}, {'i': 2,'month': 'March', 'high': 53, 'rain': 3.7}, {'i': 3,'month': 'April', 'high': 58, 'rain': 2.7}, {'i': 4,'month': 'May', 'high': 64, 'rain': 1.9}, {'i': 5,'month': 'June', 'high': 70, 'rain': 1.6}, {'i': 6,'month': 'July', 'high': 76, 'rain': 0.7}, {'i': 7,'month': 'August', 'high': 76, 'rain': 0.9}, {'i': 8,'month': 'September', 'high': 70, 'rain': 1.5}, {'i': 9,'month': 'October', 'high': 59, 'rain': 3.5}, {'i': 10,'month': 'November', 'high': 51, 'rain': 6.6}, {'i': 11,'month': 'December', 'high': 45, 'rain': 5.4}, ]; var dataMonth = function(d) { return d['month']; }; var dataHigh = function(d) { return d['high']; }; var dataRain = function(d) { return d['rain']; }; var highScale = d3.scale.linear() .domain(d3.extent(data, dataHigh)) .range([100, 0]); var rainScale = d3.scale.linear() .domain([0, d3.max(data, dataRain)]) .range([50, 0]); var xScale = d3.scale.linear() .domain([0, data.length]) .range([0, w]); var datumWidth = xScale(1) - xScale(0); var chart = d3.select('#chart').append('svg') .attr('width', w) .attr('height', h); var highG = chart.append('g').attr('class', 'highs') .attr('transform', 'translate(0, 20)'); var rainG = chart.append('g').attr('class', 'rains') .attr('transform', 'translate(' + [0, 30 + d3.max(highScale.range())] + ')'); var title = chart.append('text') .attr('x', w / 2) .attr('y', h - 10) .attr('text-anchor', 'middle') .text('Seattle Climate'); var dataLink = chart.append('a') .attr('xlink:href', 'http://en.wikipedia.org/wiki/Seattle'); dataLink.append('text') .attr('class', 'small') .attr('x', w / 2) .attr('y', h) .attr('text-anchor', 'middle') .text('[source]'); var drawChart = function() { var highGs = highG.selectAll('g.tick').data(data); var newHighGs = highGs.enter().append('g') .attr('class', 'tick'); highGs.transition().duration(1000) .attr('transform', function(d, i) { return 'translate(' + [xScale(i) + datumWidth / 2, highScale(dataHigh(d))] + ')'; }) newHighGs.append('line') .attr('x1', -datumWidth / 2) .attr('x2', datumWidth / 2); newHighGs.append('text') .attr('class', 'small') .attr('text-anchor', 'middle') .attr('dy', '-0.3em'); highGs.select('text') .text(function(d) { return dataHigh(d) + '°F'; }); // This code is almost identical to the 'highGs' block. An exercise to the // reader: should you remove the redundancy? How best to? var rainGs = rainG.selectAll('g.tick').data(data); var newRainGs = rainGs.enter().append('g') .attr('class', 'tick'); rainGs.transition().duration(1000) .attr('transform', function(d, i) { return 'translate(' + [xScale(i) + datumWidth / 2, rainScale(dataRain(d))] + ')'; }) newRainGs.append('line') .attr('x1', -datumWidth / 2) .attr('x2', datumWidth / 2); newRainGs.append('text') .attr('class', 'small') .attr('text-anchor', 'middle') .attr('dy', '-0.3em'); rainGs.select('text') .text(function(d) { return dataRain(d) + '"'; }); var labels = chart.selectAll('text.month').data(data); labels.enter().append('text') .attr('class', 'month small') .attr('y', h - 25) .attr('text-anchor', 'middle'); labels.transition().duration(1000) .attr('x', function(d, i) { return xScale(i) + datumWidth / 2; }) .text(function(d) { return dataMonth(d).substr(0, 3); }); }; drawChart(); var makeSortButton = function(field, rev) { return d3.select('#chart').append('button') .on('click', function() { data.sort(function(a, b) { return rev * (a[field] - b[field]); }); drawChart(); }); }; makeSortButton('i', 1).text('Sort by month'); makeSortButton('high', -1).text('Sort by high temperature'); makeSortButton('rain', -1).text('Sort by precipitation');
When the sorting is changed on the chart, you may see a smooth transition. Try, however, tracking a single month - like May (a good month, May). It is easy to track when swapping between month and temperature, but not so much by precipitation. It jumps, instantly, somewhere else on the chart. With object constancy, however:
<style type='text/css'> #chart { background-color: #fff; } svg { display: block; margin-bottom: 10px; } .highs line { stroke-width: 2px; stroke: #f88; } .rains line { stroke-width: 2px; stroke: #88f; } text { font-size: 13px; font-family: sans-serif; } text.small { font-size: 10px; fill: #666; } </style> <div id='chart'></div>
var w = 300; var h = 220; var data = [ {'i': 0,'month': 'January', 'high': 47, 'rain': 5.6}, {'i': 1,'month': 'February', 'high': 50, 'rain': 3.5}, {'i': 2,'month': 'March', 'high': 53, 'rain': 3.7}, {'i': 3,'month': 'April', 'high': 58, 'rain': 2.7}, {'i': 4,'month': 'May', 'high': 64, 'rain': 1.9}, {'i': 5,'month': 'June', 'high': 70, 'rain': 1.6}, {'i': 6,'month': 'July', 'high': 76, 'rain': 0.7}, {'i': 7,'month': 'August', 'high': 76, 'rain': 0.9}, {'i': 8,'month': 'September', 'high': 70, 'rain': 1.5}, {'i': 9,'month': 'October', 'high': 59, 'rain': 3.5}, {'i': 10,'month': 'November', 'high': 51, 'rain': 6.6}, {'i': 11,'month': 'December', 'high': 45, 'rain': 5.4}, ];
var dataMonth = function(d) { return d['month']; }; var dataHigh = function(d) { return d['high']; }; var dataRain = function(d) { return d['rain']; }; var keyFn = dataMonth;
var highScale = d3.scale.linear() .domain(d3.extent(data, dataHigh)) .range([100, 0]); var rainScale = d3.scale.linear() .domain([0, d3.max(data, dataRain)]) .range([50, 0]); var xScale = d3.scale.linear() .domain([0, data.length]) .range([0, w]); var datumWidth = xScale(1) - xScale(0); var chart = d3.select('#chart').append('svg') .attr('width', w) .attr('height', h); var highG = chart.append('g').attr('class', 'highs') .attr('transform', 'translate(0, 20)'); var rainG = chart.append('g').attr('class', 'rains') .attr('transform', 'translate(' + [0, 30 + d3.max(highScale.range())] + ')'); var title = chart.append('text') .attr('x', w / 2) .attr('y', h - 10) .attr('text-anchor', 'middle') .text('Seattle Climate'); var dataLink = chart.append('a') .attr('xlink:href', 'http://en.wikipedia.org/wiki/Seattle'); dataLink.append('text') .attr('class', 'small') .attr('x', w / 2) .attr('y', h) .attr('text-anchor', 'middle') .text('[source]');
var drawChart = function() { var highGs = highG.selectAll('g.tick').data(data, keyFn);
var newHighGs = highGs.enter().append('g') .attr('class', 'tick'); highGs.transition().duration(1000) .attr('transform', function(d, i) { return 'translate(' + [xScale(i) + datumWidth / 2, highScale(dataHigh(d))] + ')'; }) newHighGs.append('line') .attr('x1', -datumWidth / 2) .attr('x2', datumWidth / 2); newHighGs.append('text') .attr('class', 'small') .attr('text-anchor', 'middle') .attr('dy', '-0.3em'); highGs.select('text') .text(function(d) { return dataHigh(d) + '°F'; }); // This code is almost identical to the 'highGs' block. An exercise to the // reader: should you remove the redundancy? How best to?
var rainGs = rainG.selectAll('g.tick').data(data, keyFn);
var newRainGs = rainGs.enter().append('g') .attr('class', 'tick'); rainGs.transition().duration(1000) .attr('transform', function(d, i) { return 'translate(' + [xScale(i) + datumWidth / 2, rainScale(dataRain(d))] + ')'; }) newRainGs.append('line') .attr('x1', -datumWidth / 2) .attr('x2', datumWidth / 2); newRainGs.append('text') .attr('class', 'small') .attr('text-anchor', 'middle') .attr('dy', '-0.3em'); rainGs.select('text') .text(function(d) { return dataRain(d) + '"'; });
var labels = chart.selectAll('text.month').data(data, keyFn);
labels.enter().append('text') .attr('class', 'month small') .attr('y', h - 25) .attr('text-anchor', 'middle'); labels.transition().duration(1000) .attr('x', function(d, i) { return xScale(i) + datumWidth / 2; }) .text(function(d) { return dataMonth(d).substr(0, 3); });
};
drawChart(); var makeSortButton = function(field, rev) { return d3.select('#chart').append('button') .on('click', function() { data.sort(function(a, b) { return rev * (a[field] - b[field]); }); drawChart(); }); }; makeSortButton('i', 1).text('Sort by month'); makeSortButton('high', -1).text('Sort by high temperature'); makeSortButton('rain', -1).text('Sort by precipitation');
Watch those bars fly! Or, rather, float - the animation has been slowed considerably for illustrative purposes only. Now the eye can track the data as it changes. You can see when a month doesn't change its position; you can see when a month goes from the best to the worst. You can also spam clicks on the buttons for fun effects, if your APM is a bit low.
That must have been hard, write? The code surely changed a great deal. Not so! Go ahead, click the "Show code" button. One line added, three lines modified. Not bad, not bad.
The DOM structure of this chart was chosen arbitrarily - it consists of an SVG. Each month has a "g" element for the temperature and a "g" element for the rainfall, transformed to the appropriate x/y position. The month labels are positioned using their x property along the bottom. Why this? Why not position everything using x and y values?
We net a few benefits from transform-ed gs - the children elements are simply positioned - all temperature lines receive the same values and need never change. Positioning is easier to reason. Given a pen at the relevant x/y for a month's temperature, where do we write the label? Well, right there (with a little offset so it doesn't overlap the line).
One could also structure the DOM so each month has a g element transformed to have the appropriate x value, then y-translated gs for each of the temperature, rainfall, and month labels. Is this better? Maybe; I'm not one to judge. Both work and both are flexible.

To read more on object constancy, consider mbostock's tutorial. It provides a different example which you may find more compelling. Otherwise, that wraps up this lesson - you have enough knowledge (and enough code samples) to do some serious work.