如何使用 dc.js 避免饼图外部标签重叠
How to avoid pie chart external labels overlapping using dc.js
.width(400)
.height(200)
.externalLabels(20)
.externalRadiusPadding(5)
.drawPaths(true)
.dimension(genderDimension)
这是我的代码,但外部标签在饼图中有一小部分的地方重叠。
有什么理由提前解决这个problem.Thanks
处理此问题的唯一内置方法是使用 pieChart.minAngleForLabel:
Get or set the minimal slice angle for label rendering. Any slice with a smaller angle will not display a slice label.
它只是删除窄切片的标签。角度以弧度指定。
目前没有尝试避免标签之间的冲突。可以想象弯曲外部标签上的线条,或者使它们变长或变短,但我不知道您将使用什么算法来做到这一点。
我实际上有一些数据有几个狭窄的切片,并且有 2 个,有时是 3 个标签重叠。起初我认为: http://bl.ocks.org/dbuezas/9572040 可能是一个解决方案,但在玩了一会儿后发现仍然导致标签重叠:当窄类别恰好位于饼图的顶部或底部时。
我采用了该解决方案并对其进行了修改:
- 采用它来与 dc.js 饼图一起使用,
- 添加了重新计算步骤以确保标签不重叠。
这不是一个完美也不优雅的解决方案。在极端情况下,它会使线条相互交叉。不过,它现在对我有用。
重新计算函数如下:
function recalcLabels(chart) {
var centroids = [];
var labelYsLeft = [];
var labelYsRight = [];
var radius = chart.radius()*1.25;
// Internal recalculating function
function adjustLabel(xy, offset=15) {
var x = xy[0];
var y = xy[1];
if (x>0) { // right side
if (labelYsRight.find(elem => ((y+offset)>elem && ((y-offset)<elem)))) {
if (y>0) { // Bottom
x = x * Math.abs((y-offset)/y);
[x,y] = adjustLabel([x, y-offset-2], offset);
}
if (y<0) { // Top
x = x * Math.abs((y+offset)/y);
[x,y] = adjustLabel([x, y+offset+2], offset);
}
}
labelYsRight.push(y);
} else { // Left side
if (labelYsLeft.find(elem => ((y+offset)>elem && ((y-offset)<elem)))) {
if (y>0) { // Bottom
x = x * Math.abs((y-offset)/y);
[x,y] = adjustLabel([x, y-offset-2], offset);
}
if (y<0) { // Top
x = x * Math.abs((y+offset)/y);
[x,y] = adjustLabel([x, y+offset+2], offset);
}
}
labelYsLeft.push(y);
}
return [x,y];
}
var pie = d3.pie().sort(null).value(function(d){ return d.value; });
var chartData = pie(chart.data());
var arc = d3.arc()
.innerRadius(radius*0.5)
.outerRadius(radius*0.8);
var outerArc = d3.arc()
.innerRadius(radius*0.9)
.outerRadius(radius*0.9)
chartData.forEach(function(d, i){
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = [...posB];
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
var temp = [posA, posB, posC, midangle, d.data.key, i];
if (temp[1][0]<0 && temp[1][1]<0) {
centroids.unshift(temp);
} else {
centroids.push(temp);
}
});
var adjusted = new Array(centroids.length);
centroids.forEach(function(d) {
var [posA, posB, posC, midangle, key, i] = d
posB = adjustLabel(d[1]);
posC[1] = posB[1];
adjusted[i] = [posA, posB, posC, midangle, key];
});
return [adjusted, chartData];
}
下面是我的使用方法:
var pc = dc.pieChart("#my-chart").width(someWidth).height(someHeight)
.ordering(dc.pluck(function(d){ return d.key; }))
.radius(80)
.innerRadius(40)
.colors(someColors)
.dimension(someDim)
.group(someGrp);
pc.on('pretransition', function(chart) {
var [centroids, labels] = recalcLabels(chart);
chart.svg().select("g").select("g.pie-label-group").selectAll('polyline')
.data(labels)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d, i) {
if (d.value>0) { return [centroids[i][0], centroids[i][1], centroids[i][2]]; }
});
chart.selectAll('text.pie-slice').transition().duration(chart.transitionDuration())
.text( function(d,i) { if (d.value>0) { return centroids[i][4]; }} )
.attr('transform', function(d, i) {
var pos = centroids[i][2];
pos[0] = chart.radius() * 1.25 * (centroids[i][3] < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d,i) {
return (centroids[i][3] < Math.PI ? 'start' : 'end')
})
});
pc.on('preRedraw', function(chart) {
chart.selectAll('polyline').remove();
});
快速解释。如您所见,我调用预转换的第一件事是重新计算标签位置。然后将标签和线条手动添加到图表中(不使用内置 dc.js 函数)。在图表更新时绘制新线之前,我使用 preRedraw 删除旧线 - 这会破坏动画:使线“跳跃” - 不像 bl.ocks 示例中那样好,但它对我有用。
重新计算已完成,以便饼图顶部的标签在重叠时向下推,而下部的标签则向上推。必须以相反的顺序重新计算图表的左上角,否则线条会交叉。
它生成这样的图表:
使用 dc.js v4.2.7 和 d3.js v6.7.0 制作。
.width(400)
.height(200)
.externalLabels(20)
.externalRadiusPadding(5)
.drawPaths(true)
.dimension(genderDimension)
这是我的代码,但外部标签在饼图中有一小部分的地方重叠。 有什么理由提前解决这个problem.Thanks
处理此问题的唯一内置方法是使用 pieChart.minAngleForLabel:
Get or set the minimal slice angle for label rendering. Any slice with a smaller angle will not display a slice label.
它只是删除窄切片的标签。角度以弧度指定。
目前没有尝试避免标签之间的冲突。可以想象弯曲外部标签上的线条,或者使它们变长或变短,但我不知道您将使用什么算法来做到这一点。
我实际上有一些数据有几个狭窄的切片,并且有 2 个,有时是 3 个标签重叠。起初我认为: http://bl.ocks.org/dbuezas/9572040 可能是一个解决方案,但在玩了一会儿后发现仍然导致标签重叠:当窄类别恰好位于饼图的顶部或底部时。
我采用了该解决方案并对其进行了修改:
- 采用它来与 dc.js 饼图一起使用,
- 添加了重新计算步骤以确保标签不重叠。
这不是一个完美也不优雅的解决方案。在极端情况下,它会使线条相互交叉。不过,它现在对我有用。
重新计算函数如下:
function recalcLabels(chart) {
var centroids = [];
var labelYsLeft = [];
var labelYsRight = [];
var radius = chart.radius()*1.25;
// Internal recalculating function
function adjustLabel(xy, offset=15) {
var x = xy[0];
var y = xy[1];
if (x>0) { // right side
if (labelYsRight.find(elem => ((y+offset)>elem && ((y-offset)<elem)))) {
if (y>0) { // Bottom
x = x * Math.abs((y-offset)/y);
[x,y] = adjustLabel([x, y-offset-2], offset);
}
if (y<0) { // Top
x = x * Math.abs((y+offset)/y);
[x,y] = adjustLabel([x, y+offset+2], offset);
}
}
labelYsRight.push(y);
} else { // Left side
if (labelYsLeft.find(elem => ((y+offset)>elem && ((y-offset)<elem)))) {
if (y>0) { // Bottom
x = x * Math.abs((y-offset)/y);
[x,y] = adjustLabel([x, y-offset-2], offset);
}
if (y<0) { // Top
x = x * Math.abs((y+offset)/y);
[x,y] = adjustLabel([x, y+offset+2], offset);
}
}
labelYsLeft.push(y);
}
return [x,y];
}
var pie = d3.pie().sort(null).value(function(d){ return d.value; });
var chartData = pie(chart.data());
var arc = d3.arc()
.innerRadius(radius*0.5)
.outerRadius(radius*0.8);
var outerArc = d3.arc()
.innerRadius(radius*0.9)
.outerRadius(radius*0.9)
chartData.forEach(function(d, i){
var posA = arc.centroid(d) // line insertion in the slice
var posB = outerArc.centroid(d) // line break: we use the other arc generator that has been built only for that
var posC = [...posB];
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2 // we need the angle to see if the X position will be at the extreme right or extreme left
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1); // multiply by 1 or -1 to put it on the right or on the left
var temp = [posA, posB, posC, midangle, d.data.key, i];
if (temp[1][0]<0 && temp[1][1]<0) {
centroids.unshift(temp);
} else {
centroids.push(temp);
}
});
var adjusted = new Array(centroids.length);
centroids.forEach(function(d) {
var [posA, posB, posC, midangle, key, i] = d
posB = adjustLabel(d[1]);
posC[1] = posB[1];
adjusted[i] = [posA, posB, posC, midangle, key];
});
return [adjusted, chartData];
}
下面是我的使用方法:
var pc = dc.pieChart("#my-chart").width(someWidth).height(someHeight)
.ordering(dc.pluck(function(d){ return d.key; }))
.radius(80)
.innerRadius(40)
.colors(someColors)
.dimension(someDim)
.group(someGrp);
pc.on('pretransition', function(chart) {
var [centroids, labels] = recalcLabels(chart);
chart.svg().select("g").select("g.pie-label-group").selectAll('polyline')
.data(labels)
.enter()
.append('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d, i) {
if (d.value>0) { return [centroids[i][0], centroids[i][1], centroids[i][2]]; }
});
chart.selectAll('text.pie-slice').transition().duration(chart.transitionDuration())
.text( function(d,i) { if (d.value>0) { return centroids[i][4]; }} )
.attr('transform', function(d, i) {
var pos = centroids[i][2];
pos[0] = chart.radius() * 1.25 * (centroids[i][3] < Math.PI ? 1 : -1);
return 'translate(' + pos + ')';
})
.style('text-anchor', function(d,i) {
return (centroids[i][3] < Math.PI ? 'start' : 'end')
})
});
pc.on('preRedraw', function(chart) {
chart.selectAll('polyline').remove();
});
快速解释。如您所见,我调用预转换的第一件事是重新计算标签位置。然后将标签和线条手动添加到图表中(不使用内置 dc.js 函数)。在图表更新时绘制新线之前,我使用 preRedraw 删除旧线 - 这会破坏动画:使线“跳跃” - 不像 bl.ocks 示例中那样好,但它对我有用。
重新计算已完成,以便饼图顶部的标签在重叠时向下推,而下部的标签则向上推。必须以相反的顺序重新计算图表的左上角,否则线条会交叉。
它生成这样的图表:
使用 dc.js v4.2.7 和 d3.js v6.7.0 制作。