gbegley
10/11/2017 - 6:01 PM

Inline Labels

Inline Labels

<!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>
date	Apples	Bananas
2009	130	40
2010	137	58
2011	166	97
2012	154	117
2013	179	98
2014	187	120
2015	189	84

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.