如何使用 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 可能是一个解决方案,但在玩了一会儿后发现仍然导致标签重叠:当窄类别恰好位于饼图的顶部或底部时。

我采用了该解决方案并对其进行了修改:

  1. 采用它来与 dc.js 饼图一起使用,
  2. 添加了重新计算步骤以确保标签不重叠。

这不是一个完美也不优雅的解决方案。在极端情况下,它会使线条相互交叉。不过,它现在对我有用。

重新计算函数如下:

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 制作。