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.
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> |