d3.partition 旭日:旋转文本和其他故障

d3.partition sunbursts: rotating text and other glitches

d3.hierarchy 通常和 d3.partition 特别是我的一些 那个很棒的库中最喜欢的工具。但我将它们应用到 radial "sunburst" viz 是第一次,似乎缺少一些 重要位。

下面附上一张MCVE 生成此旭日形图的示例,以说明我的主要问题:

旋转文本

文本标签旋转超过 180 度是一个常见问题; 比照。

关注 @mbostock's own example 有这个转换代码:

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

但在转换中使用此 translate() 会使文本偏离很远 图表?

所以下面的代码根据相同的平均值进行旋转 inner/outer 圆弧半径,并将标签放在右侧(角度 < 180) 相同的方式,除了它使用 text-anchor 对齐变体 将两个深度的标签相对于同一个公共圆对齐。

d3.hierarchy.sum() 应该如何工作?

标签还在括号中包含 freq 属性。 yearHier 数据 仅为数据 leaves 提供此属性。我的印象来自 d3.hierarchy.sum()d3.partition 文件 是在根上调用 sum() 会计算总和 non-leaves ("...对于这个节点和post顺序遍历中的每个后代"); 为什么这些频率为零?

因此,作为替代方案,我尝试使用 yearHierFreq 数据,其中包括根和每年的总频率。但是使用它,d3.partition只分配了年弧的三分之二,每年也只分配了月弧的一半;请参见下面的渲染图。就好像内部节点的 freq 被重复计算一样; 为什么

<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>

可以得到如下结果

使用此代码

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")
  • 使用圆弧的半径来定位文本。使用较小的分隔距离,使文本不接触弧线

  • deg 值存储在数据中,以便您可以将其用于文本锚点

  • 根据 deg 值切换文本锚点

  • 在所有情况下都将 depth=0 视为特殊值

  • 将文本垂直居中对齐 dominant-baseline

  • d3.hierarchy.sum()将结果存储在d.value中,所以在文中使用这个