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