Aleksey Bilogur
Aleksey Bilogur

Reputation: 3856

D3 text rotation in a pie chart

I have a little D3 script:

<script>
    d3.csv("../data/school_attendance.csv", function(data) {
        // Use d3.pie() to create and configure the data that we need in a format that we can enter into.
        let arc_data = d3.pie().value(d => d['YTD Enrollment(Avg)']).padAngle(d => 0.0115)(data);

        // Create the arc factory function that will render each data segment.
        let arc = d3.arc().innerRadius(75).outerRadius(160);

        // Run through each element of arc_data, creating and appening the arc for each one.
        d3.select("svg")
            .append("g")
            .attr("id", "transform")
            .attr("transform", "translate(400, 200)")
            .selectAll('path')
            .data(arc_data)
            .enter()
            .append('path')
            .attr('d', arc)
            .attr('fill', 'steelblue');

        // Use arc and arc_data to calculate centroids, and from there to calculate.
        arc_data.forEach(function(d, i) {
            [x, y] = arc.centroid(d);
            let label = d.data['District']
            // let rotation = d['startAngle'] * 180 / Math.PI;
            let rotation = d['startAngle'] / Math.PI / 2
            d3.select("#transform").append("text")
                .attr("x", x).attr("y", y)
                .attr("text-anchor", "middle").attr("alignment-baseline", "middle")
                .attr("transform", "rotate(" + rotation + ")")
                .text(label);
        })
    })
</script>

This produces the following output:

enter image description here

I'd like to rotate the text labels so that they appear in the middle of each of the arc segments.

However, what seems to me to be the obvious answer:

let rotation = d['startAngle'] / Math.PI / 2 * 360 - 90;

Doesn't work as expected:

enter image description here

What is my error here, and what should I do to fix it?

Upvotes: 4

Views: 2508

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102218

It seems to me that one of the problems here is setting the x and y position of the labels using attr. Instead of that, translate them:

.attr("transform", "translate(" + [x,y] + ")");

After that, comes the math:

var rotation = (d.startAngle/2 + d.endAngle/2) * 180/Math.PI;

But the above variable has the problem of positioning all texts going from the center of the donut to the borders, and some labels (on the left side of the donut) end up upside down, going from the right to the left. As we read from left to write, it seems to me that this ternary is more elegant:

var rotation = d.endAngle < Math.PI ? 
    (d.startAngle/2 + d.endAngle/2) * 180/Math.PI : 
    (d.startAngle/2  + d.endAngle/2 + Math.PI) * 180/Math.PI ;

Here is a demo:

const width = 400
const height = 400;
const radius = Math.min(width, height) / 2.5;

const totals = [{
    "name": "District A",
    "value": 20
}, {
    "name": "District B",
    "value": 50
}, {
    "name": "District C",
    "value": 30
}, {
    "name": "District D",
    "value": 20
}, {
    "name": "District E",
    "value": 50
}, {
    "name": "District F",
    "value": 30
}];

const color = d3.scaleOrdinal()
    .range(['#869099', '#8c7853', '#007d4a']);

const arc = d3.arc()
    .outerRadius(radius - 10)
    .innerRadius(40);

const pie = d3.pie()
    .sort(null)
    .value((d) => {
        return d.value
    });

const svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height)
    .append('g')
    .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

const g = svg.selectAll('.arc')
    .data(pie(totals))
    .enter()
    .append('g')
    .attr('class', 'arc');

g.append('path')
    .attr('d', arc)
    .style('fill', 'steelblue')
    .style('stroke', 'white');

pie(totals).forEach(function(d, i) {
    [x, y] = arc.centroid(d);
    let label = d.data.name;
    var rotation = d.endAngle < Math.PI ? (d.startAngle / 2 + d.endAngle / 2) * 180 / Math.PI : (d.startAngle / 2 + d.endAngle / 2 + Math.PI) * 180 / Math.PI;
    svg.append("text")
        .attr("text-anchor", "middle").attr("alignment-baseline", "middle")
        .attr("transform", "translate(" + [x, y] + ") rotate(-90) rotate(" + rotation + ")")
        .text(label);
})
<script src="https://d3js.org/d3.v4.min.js"></script>

Upvotes: 9

Related Questions