Javascript d3-嵌套饼图的标签放置
我想放置类似下图所示的标签。这可能是一种“一问二问”的方式,对此表示抱歉 尝试了两种不同的方法 一个不合适的方法是,从一个圆开始,独立地绘制每个段,并依靠数据的排序方式和标记为Javascript d3-嵌套饼图的标签放置,javascript,d3.js,charts,Javascript,D3.js,Charts,我想放置类似下图所示的标签。这可能是一种“一问二问”的方式,对此表示抱歉 尝试了两种不同的方法 一个不合适的方法是,从一个圆开始,独立地绘制每个段,并依靠数据的排序方式和标记为parent的属性来识别块中的段(主段/较大段)。这样,我就无法根据主线段在圆圈中的位置轻松放置标签,而且感觉不自然 更合适的方法是,将块(主段)和内部块作为子块,这样,我可以使用质心并相应地放置标签。此外,事情看起来很自然,但我不知道如何在主分段中绘制多个内部分段,因此它看起来像我之前尝试中的图表 在每个脚本的开头
parent
的属性来识别块中的段(主段/较大段)。这样,我就无法根据主线段在圆圈中的位置轻松放置标签,而且感觉不自然
更合适的方法是,将块(主段)和内部块作为子块,这样,我可以使用质心
并相应地放置标签。此外,事情看起来很自然,但我不知道如何在主分段中绘制多个内部分段,因此它看起来像我之前尝试中的图表
在每个脚本的开头,
console.log(Data)
在colors
数组之前模拟数据,以查看我要说明的数据的确切结构。我不确定是否正确理解了这个问题。但是这太长了,不能塞进评论中,所以我写了一个答案,也许它解决了问题
第一种方法的规定问题是:
这样,我就不能很容易地根据主要部分的位置放置标签
入围
标签放置代码为:
labels.selectAll("text")
.data(keys)
.enter()
.append("text")
.style("text-anchor", "middle")
.style("font-weight", "bold")
//LABEL PLACEMENT CODE
.attr("x", (d, i) => {
return barScale(config.max * 1.2) * Math.cos(segmentSlice * i - Math.PI / 2);
})
.attr("y", (d, i) => {
return barScale(config.max * 1.2) * Math.sin(segmentSlice * i - Math.PI / 2);
})
这将沿着大段分界线放置标签。我们一共有12段,每段跨越30度。每个大段有6个子段,每个子段跨越5度。因此,您似乎只需要将标签旋转15度(3个子段跨度)
将它们像问题中的图片一样放置
首先将15度转换为弧度:
15 * PI / 180 = 0.261799
然后将上述值添加到标签放置代码:
.attr("x", (d, i) => {
return barScale(config.max * 1.2) *
Math.cos(segmentSlice * i - Math.PI / 2 + 0.261799); //HERE
}).attr("y", (d, i) => {
return barScale(config.max * 1.2) *
Math.sin(segmentSlice * i - Math.PI / 2 + 0.261799); //AND HERE
})
以下是更新的小提琴:
所有标签的位置都与给定的图片相同。“数据”属性还可用于根据大/小段的组合更改旋转角度。这样,每个标签的位置就可以精确到所需的数量。您已经拥有的布局取决于您的数据是否统一,这在现实世界中是不会发生的,因此我找到了一个数据集,并使用它创建了一个不需要完美数据的饼图 这是第一张和第二张图表的混合。我已经在代码中添加了大量的注释,所以请仔细查看并检查您是否理解正在发生的事情。我在上面放了一个演示,这样你就可以看到数据输入了
function chart(id) {
// this reads in the CSV file
d3.csv('morley3.csv').then( data => {
// this massages the data I'm using into a more suitable form for your chart
// we have 12 runs with 6 experiments in each.
// each datum is of the form
// { Run: <number>, Expt: <number>, Speed: <number> }
const filteredData = data
.filter( d => d.Run < 13 )
.map( d => { return { Run: +d.Run, Expt: +d.Expt, Speed: +d.Speed } } )
// set up the chart
const width = 800,
height = 800,
radius = Math.min(height, width) * 0.5 - 100,
// how far away from the chart the labels should be
labelOffset = 10,
svg = d3.select(id).append("svg")
.attr("width", width)
.attr("height", height),
g = svg.append("g")
.attr("transform", `translate(${width/2}, ${height/2})`),
// this will be used to generate the pie segments
arc = d3.arc()
.outerRadius(radius)
.innerRadius(0),
// group the data by the run number
// this results in 12 groups of six experiments
// the nested data has the form
// [ { key: <run #>, values: [{ Run: 1, Expt: 1, Speed: 958 }, { Run: 1, Expt: 2, Speed: 869 } ... ],
// { key: 2, values: [{ Run: 2, Expt: 1, Speed: 987 },{ Run: 2, Expt: 2, Speed: 809 } ... ],
// etc.
nested = d3.nest()
.key( d => +d.Run )
.entries(filteredData),
chunkSize = nested[0].values.length,
// d3.pie() is the pie chart generator
pie = d3.pie()
// the size of each slice will be the sum of all the Speed values for each run
.value( d => d3.sum( d.values, function (e) { return e.Speed } ) )
// sort by run #
.sort( (a,b) => a.key - b.key )
(nested)
// bind the data to the DOM. Add a `g` for each run
const runs = g.selectAll(".run")
.data(pie, d => d.key )
.enter()
.append("g")
.classed('run', true)
.each( d => {
// run the pie generator on the children
// d.data.values is all the experiments in the run, or in pie terms,
// all the experiments in this piece of the pie. We're going to use
// `startAngle` and `endAngle` to specify that we're only generating
// part of the pie. The values for `startAngle` and `endAngle` come
// from using the pie chart generator on the run data.
d.children = d3.pie()
.value( e => e.Speed )
.sort( (a,b) => a.Expt - b.Expt )
.startAngle( d.startAngle )
.endAngle( d.endAngle )
( d.data.values )
})
// we want to label each run (rather than every single segment), so
// the labels get added next.
runs.append('text')
.classed('label', true)
// if the midpoint of the segment is on the right of the pie, set the
// text anchor to be at the start. If it is on the left, set the text anchor
// to the end.
.attr('text-anchor', d => {
d.midPt = (0.5 * (d.startAngle + d.endAngle))
return d.midPt < Math.PI ? 'start' : 'end'
} )
// to calculate the position of the label, I've taken the mid point of the
// start and end angles for the segment. I've then used d3.pointRadial to
// convert the angle (in radians) and the distance from the centre of
// the circle/pie (pie radius + labelOffset) into cartesian coordinates.
// d3.pointRadial returns [x, y] coordinates
.attr('x', d => d3.pointRadial( d.midPt, radius + labelOffset )[0] )
.attr('y', d => d3.pointRadial( d.midPt, radius + labelOffset )[1] )
// If the segment is in the upper half of the pie, move the text up a bit
// so that the label doesn't encroach on the pie itself
.attr('dy', d => {
let dy = 0.35;
if ( d.midPt < 0.5 * Math.PI || d.midPt > 1.5 * Math.PI ) {
dy -= 3.0;
}
return dy + 'em'
})
.text( d => {
return 'Run ' + d.data.key + ', experiments 1 - 6'
})
.call(wrap, 50)
// now we can get on to generating the sub segments within each main segment.
// add another g for each experiment
const expts = runs.selectAll('.expt')
// we already have the data bound to the DOM, but we want the d.children,
// which has the layout information from the pie chart generator
.data( d => d.children )
.enter()
.append('g')
.classed('expt', true)
// add the paths for each sub-segment
expts.append('path')
.classed('speed-segment', true)
.attr('d', arc)
// I simplified this slightly to use one of the built-in d3 colour schemes
// my data was already numeric so it was easy to use the run # as the colour
.attr('fill', (d,i) => {
const c = i / chunkSize,
color = d3.rgb( d3.schemeSet3[ d.data.Run - 1 ] );
return c < 1 ? color.brighter(c*0.5) : color;
})
// add a title element that appears when mousing over the segment
.append('title')
.text(d => 'Run ' + d.data.Run + ', experiment ' + d.data.Expt + ', speed: ' + d.data.Speed )
// add the lines
expts.append('line')
.attr('y2', radius)
// assign a class to each line so we can control the stroke, etc., using css
.attr('class', d => {
return 'run-' + d.data.Run + ' expt-' + d.data.Expt
})
// convert the angle from radians to degrees
.attr("transform", d => {
return "rotate(" + (180 + d.endAngle * 180 / Math.PI) + ")";
});
function wrap(text, width) {
text.each(function () {
let text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.2, // ems
tfrm = text.attr('transform')
y = text.attr("y"),
x = text.attr("x"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", ++lineNumber * lineHeight + dy + "em")
.text(word);
}
}
});
}
return svg;
})
}
chart('#chart');
功能图(id){
//这将读取CSV文件
d3.csv('morley3.csv')。然后(数据=>{
//这会将我正在使用的数据转换为更适合您的图表的形式
//我们有12次跑步,每次6次实验。
//每个基准的形式如下所示
//{运行:,导出:,速度:}
常量filteredData=数据
.filter(d=>d.Run<13)
.map(d=>{return{Run:+d.Run,Expt:+d.Expt,Speed:+d.Speed})
//设置图表
常数宽度=800,
高度=800,
半径=数学最小值(高度、宽度)*0.5-100,
//标签应该离图表多远
labelOffset=10,
svg=d3.选择(id).追加(“svg”)
.attr(“宽度”,宽度)
.attr(“高度”,高度),
g=svg.append(“g”)
.attr(“transform”、`translate(${width/2}、${height/2})`),
//这将用于生成饼图分段
弧=d3.arc()
.外部(半径)
.内半径(0),
//按运行编号对数据进行分组
//这将导致12组6个实验
//嵌套数据具有以下格式:
//[{key:,value:[{Run:1,Expt:1,Speed:958},{Run:1,Expt:2,Speed:869}…],
//{键:2,值:[{Run:2,Expt:1,Speed:987},{Run:2,Expt:2,Speed:809}…],
//等等。
nested=d3.nest()
.key(d=>+d.Run)
.条目(过滤数据),
chunkSize=nested[0].values.length,
//pie()是饼图生成器
pie=d3.pie()
//每个切片的大小将是每次运行的所有速度值之和
.value(d=>d3.sum(d.values,函数(e){返回e.Speed}))
//按运行排序#
.sort((a,b)=>a.key-b.key)
(嵌套)
//将数据绑定到DOM。为每次运行添加一个'g'
const runs=g.selectAll(“.run”)
.data(饼图,d=>d.key)
.输入()
.附加(“g”)
.classed('run',true)
.每个(d=>{
//对孩子们运行饼图生成器
//d.data.values是运行中的所有实验,或用饼图表示,
//在这块馅饼上做的所有实验,我们将使用
//'startAngle'和'endAngle'指定我们只生成
//“startAngle”和“endAngle”的值
//在运行数据上使用饼图生成器。
d、 children=d3.pie()
.值(e=>e.速度)
.sort((a,b)=>a.Expt-b.Expt)
.startAngle(d.startAngle)
.端角(d.端角)
(d.数据值)
})
//我们希望标记每个运行(而不是每个段),因此
//接下来将添加标签。
runs.append('text')
.classed('label',true)
//如果线段的中点位于饼图的右侧,请设置
//文本锚定位于起始位置。如果文本锚定位于左侧,请设置文本锚定
//到最后。
.attr('text-anchor',d=>{
d、 midPt=(0.5*(d.startAngle+d.endAngle))
返回d.midPt