block by EE2dev fc880e1cfbb80f649878f3d5b9e8ed93

Vertical alignment of text labels

Full Screen

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

index.html

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