Reputation: 572
d3.hierarchy generally and d3.partition in particular are some of my favorite tools out of that great library. But I'm applying them to radial "sunburst" viz for the first time, and seem to be missing some important bits.
Attached below is a MCVE
example generating this sunburst, to illustrate my main questions:
Rotating text labels beyond 180 degrees is a common issue; cf. this recent SO post
Following @mbostock's own example has this transform code:
.attr("transform", function(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
})
but using this translate()
in the transform throws the text far off
the chart?
So the code below does a rotation based on the same average of
inner/outer arc radii, and places the labels right-side (angles < 180)
the same way, except that it uses a text-anchor
alignment variation
to align both depths' labels with respect to the same common circle.
1 I've had to modify the radius by a hacked factor of 1.22
to nudge
the (right-side) labels close to the line; why?
2 This works great for all labels except the root's, which I want centered; how can I do that?
3 But the new left-side (> 180 degrees) labels are not well-placed,
and I've needed to add a depth-specific hackRatio
in order to
moderate the translation to even get them this close; why?
4 A deeper problem is to figure out how to use the same
text-anchor
alignment trick used for the other labels? I want to do the rotation "in place"
prior to alignment being applied; how can I do that?
The labels also include the freq
attribute in parentheses. The yearHier
data
provides this attribute only for data leaves. My impression from
the d3.hierarchy.sum()
and d3.partition doc
was that the call to sum()
on the root would calculate the sums for
non-leaves ("... for this node and each descendant in post-order traversal"); why are these frequencies zero?
So as an alternative, I tried using the yearHierFreq
data which incudes total frequencies for root and each year. But using it, d3.partition allocates only only two thirds of the years' arcs, and only half of the months' arcs within each year; see rendering below. It's as if the interior nodes' freq
is being double counted; why?
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
var ColorNames = ["Blue", "Gray", "Purple", "Fuchsia", "Aqua", "Maroon", "Olive", "Yellow", "Teal", "Navy", "Green", "Silver", "Red", "Lime"];
// following after http://bl.ocks.org/kerryrodden/7090426
var width = 900;
var height = 900;
var radius = Math.min(width, height) / 2 * 0.7;
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.partition()
.size([2 * Math.PI, radius * radius]);
var arc = d3.arc()
.startAngle(function(d) { return d.x0; })
.endAngle(function(d) { return d.x1; })
.innerRadius(function(d) { return Math.sqrt(d.y0); })
.outerRadius(function(d) { return Math.sqrt(d.y1); });
function createSunburst(json) {
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// Turn the data into a d3 hierarchy and calculate the sums.
var root = d3.hierarchy(json)
.sum(function(d) { return d.freq; })
.sort(function(a, b) { return b.name - a.name; });
var partition = d3.partition()
.size([2 * Math.PI, radius * radius]);
var nodes = partition(root).descendants();
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d,i) { return ColorNames[i % 14]; })
.style("opacity", 1);
var texts = vis.selectAll("text")
.data(nodes)
.enter().append("text")
/* .attr("transform", function(d) {
// https://beta.observablehq.com/@mbostock/d3-sunburst
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
})
*/
.attr("transform", function(d) {
var deg;
if (d.depth==0) {
deg = 90;
} else {
deg = 180 / Math.PI * (d.x0 +d.x1) / 2;
}
var trans = `rotate(${deg-90})`;
if (deg > 180) {
var hackRatio = (d.depth == 0) ? 160 : 130;
var yavg = (d.y0 + d.y1) / 2 / hackRatio;
trans += ` translate(${yavg},0) rotate(180)`;
}
return trans})
.attr("x", radius / 1.22 )
.text(function(d) {return `${d.data.name} (${d.data.freq})`;})
.attr("text-anchor", function(d) {
var alignVec = ["center","end","start"];
return alignVec[d.depth];})
};
var yearHier = {"freq": 0, "name": "AllYears", "children": [{"freq": 0, "name": "2017", "children": [{"freq": 5, "name": "January", "children": []}, {"freq": 17, "name": "February", "children": []}, {"freq": 16, "name": "March", "children": []}, {"freq": 2, "name": "April", "children": []}, {"freq": 18, "name": "May", "children": []}, {"freq": 14, "name": "June", "children": []}, {"freq": 17, "name": "July", "children": []}, {"freq": 2, "name": "August", "children": []}, {"freq": 10, "name": "September", "children": []}, {"freq": 6, "name": "October", "children": []}, {"freq": 10, "name": "November", "children": []}, {"freq": 17, "name": "December", "children": []}]}, {"freq": 0, "name": "2018", "children": [{"freq": 14, "name": "January", "children": []}, {"freq": 6, "name": "February", "children": []}, {"freq": 13, "name": "March", "children": []}, {"freq": 15, "name": "April", "children": []}, {"freq": 15, "name": "May", "children": []}, {"freq": 4, "name": "June", "children": []}, {"freq": 7, "name": "July", "children": []}, {"freq": 12, "name": "August", "children": []}, {"freq": 17, "name": "September", "children": []}, {"freq": 8, "name": "October", "children": []}, {"freq": 10, "name": "November", "children": []}, {"freq": 12, "name": "December", "children": []}]}, {"freq": 0, "name": "2019", "children": [{"freq": 10, "name": "January", "children": []}, {"freq": 12, "name": "February", "children": []}, {"freq": 15, "name": "March", "children": []}, {"freq": 6, "name": "April", "children": []}, {"freq": 14, "name": "May", "children": []}, {"freq": 3, "name": "June", "children": []}, {"freq": 6, "name": "July", "children": []}, {"freq": 9, "name": "August", "children": []}, {"freq": 18, "name": "September", "children": []}, {"freq": 4, "name": "October", "children": []}, {"freq": 8, "name": "November", "children": []}, {"freq": 16, "name": "December", "children": []}]}]}
var yearHierFreq = {"freq": 355, "name": "AllMonths", "children": [{"freq": 83, "name": "2017", "children": [{"freq": 4, "name": "January", "children": []}, {"freq": 7, "name": "February", "children": []}, {"freq": 4, "name": "March", "children": []}, {"freq": 11, "name": "April", "children": []}, {"freq": 16, "name": "May", "children": []}, {"freq": 8, "name": "June", "children": []}, {"freq": 5, "name": "July", "children": []}, {"freq": 3, "name": "August", "children": []}, {"freq": 10, "name": "September", "children": []}, {"freq": 3, "name": "October", "children": []}, {"freq": 2, "name": "November", "children": []}, {"freq": 10, "name": "December", "children": []}]}, {"freq": 156, "name": "2018", "children": [{"freq": 14, "name": "January", "children": []}, {"freq": 8, "name": "February", "children": []}, {"freq": 12, "name": "March", "children": []}, {"freq": 10, "name": "April", "children": []}, {"freq": 16, "name": "May", "children": []}, {"freq": 17, "name": "June", "children": []}, {"freq": 19, "name": "July", "children": []}, {"freq": 14, "name": "August", "children": []}, {"freq": 4, "name": "September", "children": []}, {"freq": 17, "name": "October", "children": []}, {"freq": 19, "name": "November", "children": []}, {"freq": 6, "name": "December", "children": []}]}, {"freq": 116, "name": "2019", "children": [{"freq": 4, "name": "January", "children": []}, {"freq": 15, "name": "February", "children": []}, {"freq": 12, "name": "March", "children": []}, {"freq": 8, "name": "April", "children": []}, {"freq": 3, "name": "May", "children": []}, {"freq": 5, "name": "June", "children": []}, {"freq": 13, "name": "July", "children": []}, {"freq": 19, "name": "August", "children": []}, {"freq": 12, "name": "September", "children": []}, {"freq": 11, "name": "October", "children": []}, {"freq": 5, "name": "November", "children": []}, {"freq": 9, "name": "December", "children": []}]}]}
createSunburst(yearHier);
d3.select(self.frameElement).style("height", "700px");
</script>
Upvotes: 2
Views: 524
Reputation: 28713
You can get the following result
with this code
var radiusSeparation = 5;
var texts = vis.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("transform", function(d) {
if (d.depth == 0) return null;
d.deg = 180 / Math.PI * (d.x0 + d.x1) * 0.5;
var translate = d.depth == 1 ? Math.sqrt(d.y1)-radiusSeparation : Math.sqrt(d.y0)+radiusSeparation;
var trans = `rotate(${(d.deg-90).toFixed(2)}) translate(${translate.toFixed(2)},0)`;
if (d.deg > 180) {
trans += ` rotate(180)`;
}
return trans;
})
.text( d => `${d.data.name} (${d.value})` )
.attr("text-anchor", function(d) {
if (d.depth == 0) return "middle";
if (d.depth == 1) return d.deg < 180 ? "end" : "start";
return d.deg < 180 ? "start" : "end";
})
.attr("dominant-baseline", "middle")
use the radius of your arcs to position the text. Use a small separation distance so the text does not touch the arcs
store the deg
value in the datum so you can use it for the text anchor
switch text-anchor based on deg value
treat depth=0 as special in all cases
vertical align the text to the middle with dominant-baseline
d3.hierarchy.sum() stores the result in d.value
, so use this in the text
Upvotes: 3