This is an example how to compute the position of text labels for stacked bar charts. The challenge is to align the labels when the stacked areas are very small and next to each other leading to overlapping text labels. Or when a stacked area is very close to the border.
In order to simulate the algorithm, rectangles are drawn, corresponding to the bounding box of a text label. On the left, the rectangles are randomly initialized. In one or two passes the final position is computed. Reload page to see different initial seeting.
Built with blockbuilder.org
xxxxxxxxxx
<meta charset="utf-8">
<style>
rect {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
rect.before{
stroke: red;
}
rect.after{
stroke: orange;
}
rect.final{
stroke: green;
}
</style>
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 40, right: 40, bottom: 40, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("rect")
.attr("class", "frame")
.attr("width", width)
.attr("height", height);
var rectHeight = 35;
var data = [];
var trans = 0;
for (var i=0; i<8; i++) {
data.push(Math.round(Math.random() * (height) - rectHeight/2));
}
data.sort(function(a, b){return b - a});
drawRectangles(g, data, "before");
var dataObject = createObject(data);
dataObject = adjustBottoms(dataObject);
var data2 = trimObject(dataObject);
drawRectangles(g, data2, "after");
if (data2[data2.length-1] < 0) {
dataObject = adjustTops(dataObject);
var data3 = trimObject(dataObject);
drawRectangles(g, data3, "final");
}
function drawRectangles(sel, data, className){
var gNew = sel.append("g")
.attr("transform", "translate(" + trans + ", 0)");
gNew.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {return "translate(100, " + d + ")";})
.append("rect")
.attr("class", className)
.attr("width", "50px")
.attr("height", rectHeight);
trans += 200;
}
function createObject(data) {
// setup data structure with rectangles from bottom to the top
var dataObject = [];
var obj = {top: height, bottom: height + rectHeight}; // add dummy rect for lower bound
dataObject.push(obj);
data.forEach(function(d,i){
obj = {top: d, bottom: d + rectHeight}
dataObject.push(obj);
});
obj = {top: 0 - rectHeight, bottom: 0}; // add dummy rect for upper bound
dataObject.push(obj);
return dataObject;
}
function trimObject(dataObject) { // convert back to original array of values, also remove dummies
var data3 = [];
dataObject.forEach(function(d,i){
if (!(i === 0 || i === dataObject.length-1)) {
data3.push(d.top);
}
});
return data3;
}
function adjustBottoms(dataObject){
dataObject.forEach(function(d,i){
if (!(i === 0 || i === dataObject.length-1)) {
var diff = dataObject[i-1].top - d.bottom;
if (diff < 0) { // move rect up
d.top += diff;
d.bottom += diff;
}
}
});
return dataObject;
}
function adjustTops(dataObject){
for (var i = dataObject.length; i-- > 0; ){
if (!(i === 0 || i === dataObject.length-1)) {
var diff = dataObject[i+1].bottom - dataObject[i].top;
if (diff > 0) { // move rect down
dataObject[i].top += diff;
dataObject[i].bottom += diff;
}
}
};
return dataObject;
}
</script>
https://d3js.org/d3.v4.min.js