score:1

@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

score:1

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) { ..... });

score: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.

score:5

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.


Related Query

More Query from same tag