Using polylabel plus some extra boundary checks to position labels in a stacked area chart.
See also: Stacked area label placement #2
xxxxxxxxxx
<meta charset="utf-8">
<style>
text {
font-family: sans-serif;
font-size: 14px;
fill: #222;
}
.axis line, .axis path {
stroke: #222;
}
.area text {
font-size: 18px;
text-anchor: middle;
}
.hidden {
display: none;
}
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="polylabel.min.js"></script>
<script>
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
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]);
var xg = svg.append("g")
.attr("class", "axis x")
.attr("transform", "translate(0 " + height + ")");
var yg = svg.append("g")
.attr("class", "axis y");
series.append("text")
.attr("dy", 5)
.text(d => d);
var stack = d3.stack().keys(turtles);
var line = d3.line()
.curve(d3.curveMonotoneX);
randomize();
function randomize() {
var data = [];
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] : 20));
});
}
var stacked = stack(data);
x.domain([0, data.length - 1]);
y.domain([0, d3.max(stacked[stacked.length - 1].map(d => d[1]))]);
series.data(getPositions(stacked))
.select("path")
.attr("d", d => d.path);
series.select("text")
.classed("hidden", isHidden)
.attr("x", d => d.label[0])
.attr("y", d => d.label[1]);
xg.call(d3.axisBottom(x).tickSizeOuter(0));
yg.call(d3.axisLeft(y).tickSizeOuter(0));
setTimeout(randomize, 750);
}
function getPositions(stacked) {
return stacked.map(function(area){
var top = area.map((f, j) => [x(j), y(f[1])]),
bottom = area.map((f, j) => [x(j), y(f[0])]).reverse();
// Exclude the left- and right-most points from the polygon to avoid the edges a bit
return {
area: area,
label: polylabel([top.slice(1, area.length - 1).concat(bottom.slice(1, area.length - 1))]),
path: line(top) + line(bottom).replace("M", "L") + "Z"
};
});
}
// Does label fit in its assigned position?
function isHidden(d) {
var bbox = this.getBBox(),
labelStart = d.label[0] - bbox.width / 2,
labelEnd = d.label[0] + bbox.width / 2,
startIndex,
endIndex;
// Overlaps the left or right edge
if (labelEnd > width || labelStart < 0) {
return true;
}
// left x value
startIndex = Math.max(0, Math.floor(x.invert(labelStart)));
// right x value
endIndex = Math.min(d.area.length - 1, Math.ceil(x.invert(labelEnd)));
for (var i = startIndex; i <= endIndex; i++) {
// Would intersect the bottom
if (y(d.area[i][0]) < d.label[1] + 5 + bbox.height / 2) {
return true;
}
// Would intersect the top
if (y(d.area[i][1]) > d.label[1] + 5 - bbox.height / 2) {
return true;
}
}
return false;
}
</script>
https://d3js.org/d3.v4.min.js