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
对齐变体
将两个深度的标签相对于同一个公共圆对齐。
1 我不得不修改半径 1.22
靠近线的(右侧)标签; 为什么?
2 这适用于所有标签,除了我想要的根标签
居中; 我该怎么做?
3但是新的左侧(>180度)标签位置不好,
我需要添加一个深度特定的 hackRatio
以便
适度翻译,甚至让他们如此接近; 为什么?
4更深层次的问题是弄清楚如何使用相同的
text-anchor
对齐技巧用于其他标签?我想做旋转 "in place"
在应用对齐之前; 我该怎么做?
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
中,所以在文中使用这个
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
对齐变体
将两个深度的标签相对于同一个公共圆对齐。
1 我不得不修改半径
1.22
靠近线的(右侧)标签; 为什么?2 这适用于所有标签,除了我想要的根标签 居中; 我该怎么做?
3但是新的左侧(>180度)标签位置不好, 我需要添加一个深度特定的
hackRatio
以便 适度翻译,甚至让他们如此接近; 为什么?4更深层次的问题是弄清楚如何使用相同的
text-anchor
对齐技巧用于其他标签?我想做旋转 "in place" 在应用对齐之前; 我该怎么做?
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
中,所以在文中使用这个