block by fogonwater d1bc4b9ac52da208766aa54f3ce127c0

Stacked area label placement #2

Full Screen

Picking best label positions in a stacked area chart by sweeping through each series and finding the largest minimum vertical difference wide enough to fit the label (if one exists).

A potential improvement might be to come up with a list of candidates for each area and then pick a combination that’s vertically aligned or reads left to right from top to bottom. It also might be desirable to pick the rightmost available space instead of the tallest?

See also: Stacked area label placement

forked from veltman‘s block: Stacked area label placement #2

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  text {
    font-family: sans-serif;
    font-size: 12px;
    fill: #222;
  }
  .axis line, .axis path {
    stroke: #222;
  }

  .area text {
    font-size: 14px;
    text-anchor: middle;
  }

  .hidden {
    display: none;
  }
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
console.clear()
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom,
    random = d3.randomNormal(0, 3),
    turtles = ["Leonardo", "Donatello", "Raphael", "Michelangelo"],
    colors = ["#ef9a9a", "#9fa8da", "#ffe082", "#80cbc4"];

var svg = d3.select("svg").append("g")
    .attr("transform", "translate(" + margin.left + " " + margin.top + ")");

var x = d3.scaleLinear().range([0, width]),
    y = d3.scaleLinear().range([height, 0]);

var series = svg.selectAll(".area")
  .data(turtles)
  .enter()
  .append("g")
  .attr("class", "area");

series.append("path")
  .attr("fill", (d, i) => colors[i]);

series.append("text")
  .attr("dy", 5)
  .text(d => d);

var xg = svg.append("g")
  .attr("class", "axis x")
  .attr("transform", "translate(0 " + height + ")");

var yg = svg.append("g")
  .attr("class", "axis y");

var stack = d3.stack().keys(turtles);

var line = d3.line()
  .curve(d3.curveMonotoneX);

randomize()

function randomize() {

  var data = [];

  // Random walk
  for (var i = 0; i < 40; i++) {
    data[i] = {};
    turtles.forEach(function(turtle){
      data[i][turtle] = Math.max(0, random() + (i ? data[i - 1][turtle] : 20));
    });
  }

  var stacked = stack(data);
  console.log(stacked)

  x.domain([0, data.length - 1]);
  y.domain([0, d3.max(stacked[stacked.length - 1].map(d => d[1]))]);

  series.data(stacked)
    .select("path")
    .attr("d", getPath);
	
  series.select("text")
    .classed("hidden", false)
    .datum(getBestLabel)
    .classed("hidden", d => !d)
    .filter(d => d)
    .attr("x", d => d[0])
    .attr("y", d => d[1]);
	
  
  //xg.call(d3.axisBottom(x).tickSizeOuter(0));
  //yg.call(d3.axisLeft(y).tickSizeOuter(0));

  //setTimeout(randomize, 750);

}

function getPath(area) {
  var top = area.map((f, j) => [x(j), y(f[1])]),
      bottom = area.map((f, j) => [x(j), y(f[0])]).reverse();

  return line(top) + line(bottom).replace("M", "L") + "Z";
}

// Could do this in linear time ¯\_(ツ)_/¯
function getBestLabel(points) {

  var bbox = this.getBBox(),
      numValues = Math.ceil(x.invert(bbox.width + 20)),
      bestRange = -Infinity,
      bestPoint;

  for (var i = 1; i < points.length - numValues - 1; i++) {

    var set = points.slice(i, i + numValues),
        floor = d3.min(set, d => y(d[0])),
        ceiling = d3.max(set, d => y(d[1]));

    if (floor - ceiling > bbox.height + 20 && floor - ceiling > bestRange) {
      bestRange = floor - ceiling;
      bestPoint = [
        x(i + (numValues - 1) / 2),
        (floor + ceiling) / 2
      ];
    }
  }

  return bestPoint;

}

</script>