简体   繁体   English

d3.js传播饼图的标签

[英]d3.js spreading labels for pie charts

I'm using d3.js - I have a pie chart here. 我正在使用d3.js - 我这里有一个饼图。 The problem though is when the slices are small - the labels overlap. 但问题是切片很小时 - 标签重叠。 What is the best way of spreading out the labels. 展开标签的最佳方式是什么?

重叠标签

一个3d馅饼

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

Here is the code for the labels. 这是标签的代码。 I am curious - is it possible to mock a 3d pie chart with d3? 我很好奇 - 是否有可能用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();

As @The Old County discovered, the previous answer I posted fails in firefox because it relies on the SVG method .getIntersectionList() to find conflicts, and that method hasn't been implemented yet in Firefox . 正如@The Old County发现的那样,我之前发布的答案在firefox中失败,因为它依赖于SVG方法.getIntersectionList()来查找冲突,并且该方法尚未在Firefox中实现

That just means we have to keep track of label positions and test for conflicts ourselves. 这只意味着我们必须跟踪标签位置并自己测试冲突。 With d3, the most efficient way to check for layout conflicts involves using a quadtree data structure to store positions, that way you don't have to check every label for overlap, just those in a similar area of the visualization. 使用d3,检查布局冲突的最有效方法是使用四叉树数据结构来存储位置,这样您就不必检查每个标签的重叠,只需检查可视化的类似区域。

The second part of the code from the previous answer gets replaced with: 上一个答案的代码的第二部分将替换为:

        /* 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);

Note that I've changed the parameter names for the edges of the label to l/r/b/t (left/right/bottom/top) to keep everything logical in my mind. 请注意,我已将标签边缘的参数名称更改为l / r / b / t(左/右/底部/顶部),以保持一切符合逻辑。

Live fiddle here: http://jsfiddle.net/Qh9X5/1249/ 现场小提琴: http//jsfiddle.net/Qh9X5/1249/

An added benefit of doing it this way is that you can check for conflicts based on the final position of the labels, before actually setting the position. 这样做的另一个好处是,您可以在实际设置位置之前根据标签的最终位置检查冲突。 Which means that you can use transitions for moving the labels into position after figuring out the positions for all the labels. 这意味着您可以在确定所有标签的位置后使用过渡将标签移动到位。

Should be possible to do. 应该可以做到。 How exactly you want to do it will depend on what you want to do with spacing out the labels. 你究竟想要做什么取决于你想要做什么来隔开标签。 There is not, however, a built in way of doing this. 但是,没有一种内置的方法可以做到这一点。

The main problem with the labels is that, in your example, they rely on the same data for positioning that you are using for the slices of your pie chart. 标签的主要问题在于,在您的示例中,它们依赖于您用于饼图切片的相同数据进行定位。 If you want them to space out more like excel does (ie give them room), you'll have to get creative. 如果你想让他们更像excel那样(即给他们空间),你必须要有创意。 The information you have is their starting position, their height, and their width. 您拥有的信息是他们的起始位置,高度和宽度。

A really fun (my definition of fun) way to go about solving this would be to create a stochastic solver for an optimal arrangement of labels. 一个非常有趣的(我的乐趣定义)解决这个问题的方法是创建一个随机求解器来优化标签排列。 You could do this with an energy-based method. 你可以用基于能量的方法做到这一点。 Define an energy function where energy increases based on two criteria: distance from start point and overlap with nearby labels. 定义能量函数,其中能量基于两个标准增加:距起点的距离和与附近标签的重叠。 You can do simple gradient descent based on that energy criteria to find a locally optimal solution with regards to your total energy, which would result in your labels being as close as possible to their original points without a significant amount of overlap, and without pushing more points away from their original points. 您可以根据能量标准进行简单的梯度下降,找到与您的总能量相关的局部最优解,这将导致您的标签尽可能接近原始点而不会出现大量重叠,并且无需推动更多远离原点。

How much overlap is tolerable would depend on the energy function you specify, which should be tunable to give a good looking distribution of points. 可以容忍多少重叠取决于您指定的能量函数,该函数应该是可调的,以提供良好的点分布。 Similarly, how much you're willing to budge on point closeness would depend on the shape of your energy increase function for distance from the original point. 同样地,你愿意在点接近度上做多少取决于能量增加函数的形状与原点的距离。 (A linear energy increase will result in closer points, but greater outliers. A quadratic or a cubic will have greater average distance, but smaller outliers.) (线性能量增加将导致更接近的点,但更大的异常值。二次或三次将具有更大的平均距离,但更小的异常值。)

There might also be an analytical way of solving for the minima, but that would be harder. 可能还有一种解决最小值的分析方法,但这会更难。 You could probably develop a heuristic for positioning things, which is probably what excel does, but that would be less fun. 你可能可以开发一种用于定位事物的启发式算法,这可能就是excel所做的,但这样就不那么有趣了。

One way to check for conflicts is to use the <svg> element's getIntersectionList() method. 检查冲突的一种方法是使用<svg>元素的getIntersectionList()方法。 That method requires you to pass in an SVGRect object (which is different from a <rect> element!), such as the object returned by a graphical element's .getBBox() method. 该方法要求您传入一个SVGRect对象(与<rect>元素不同!),例如图形元素的.getBBox()方法返回的对象。

With those two methods, you can figure out where a label is within the screen and if it overlaps anything. 使用这两种方法,您可以确定标签在屏幕中的位置以及它是否与任何内容重叠。 However, one complication is that the rectangle coordinates passed to getIntersectionList are interpretted within the root SVG's coordinates, while the coordinates returned by getBBox are in the local coordinate system. 然而,一个复杂因素是传递给getIntersectionList的矩形坐标在根SVG的坐标内被解释,而getBBox返回的坐标在本地坐标系中。 So you also need the method getCTM() (get cumulative transformation matrix) to convert between the two. 所以你还需要方法getCTM() (得到累积变换矩阵)来在两者之间进行转换。

I started with the example from Lars Khottof that @TheOldCounty had posted in a comment, as it already included lines between the arc segments and the labels. 从Lars Khottof的例子开始, @ TheOldCounty已在评论中发布,因为它已包括弧段和标签之间的线。 I did a little re-organization to put the labels, lines and arc segments in separate <g> elements. 我做了一点重新组织,将标签,线条和弧段放在单独的<g>元素中。 That avoids strange overlaps (arcs drawn on top of pointer lines) on update, and it also makes it easy to define which elements we're worried about overlapping -- other labels only, not the pointer lines or arcs -- by passing the parent <g> element as the second parameter to getIntersectionList . 这避免了奇怪的重叠上更新(对指针线上面描绘的圆弧),也可以很容易地确定哪些元素,我们很担心重叠-唯一的其他标签,而非指针直线或圆弧-通过将母<g>元素作为getIntersectionList的第二个参数。

The labels are positioned one at a time using an each function, and they have to be actually positioned (ie, the attribute set to its final value, no transitions) at the time the position is calculated, so that they are in place when getIntersectionList is called for the next label's default position. 使用each函数一次一个地定位标签,并​​且在计算位置时它们必须实际定位(即,属性设置为其最终值,没有过渡),以便在getIntersectionList时它们就位。调用下一个标签的默认位置。

The decision of where to move a label if it overlaps a previous label is a complex one, as @ckersch's answer outlines. 如果它重叠以前的标签,其中移动标签的决定是一个复杂的,如@ ckersch的答案概括。 I keep it simple and just move it to the right of all the overlapped elements. 我保持简单,只需将其移动到所有重叠元素的右侧。 This could cause a problem at the top of the pie, where labels from the last segments could be moved so that they overlap labels from the first segments, but that's unlikely if the pie chart is sorted by segment size. 这可能会导致饼图顶部出现问题,其中最后一个片段的标签可以移动,以便它们与第一个片段的标签重叠,但如果饼图按片段大小排序则不太可能。

Here's the key code: 这是关键代码:

    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;
            });
    });

And here's the working fiddle: http://jsfiddle.net/Qh9X5/1237/ 这是工作小提琴: http//jsfiddle.net/Qh9X5/1237/

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM