Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid labels overlapping in a D3.js pie chart?

I'm drawing a pie chart using D3.js with a quite simple script. The problem is that when slices are small, their labels overlap.

What options do I have to prevent them from overlapping? Does D3.js have built-in mechanisms I could exploit?

Demo: http://jsfiddle.net/roxeteer/JTuej/

var container = d3.select("#piechart");
var data = [
        { name: "Group 1", value: 1500 },
        { name: "Group 2", value: 500 },
        { name: "Group 3", value: 100 },
        { name: "Group 4", value: 50 },
        { name: "Group 5", value: 20 }
    ];
var width = 500;
var height = 500;
var radius = 150;
var textOffset = 14;

var color = d3.scale.category20();

var svg = container.append("svg:svg")
    .attr("width", width)
    .attr("height", height);

var pie = d3.layout.pie().value(function(d) {
    return d.value;
});

var arc = d3.svg.arc()
    .outerRadius(function(d) { return radius; });

var arc_group = svg.append("svg:g")
    .attr("class", "arc")
    .attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");

var label_group = svg.append("svg:g")
    .attr("class", "arc")
    .attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");

var pieData = pie(data);

var paths = arc_group.selectAll("path")
    .data(pieData)
    .enter()
    .append("svg:path")
    .attr("stroke", "white")
    .attr("stroke-width", 0.5)
    .attr("fill", function(d, i) { return color(i); })
    .attr("d", function(d) {
        return arc({startAngle: d.startAngle, endAngle: d.endAngle});
    });

var labels = label_group.selectAll("path")
    .data(pieData)
    .enter()
    .append("svg:text")
    .attr("transform", function(d) {
        return "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) * (radius + textOffset) + "," + Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) * (radius + textOffset) + ")";
    })
    .attr("text-anchor", function(d){
        if ((d.startAngle  +d.endAngle) / 2 < Math.PI) {
            return "beginning";
        } else {
            return "end";
        }
    })
    .text(function(d) {
        return d.data.name;
    });
like image 325
Visa Kopu Avatar asked Oct 30 '13 12:10

Visa Kopu


2 Answers

D3 doesn't offer anything built-in that does this, but you can do it by, after having added the labels, iterating over them and checking if they overlap. If they do, move one of them.

var prev;
labels.each(function(d, i) {
  if(i > 0) {
    var thisbb = this.getBoundingClientRect(),
        prevbb = prev.getBoundingClientRect();
    // move if they overlap
    if(!(thisbb.right < prevbb.left || 
            thisbb.left > prevbb.right || 
            thisbb.bottom < prevbb.top || 
            thisbb.top > prevbb.bottom)) {
        var ctx = thisbb.left + (thisbb.right - thisbb.left)/2,
            cty = thisbb.top + (thisbb.bottom - thisbb.top)/2,
            cpx = prevbb.left + (prevbb.right - prevbb.left)/2,
            cpy = prevbb.top + (prevbb.bottom - prevbb.top)/2,
            off = Math.sqrt(Math.pow(ctx - cpx, 2) + Math.pow(cty - cpy, 2))/2;
        d3.select(this).attr("transform",
            "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) *
                                    (radius + textOffset + off) + "," +
                           Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) *
                                    (radius + textOffset + off) + ")");
    }
  }
  prev = this;
});

This checks, for each label, if it overlaps with the previous label. If this is the case, a radius offset is computed (off). This offset is determined by half the distance between the centers of the text boxes (this is just a heuristic, there's no specific reason for it to be this) and added to the radius + text offset when recomputing the position of the label as originally.

The maths is a bit involved because everything needs to be checked in two dimensions, but it's farily straightforward. The net result is that if a label overlaps a previous label, it is pushed further out. Complete example here.

like image 170
Lars Kotthoff Avatar answered Sep 18 '22 13:09

Lars Kotthoff


The actual problem here is one of label clutter. So, you could try not displaying labels for very narrow arcs:

.text(function(d) {
    if(d.endAngle - d.startAngle<4*Math.PI/180){return ""}
    return d.data.key; });

This is not as elegant as the alternate solution, or codesnooker's resolution to that issue, but might help reduce the number of labels for those who have too many. If you need labels to be able to be shown, a mouseover might do the trick.

like image 36
Roland Heath Avatar answered Sep 17 '22 13:09

Roland Heath