Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active October 23, 2019 23:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save mbostock/4b66c0d9be9a0d56484e to your computer and use it in GitHub Desktop.
Save mbostock/4b66c0d9be9a0d56484e to your computer and use it in GitHub Desktop.
Inline Labels
license: gpl-3.0
redirect: https://observablehq.com/@d3/inline-labels

This example shows how to implement Ann K. Emery’s technique of placings labels directly on top of a line in D3 4.0 Alpha.

To construct the multi-series line chart, the data is first transformed into separate arrays for each series. (The series names are automatically derived from the columns in the TSV file, thanks to a new dsv.parse feature.)

var series = data.columns.slice(1).map(function(key) {
  return data.map(function(d) {
    return {
      key: key,
      date: d.date,
      value: d[key]
    };
  });
});

A label is rendered for each point in each series. Beneath this label, a white rectangle is added, whose size and position is computed automatically using element.getBBox plus a little bit of padding. The resulting label is thus legible. The last label for each series gets an additional tspan to show the series name.

date Apples Bananas
2009 130 40
2010 137 58
2011 166 97
2012 154 117
2013 179 98
2014 187 120
2015 189 84
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke-width: 1.5px;
}
.label {
text-anchor: middle;
}
.label rect {
fill: white;
}
.label-key {
font-weight: bold;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.0.0-alpha.9.min.js"></script>
<script>
var parseTime = d3.timeParse("%Y");
var svg = d3.select("svg");
var margin = {top: 30, right: 50, bottom: 30, left: 30},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
labelPadding = 3;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.requestTsv("data.tsv", function(d) {
d.date = parseTime(d.date);
for (var k in d) if (k !== "date") d[k] = +d[k];
return d;
}, function(error, data) {
if (error) throw error;
var series = data.columns.slice(1).map(function(key) {
return data.map(function(d) {
return {
key: key,
date: d.date,
value: d[key]
};
});
});
var x = d3.scaleTime()
.domain([data[0].date, data[data.length - 1].date])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(series, function(s) { return d3.max(s, function(d) { return d.value; }); })])
.range([height, 0]);
var z = d3.scaleCategory10();
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var serie = g.selectAll(".serie")
.data(series)
.enter().append("g")
.attr("class", "serie");
serie.append("path")
.attr("class", "line")
.style("stroke", function(d) { return z(d[0].key); })
.attr("d", d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.value); }));
var label = serie.selectAll(".label")
.data(function(d) { return d; })
.enter().append("g")
.attr("class", "label")
.attr("transform", function(d, i) { return "translate(" + x(d.date) + "," + y(d.value) + ")"; });
label.append("text")
.attr("dy", ".35em")
.text(function(d) { return d.value; })
.filter(function(d, i) { return i === data.length - 1; })
.append("tspan")
.attr("class", "label-key")
.text(function(d) { return " " + d.key; });
label.append("rect", "text")
.datum(function() { return this.nextSibling.getBBox(); })
.attr("x", function(d) { return d.x - labelPadding; })
.attr("y", function(d) { return d.y - labelPadding; })
.attr("width", function(d) { return d.width + 2 * labelPadding; })
.attr("height", function(d) { return d.height + 2 * labelPadding; });
});
</script>
@etdsoft
Copy link

etdsoft commented Sep 14, 2016

It seems there is a change between the 4.0-alpha and 4.2 that prevents this from working?

Cloning the gist and updating the reference to <script src="http://d3js.org/d3.v4.min.js"></script> results in empty white boxes (no labels).

This, I'm figuring, has to do with:

  label.append("rect", "text")
      .datum(function() { return this.previousSibling.getBBox(); })
      .attr("x", function(d) { return d.x - labelPadding; })
      .attr("y", function(d) { return d.y - labelPadding; })
      .attr("width", function(d) { return d.width + 2 * labelPadding; })
      .attr("height", function(d) { return d.height + 2 * labelPadding; });

And the fact that in the 4.0-alpha version it results in:

<g class="label" transform="translate(146.5997261524418,304.973544973545)">
  <rect x="-8.5625" y="-9.203125" width="17.125" height="18.015625"></rect>
  <text dy=".35em">58</text>
</g>

And in 4.2 it results in:

<g class="label" transform="translate(146.5997261524418,304.973544973545)">
  <text dy=".35em">58</text>
  <rect x="-8.5625" y="-9.203125" width="17.125" height="18.015625"></rect>
</g>

I also had to change this.previousSibling to this.nextSibling.

For some reason in 4.2, the order of elements is reversed, and the text is hidden under the rectangle.

It seems like an easy fix, but I don't have a clue.

Ideas?

@celso
Copy link

celso commented May 7, 2017

Did you fix this? I'd like to see the solution.

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