Reputation: 3856
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:
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:
What is my error here, and what should I do to fix it?
Upvotes: 4
Views: 2508
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