Picking best label positions in a streamgraph along the same lines as this example, but fitting the maximum possible font size in each area.
Last active
January 16, 2020 10:50
-
-
Save veltman/7ae69fcf60b75ad1cc416bd579f52deb to your computer and use it in GitHub Desktop.
Streamgraph label positions #2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
text { | |
font-family: sans-serif; | |
fill: #222; | |
} | |
.area text { | |
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), | |
maxFontSize = 128, | |
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", "0.35em") | |
.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 < 60; 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); | |
series.select("text") | |
.classed("hidden", false) | |
.style("font-size", maxFontSize + "px") | |
.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, | |
found; | |
// Assumes constant aspect ratio which isn't exactly true | |
// Could update the text element with new font-size and | |
// remeasure every iteration to be precise | |
for (var i = maxFontSize; i >= 2; i -= 2) { | |
numValues = Math.ceil(x.invert(1.2 * bbox.width * i / maxFontSize)), | |
found = findSpace(points, numValues, bbox.height * i / maxFontSize); | |
if (found) { | |
d3.select(this).style("font-size", i + "px"); | |
return found; | |
} | |
} | |
return null; | |
} | |
function findSpace(points, numValues, textHeight) { | |
var 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 > textHeight * 1.2 && 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