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