Skip to content

Instantly share code, notes, and snippets.

@mbostock

mbostock/.block

Last active Oct 23, 2019
Embed
What would you like to do?
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

This comment has been minimized.

Copy link

@etdsoft 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

This comment has been minimized.

Copy link

@celso 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
You can’t perform that action at this time.