Skip to content

Instantly share code, notes, and snippets.

@veltman
Last active September 17, 2017 12:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veltman/14006cc042f5dff5a6e1ddf041afbae6 to your computer and use it in GitHub Desktop.
Save veltman/14006cc042f5dff5a6e1ddf041afbae6 to your computer and use it in GitHub Desktop.
Streamgraph label positions

Picking best label positions in a streamgraph along the same lines as this stacked area chart example.

If a label doesn't fit in the top or bottom series, it tries to place it in the adjacent empty space.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: 14px sans-serif;
fill: #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: 10, right: 0, bottom: 10, left: 0 },
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 stack = d3.stack().keys(turtles)
.order(d3.stackOrderInsideOut)
.offset(d3.stackOffsetWiggle);
var line = d3.line()
.curve(d3.curveMonotoneX);
randomize();
function randomize() {
var data = [];
// Random-ish 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] : 10));
});
}
var stacked = stack(data);
x.domain([0, data.length - 1]);
y.domain([
d3.min(stacked.map(d => d3.min(d.map(f => f[0])))),
d3.max(stacked.map(d => d3.max(d.map(f => f[1]))))
]);
series.data(stacked)
.select("path")
.attr("d", getPath);
stacked.forEach(function(d, i){
if (d[0][1] === d3.max(stacked.map(f => f[0][1]))) {
d.top = true;
}
if (d[0][0] === d3.min(stacked.map(f => f[0][0]))) {
d.bottom = true;
}
});
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]);
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";
}
function getBestLabel(points) {
var bbox = this.getBBox(),
numValues = Math.ceil(x.invert(bbox.width + 20)),
finder = findSpace(points, bbox, numValues);
// Try to fit it inside, otherwise try to fit it above or below
return finder() ||
(points.top && finder(y.range()[1])) ||
(points.bottom && finder(null, y.range()[0]));
}
function findSpace(points, bbox, numValues) {
return function(top, bottom) {
var bestRange = -Infinity,
bestPoint,
set,
floor,
ceiling,
textY;
// Could do this in linear time ¯\_(ツ)_/¯
for (var i = 1; i < points.length - numValues - 1; i++) {
set = points.slice(i, i + numValues);
if (bottom != null) {
floor = bottom;
ceiling = d3.max(set, d => y(d[0]));
} else if (top != null) {
floor = d3.min(set, d => y(d[1]));
ceiling = top;
} else {
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;
if (bottom != null) {
textY = ceiling + bbox.height / 2 + 10;
} else if (top != null) {
textY = floor - bbox.height / 2 - 10;
} else {
textY = (floor + ceiling) / 2;
}
bestPoint = [
x(i + (numValues - 1) / 2),
textY
];
}
}
return bestPoint;
};
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment