d3.js传播饼图的标签

时间:2014-02-21 16:35:33

标签: javascript charts d3.js pie-chart

我正在使用d3.js - 我这里有一个饼图。但问题是切片很小时 - 标签重叠。什么是展开标签的最佳方式。

Overlapped labels

a 3d pie

http://jsfiddle.net/BxLHd/16/

以下是标签的代码。我很好奇 - 是否可以用d3模拟一个三维饼图?

                        //draw labels                       
                        valueLabels = label_group.selectAll("text.value").data(filteredData)
                        valueLabels.enter().append("svg:text")
                                .attr("class", "value")
                                .attr("transform", function(d) {
                                    return "translate(" + Math.cos(((d.startAngle+d.endAngle - Math.PI)/2)) * (that.r + that.textOffset) + "," + Math.sin((d.startAngle+d.endAngle - Math.PI)/2) * (that.r + that.textOffset) + ")";
                                })
                                .attr("dy", function(d){
                                        if ((d.startAngle+d.endAngle)/2 > Math.PI/2 && (d.startAngle+d.endAngle)/2 < Math.PI*1.5 ) {
                                                return 5;
                                        } else {
                                                return -7;
                                        }
                                })
                                .attr("text-anchor", function(d){
                                        if ( (d.startAngle+d.endAngle)/2 < Math.PI ){
                                                return "beginning";
                                        } else {
                                                return "end";
                                        }
                                }).text(function(d){
                                        //if value is greater than threshold show percentage
                                        if(d.value > threshold){
                                            var percentage = (d.value/that.totalOctets)*100;
                                            return percentage.toFixed(2)+"%";
                                        }
                                });

                        valueLabels.transition().duration(this.tweenDuration).attrTween("transform", this.textTween);
                        valueLabels.exit().remove();

3 个答案:

答案 0 :(得分:6)

正如@The Old County发现的那样,我发布的上一个答案在firefox中失败,因为它依赖于SVG方法.getIntersectionList()来查找冲突,并且该方法hasn't been implemented yet in Firefox

这意味着我们必须跟踪标签位置并自行测试冲突。使用d3,检查布局冲突的最有效方法是使用quadtree data structure来存储位置,这样您就不必检查每个标签是否有重叠,只有那些类似于可视化领域。

上一个答案的代码的第二部分将替换为:

        /* check whether the default position 
           overlaps any other labels*/
        var conflicts = [];
        labelLayout.visit(function(node, x1, y1, x2, y2){
            //recurse down the tree, adding any overlapping labels
            //to the conflicts array

            //node is the node in the quadtree, 
            //node.point is the value that we added to the tree
            //x1,y1,x2,y2 are the bounds of the rectangle that
            //this node covers

            if (  (x1 > d.r + maxLabelWidth/2) 
                    //left edge of node is to the right of right edge of label
                ||(x2 < d.l - maxLabelWidth/2) 
                    //right edge of node is to the left of left edge of label
                ||(y1 > d.b + maxLabelHeight/2)
                    //top (minY) edge of node is greater than the bottom of label
                ||(y2 < d.t - maxLabelHeight/2 ) )
                    //bottom (maxY) edge of node is less than the top of label

                  return true; //don't bother visiting children or checking this node

            var p = node.point;
            var v = false, h = false;
            if ( p ) { //p is defined, i.e., there is a value stored in this node
                h =  ( ((p.l > d.l) && (p.l <= d.r))
                   || ((p.r > d.l) && (p.r <= d.r)) 
                   || ((p.l < d.l)&&(p.r >=d.r) ) ); //horizontal conflict

                v =  ( ((p.t > d.t) && (p.t <= d.b))
                   || ((p.b > d.t) && (p.b <= d.b))  
                   || ((p.t < d.t)&&(p.b >=d.b) ) ); //vertical conflict

                if (h&&v)
                    conflicts.push(p); //add to conflict list
            }

        });

        if (conflicts.length) {
            console.log(d, " conflicts with ", conflicts);  
            var rightEdge = d3.max(conflicts, function(d2) {
                return d2.r;
            });

            d.l = rightEdge;
            d.x = d.l + bbox.width / 2 + 5;
            d.r = d.l + bbox.width + 10;
        }
        else console.log("no conflicts for ", d);

        /* add this label to the quadtree, so it will show up as a conflict
           for future labels.  */
        labelLayout.add( d );
        var maxLabelWidth = Math.max(maxLabelWidth, bbox.width+10);
        var maxLabelHeight = Math.max(maxLabelHeight, bbox.height+10);

请注意,我已将标签边缘的参数名称更改为l / r / b / t(左/右/底部/顶部),以保持一切符合我的想法。

在这里小提琴:http://jsfiddle.net/Qh9X5/1249/

这样做的另一个好处是,您可以在实际设置位置之前根据标签的最终位置检查冲突。这意味着您可以在确定所有标签的位置后使用过渡将标签移动到位。

答案 1 :(得分:2)

应该可以做到。你究竟想要做什么取决于你想要做什么来隔开标签。但是,没有一种内置的方法可以做到这一点。

标签的主要问题在于,在您的示例中,它们依赖于您用于饼图切片的相同数据进行定位。如果你想让他们更像excel一样(即给他们空间),你必须要有创意。您拥有的信息是他们的起始位置,高度和宽度。

一个非常有趣的(我的乐趣定义)解决这个问题的方法是创建一个随机求解器来优化标签排列。你可以用基于能量的方法做到这一点。定义能量函数,其中能量基于两个标准增加:距起点的距离和与附近标签的重叠。您可以根据能量标准进行简单的梯度下降,找到与您的总能量相关的局部最优解,这将导致您的标签尽可能接近原始点而不会出现大量重叠,并且无需推动更多远离原点。

可以容忍多少重叠取决于您指定的能量函数,该函数应该是可调的,以提供良好的点分布。同样地,你愿意在点接近度上做多少取决于能量增加函数的形状与原点的距离。 (线性能量增加将导致更接近的点,但更大的异常值。二次或三次将具有更大的平均距离,但更小的异常值。)

可能还有一种解决最小值的分析方法,但这会更难。你可能可以开发一种用于定位事物的启发式算法,这可能就是excel所做的,但这样就不那么有趣了。

答案 2 :(得分:0)

检查冲突的一种方法是使用<svg>元素的getIntersectionList()方法。该方法要求您传入SVGRect对象(与<rect>元素不同!),例如图形元素的.getBBox()方法返回的对象。

使用这两种方法,您可以确定标签在屏幕内的位置以及它是否与任何内容重叠。然而,一个复杂因素是传递给getIntersectionList的矩形坐标在根SVG的坐标内被解释,而getBBox返回的坐标在本地坐标系中。所以你还需要方法getCTM()(得到累积变换矩阵)来在两者之间进行转换。

我从the example from Lars Khottof开始,@ TheOldCounty已在评论中发布,因为它已包含弧段和标签之间的线条。我做了一点重新组织,将标签,线条和弧段放在单独的<g>元素中。这样可以避免在更新时出现奇怪的重叠(在指针线上绘制弧线),并且还可以轻松定义我们担心重叠的元素 - 仅限其他标签,而不是指针线或arcs - 将父<g>元素作为第二个参数传递给getIntersectionList

使用each函数一次定位一个标签,并且在计算位置时必须将它们实际定位(即,属性设置为其最终值,无转换),因此当getIntersectionList被调用到下一个标签的默认位置时,它们就位。

决定移动标签(如果它与前一个标签重叠)是一个复杂的决定,因为@ ckersch的答案概述。我保持简单,只需将其移动到所有重叠元素的右侧。这可能会导致饼图顶部出现问题,其中最后一个片段的标签可以移动,以便它们与第一个片段的标签重叠,但如果饼图按片段大小排序则不太可能。

以下是关键代码:

    labels.text(function (d) {
        // Set the text *first*, so we can query the size
        // of the label with .getBBox()
        return d.value;
    })
    .each(function (d, i) {
        // Move all calculations into the each function.
        // Position values are stored in the data object 
        // so can be accessed later when drawing the line

        /* calculate the position of the center marker */
        var a = (d.startAngle + d.endAngle) / 2 ;

        //trig functions adjusted to use the angle relative
        //to the "12 o'clock" vector:
        d.cx = Math.sin(a) * (that.radius - 75);
        d.cy = -Math.cos(a) * (that.radius - 75);

        /* calculate the default position for the label,
           so that the middle of the label is centered in the arc*/
        var bbox = this.getBBox();
        //bbox.width and bbox.height will 
        //describe the size of the label text
        var labelRadius = that.radius - 20;
        d.x =  Math.sin(a) * (labelRadius);
        d.sx = d.x - bbox.width / 2 - 2;
        d.ox = d.x + bbox.width / 2 + 2;
        d.y = -Math.cos(a) * (that.radius - 20);
        d.sy = d.oy = d.y + 5;

        /* check whether the default position 
           overlaps any other labels*/

        //adjust the bbox according to the default position
        //AND the transform in effect
        var matrix = this.getCTM();
        bbox.x = d.x + matrix.e;
        bbox.y = d.y + matrix.f;

        var conflicts = this.ownerSVGElement
                            .getIntersectionList(bbox, this.parentNode);

        /* clear conflicts */
        if (conflicts.length) {
            console.log("Conflict for ", d.data, conflicts);   
            var maxX = d3.max(conflicts, function(node) {
                var bb = node.getBBox();
                return bb.x + bb.width;
            })

            d.x = maxX + 13;
            d.sx = d.x - bbox.width / 2 - 2;
            d.ox = d.x + bbox.width / 2 + 2;

        }

        /* position this label, so it will show up as a conflict
           for future labels. (Unfortunately, you can't use transitions.) */
        d3.select(this)
            .attr("x", function (d) {
                return d.x;
            })
            .attr("y", function (d) {
                return d.y;
            });
    });

这是工作小提琴:http://jsfiddle.net/Qh9X5/1237/