如果有足够的空间,D3可以将弧形标签放在饼图中。

9
我会在饼图的每个弧中放置一个文本元素,就像这个例子所示:http://bl.ocks.org/mbostock/3887235。但是只有在整个文本能够适应空间时才会加入文本元素,因此我必须将我的文本元素的尺寸与每个弧中“可用”空间的尺寸进行比较。我认为我可以使用 getBBox() 来获取文本维度......但是如何获取(并比较)每个弧中可用空间的维度呢?谢谢!
2个回答

39

这个问题以前已经 问过多次了。

我之前提出的解决方案是旋转标签,但是它从来没有完全满足我。部分原因是一些浏览器做的可怕的字体渲染和失去清晰度带来的可读性损失,以及当一个标签跨过180°线时的奇怪翻转。在某些情况下,结果是可以接受且不可避免的,例如当标签太长时。

另一个解决方案是由Lars建议的,即将标签放在饼图外部。然而,这只是将标签推向外部,使它们拥有更大的半径,但并不能完全解决重叠的问题。

另一种解决方案实际上就是使用您提出的技术:仅删除不适合的标签。

隐藏溢出的标签

比较 原始版本,其中标签>= 65溢出到了解决方案,而在解决方案中,溢出的标签已经消失。

简化问题

关键是要看到这个问题是找出一个凸多边形(矩形,边界框)是否被包含在另一个凸多边形(近似)(楔形)中。

问题可以简化为找出矩形的所有点是否都位于楔形内部。如果是,则矩形位于弧线内部。

一个点是否位于楔形内部

现在这部分很容易。只需要检查:

  1. 该点到中心的距离小于半径radius
  2. 弧上该点所对应的角度在startAngleendAngle之间。
function pointIsInArc(pt, ptData, d3Arc) {
  // Center of the arc is assumed to be 0,0
  // (pt.x, pt.y) are assumed to be relative to the center
  var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
      r2 = d3Arc.outerRadius()(ptData),
      theta1 = d3Arc.startAngle()(ptData),
      theta2 = d3Arc.endAngle()(ptData);

  var dist = pt.x * pt.x + pt.y * pt.y,
      angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system.

  angle = (angle < 0) ? (angle + Math.PI * 2) : angle;

  return (r1 * r1 <= dist) && (dist <= r2 * r2) && 
         (theta1 <= angle) && (angle <= theta2);
}

找到标签的边界框

既然我们已经完成了第一步,第二步就是确定矩形的四个角落。这也很容易:

g.append("text")
    .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
    .attr("dy", ".35em")
    .style("text-anchor", "middle")
    .text(function(d) { return d.data.age; })
    .each(function (d) {
       var bb = this.getBBox(),
           center = arc.centroid(d);

       var topLeft = {
         x : center[0] + bb.x,
         y : center[1] + bb.y
       };

       var topRight = {
         x : topLeft.x + bb.width,
         y : topLeft.y
       };

       var bottomLeft = {
         x : topLeft.x,
         y : topLeft.y + bb.height
       };

       var bottomRight = {
         x : topLeft.x + bb.width,
         y : topLeft.y + bb.height
       };

       d.visible = pointIsInArc(topLeft, d, arc) &&
                   pointIsInArc(topRight, d, arc) &&
                   pointIsInArc(bottomLeft, d, arc) &&
                   pointIsInArc(bottomRight, d, arc);

    })
    .style('display', function (d) { return d.visible ? null : "none"; });

解决方案的精髓在于each函数。我们首先将文本放置在正确的位置,以便DOM呈现它。然后我们使用getBBox()方法获取text用户空间中的边界框。 任何具有设置了transform属性的元素都会创建一个新的用户空间。在我们的例子中,该元素是文本框本身。因此返回的边界框相对于文本的中心,因为我们将text-anchor设置为middle

由于我们对其应用了变换'translate(' + arc.centroid(d) + ')',因此可以计算出text相对于arc的位置。一旦我们有了中心点,我们只需从中心点计算出topLefttopRightbottomLeftbottomRight点,并查看它们是否都位于wedge内。

最后,我们确定所有点是否都位于wedge内,如果不符合,则将display CSS属性设置为none

工作演示

原始版本

解决方案

注意事项

  1. 我正在使用innerRadius。如果非零,将使wedge非凸,这将使计算变得更加复杂!然而,我认为这里的危险不是很大,因为它可能失败的唯一情况是这种情况,而且,坦白地说,我不认为它经常会发生(我在寻找这个反例时遇到了麻烦):

    The failure case

  2. xy被颠倒,并且在计算Math.atan2y带有负号。这是因为Math.atan2d3.svg.arc视坐标系和svgy方向的差异。

    Math.atan2的坐标系

    Coordinate system for <code>Math.atan2</code> θ = Math.atan2(y, x) = Math.atan2(-svg.y, x)

    d3.svg.arc的坐标系

    Coordinate system for <code>d3.svg.arc</code> θ = Math.atan2(x, y) = Math.atan2(x, -svg.y)


2
您无法使用边界框来实现此目的,因为边界框比饼图楔形大得多。也就是说,即使外边缘处的楔形足够宽以容纳文本,这并不意味着在文本的实际位置它就足够宽了。
不幸的是,没有简单的方法可以实现您正在尝试的像素级重叠测试。请参见例如此问题以获取更多信息。我建议将文本标签放在饼图外部,以避免出现此问题。

+1 对于这个解决方案。我们采用这种方法。我认为微软 Excel 也是这样做的。 - Mike Rifgin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接