【问题标题】:d3.js spreading labels for pie chartsd3.js 散布饼图的标签
【发布时间】:2014-03-23 07:04:51
【问题描述】:

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

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

这是标签的代码。我很好奇 - 是否可以用 d3 模拟 3d 饼图?

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

【问题讨论】:

  • 与您在此处所做的标记图表类似的问题 Lars - jsfiddle.net/Qh9X5/1234 - 我添加了更多数据以显示类似问题
  • 关于“3D 饼图”问题:需要大量额外的工作才能使阴影和一切都正确,而且所有这些都是为了一个非常难以阅读的图形。我不会推荐它!
  • 顺便说一句,我鼓励你阅读this help page article on creating simplified working examples。您的小提琴中有大量与标签位置无关的东西,这会让其他人难以处理。

标签: javascript charts d3.js pie-chart


【解决方案1】:

正如@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/

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

【讨论】:

    【解决方案2】:

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

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

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

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

    如果标签与先前的标签重叠,where 移动标签的决定是一个复杂的决定,正如@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/

    【讨论】:

    • 我已扩展此示例以提供更多数据 - jsfiddle.net/JTuej/16
    • 您使用的是什么浏览器,它给您带来了什么错误?第 235 行是 var conflicts = this.ownerSVGElement.getIntersectionList(bbox, this.parentNode); 这些都是标准接口属性/方法,但有些浏览器可能不尊重...
    • 它的 firefox 17.0.4 -- NS_ERROR_NOT_IMPLEMENTED: 组件返回失败代码: 0x80004001 (NS_ERROR_NOT_IMPLEMENTED) [nsIDOMSVGSVGElement.getIntersectionList] [Break On This Error] .getIntersectionList(bbox, this.parentNode);
    • 呃。是的。看起来 Firefox 还没有实现.getIntersectionList() 和相关方法:MDN articleBugzilla page。您必须跟踪所有标签位置并自己检查是否存在冲突。
    【解决方案3】:

    应该可以做到。你想怎么做取决于你想用标签间隔做什么。但是,没有内置的方法可以做到这一点。

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

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

    可以容忍多少重叠取决于您指定的能量函数,该能量函数应该是可调整的,以提供一个好看的点分布。同样,您愿意在点接近度上做出多少让步取决于与原始点的距离的能量增加函数的形状。 (线性能量增加会导致点更近,但离群值更大。二次或三次的平均距离更大,但离群值更小。)

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

    【讨论】:

    • 这确实是技术性的。您是否有一个基本示例可以遵循当前 jfiddle 代码的标签间距问题?还是静态饼图组成?我看过这个例子 - 正常放置标签 - 但如果它们看起来重叠然后替换它们 - jsfiddle.net/JTuej/9
    • @TheOldCounty:真希望你以前发现过这个例子!看起来和我的很相似。
    • 我是靠运气才发现的。我认为是拉斯的。虽然还是有重叠。
    猜你喜欢
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-27
    • 1970-01-01
    • 1970-01-01
    • 2014-08-17
    • 1970-01-01
    相关资源
    最近更新 更多