Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active November 8, 2019 08:57
Show Gist options
  • Save veltman/0f7ed47d7839ba0afa8d23414aeb8933 to your computer and use it in GitHub Desktop.
Save veltman/0f7ed47d7839ba0afa8d23414aeb8933 to your computer and use it in GitHub Desktop.
Stacked area label placement #2

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

<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font-family: sans-serif;
font-size: 14px;
fill: #222;
}
.axis line, .axis path {
stroke: #222;
}
.area text {
font-size: 20px;
text-anchor: middle;
}
.hidden {
display: none;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
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);
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment