7
votes

I'm using d3.js to create a donut chart with labels on the outside. Using some trigonometry based on the centroids of each slice of the pie, I position the labels.

g.append("g")
        .attr("class", "percentage")
        .append("text")
            .attr("transform", function(d)
                { 
                    var c = arc.centroid(d);
                    var x = c[0];
                    var y = c[1];
                    var h = Math.sqrt(x*x + y*y);
                    return "translate(" + (x/h * obj.labelRadius) +  ',' + (y/h * obj.labelRadius) +  ")"; 
                }
            ) 
            .attr("dy", ".4em")
            .attr("text-anchor", function(d) 
                {
                    return (d.endAngle + d.startAngle)/2 > Math.PI ? "end" : "start";
                }
            )
            .text(function(d) { return d.data.percentage+"%"; });

What I'm ultimately trying to accomplish is to rearrange labels that are outside the edges of the pie chart, to prevent overlaps.

enter image description here

One of the ways I have tried to solve the problem is to define set "anchor points", where labels can be positioned, guaranteeing that they will no overlap. Problem is mapping the centroids to the anchors and preserving some sense of visual correspondence between the slices and the labels (Specially difficult when slices are slim).

enter image description here

Image above shows the possible location of the anchors (centroids of the slices shown). With these positions it is impossible to have an overlap.

Adding complexity to the problem is the fact that when labels (they're horizontal) are close to the top or bottom of the pie, they are more easily overlapped, than when they are on the right or left of the pie.

Any ideas on how to approach this problem?

[EDIT] Following the suggestion of meetamit, I implemented the following:

.attr("dy", function(d)
{
    var c = arc.centroid(d);
        var x = c[0];
        var y = c[1];
        var h = Math.sqrt(x*x + y*y);
        var dy = y/h * obj.labelRadius; 
    dy=dy*fontSizeParam*.14/heightParam);
    return (dy)+"em";
})

It helps a bit, and gives some room to the labels, still looking for a solution that will cover all cases though...

2
Could you put up an example of what you have so far on jsfiddle? - Jonah
I added an image for clarity. I haven't implemented in code the "anchor" solution, as I've worked out on paper various cases where it can fail. - Cenobyte321
I see. Indeed, you have a difficult problem, and one that really has little to do with d3 per se: you are seeking a general algorithm for the placement of labels on your graph. Can you make any assumptions? Eg, if all the pieces were as little as the green and yellow ones, there would simply be no good way to fit all the labels in? What about showing the label only on hover for pieces that are below a certain size? - Jonah
Thanks Jonah. Hover is ruled out as they are touch devices and won't support interactivity. Some possible assumptions: No more than 12 slices allowed, so there is always a possible arrangement of the labels without overlap. Mapping the positions and keeping visual coherence is the problem here. - Cenobyte321
Could you not rotate your labels to stick radially outward? - Anko

2 Answers

1
votes

Can't you create two arcs? one for the chart, and one for the labels?

// first arc used for drawing the pie chart
var arc = d3.svg.arc()
  .outerRadius(radius - 10)
  .innerRadius(0);

// label attached to first arc
g.append("text")
  .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
  .attr("dy", ".35em")
  .style("text-anchor", "middle")
  .text(function(d) { return d.data.age; });

// second arc for labels
var arc2 = d3.svg.arc()
  .outerRadius(radius + 20)
  .innerRadius(radius + 20);

// label attached to second arc
g.append("text")
  .attr("transform", function(d) { return "translate(" + arc2.centroid(d) + ")"; })
  .attr("dy", ".35em")
  .style("text-anchor", "middle")
  .text(function(d) { return d.data.age; });
1
votes

There is a good, pragmatic d3.js-based solution by programmer John Williams presented here:

https://www.safaribooksonline.com/blog/2014/03/11/solving-d3-label-placement-constraint-relaxing/

It should work well for cases with reasonable restrictions, e.g. a maximum of 12 labels as discussed above. There are also pointers in the article to more advanced algorithms, but this simple approach may actually, when used with sufficient label-content constraints, give results that have a more orderly visual appearance than other methods would yield.