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

11,237

Solution 1

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.

Solution 2

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.

Solution 3

For small angles(less than 5% of the Pie Chart), I have changed the centroid value for the respective labels. I have used this code:

    arcs.append("text") 
        .attr("transform", function(d,i) {
            var centroid_value = arc.centroid(d);

            var pieValue = ((d.endAngle - d.startAngle)*100)/(2*Math.PI);                
            var accuratePieValue = pieValue.toFixed(0);
            if(accuratePieValue <= 5){
                var pieLableArc = d3.svg.arc().innerRadius(i*20).outerRadius(outer_radius + i*20);
                centroid_value = pieLableArc.centroid(d);
            }

            return "translate(" + centroid_value + ")";
        })
        .text(function(d, i) { ..... });

Solution 4

@LarsKotthoff

Finally I have solved the problem. I have used stack approach to display the labels. I made a virtual stack on both left and right side. Based the angle of the slice, I allocated the stack-row. If stack row is already filled then I find the nearest empty row on both top and bottom of desired row. If no row found then the value (on the current side) with least share angle is removed from the stack and labels are adjust accordingly.

See the working example here: http://manicharts.com/#/demosheet/3d-donut-chart-smart-labels

Share:
11,237
Visa Kopu
Author by

Visa Kopu

Freelance Web Developer for hire, a Mac fanboy and father of two.

Updated on June 18, 2022

Comments

  • Visa Kopu
    Visa Kopu almost 2 years

    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;
        });
    
  • Chris Nicola
    Chris Nicola about 10 years
    This approach works but it has some quirks. The most notable is that it wont work if you are using transitions unless you have to wait till they have moved. With some minor changes it isn't to hard to pre-compute the locations by using d3.svg.arc() to position the labels. I've done this in my AngularD3 library here: github.com/WealthBar/angular-d3/blob/…
  • Lars Kotthoff
    Lars Kotthoff about 10 years
    Or you could simply do the calculations as above and store the positions, reset to the original ones and then start the transition to the previously computed positions.
  • Chris Nicola
    Chris Nicola about 10 years
    That's what I did. However you can't use bounding box unless you've already positioned them. With transitions this results in a "jerking" effect as the overlapping items move twice.
  • nothingisnecessary
    nothingisnecessary over 9 years
    Awesome - this works for me (not using transitions), but since only adjusts position of slices 2 through N it fails to detect overlap between the labels for Nth slice and 1st slice. Fortunately this was easy to fix by using same logic to compare label-N to label-1 and by using a different textOffset (to avoid going around and around).
  • Lars Kotthoff
    Lars Kotthoff over 9 years
    @nothingisnecessary This question also has a few answers that may be helpful in this context.
  • Lars Kotthoff
    Lars Kotthoff about 9 years
    @codesnooker Yes, it won't work in extreme cases like this. Two things would be necessary to make it work: 1) take into account overlap between any labels and not just subsequent ones and 2) iterate the moving label process until convergence. Both would make it much more computationally expensive.
  • codesnooker
    codesnooker about 9 years
    @LarsKotthoff Thanks! I am trying now different approach. I will post the link if I succeed. Your solution gave me a good start.
  • Liad Livnat
    Liad Livnat almost 8 years
    the solution is not working for other example since it based only on this example
  • Piotr Chojnacki
    Piotr Chojnacki almost 4 years
    The link is dead, unfortunately.
  • codesnooker
    codesnooker almost 4 years
    yeah... actually sold off the product, so I have had to remove it. Will try to arrange something similar in future.
  • Caleb Hensley
    Caleb Hensley over 3 years
    Any updates on a similar arrangement? I'm having an issue with overlapping labels, I would love to see how you've been able to solve this @codesnooker