不同尺寸节点的碰撞检测未按预期工作

时间:2017-03-03 22:30:33

标签: javascript d3.js

我有一个功能齐全的d3.js力导向图。尝试添加冲突检测,以便节点不重叠。但需要注意的是,根据其 d.inDegree d.outDegree

计算出的半径变化的节点
    node.attr("r", function(d) {
    var weight = d.inDegree ? d.inDegree : 0 + d.outDegree ? d.outDegree : 0;
    weight = weight > 20 ? 20 : (weight < 5 ? 5 : weight);
    return weight;
  });

现在我试图在碰撞检测功能中使用这个变化的半径

    var padding = 1;
    var radius = function(d) { var weight = d.inDegree ? d.inDegree : 0 + d.outDegree ? d.outDegree : 0;
    weight = weight > 20 ? 20 : (weight < 5 ? 5 : weight);
    return weight;}
function collide(alpha) {
  var quadtree = d3.quadtree(d3GraphData.nodes);
  return function(e) {

    var rb = 2*radius + padding,
        nx1 = e.x - rb,
        nx2 = e.x + rb,
        ny1 = e.y - rb,
        ny2 = e.y + rb;
    quadtree.visit(function(quad, x1, y1, x2, y2) {
      if (quad.point && (quad.point !== e)) {
        var x = e.x - quad.point.x,
            y = e.y - quad.point.y,
            l = Math.sqrt(x * x + y * y);
          if (l < rb) {
          l = (l - rb) / l * alpha;
          e.x -= x *= l;
          e.y -= y *= l;
          quad.point.x += x;
          quad.point.y += y;
        }
      }
      return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
    });
  };
}

控制台上没有错误,但是当相互推入时节点仍然重叠。所以,我无法弄清楚将其归因于什么或如何调试它。

以下是fiddle

1 个答案:

答案 0 :(得分:5)

这是一个有趣的问题,每隔一段时间就会出现各种各样的问题。我记得最后一次回答其中一个是Gerardo Furtado的"Conflict between d3.forceCollide() and d3.forceX/Y() with high strength() value"。一开始可能很难掌握,但一旦完全沉入,几乎是显而易见的。所以请耐心等待,因为我会先用简单的话来说明这一点。

使用d3.forceSimulation()时,请务必牢记, 它的内在运作远不是自然力量的现实复制品。一个主要缺点是,力是顺序而不是同时施加的。甚至单个力的计算也将一次接一个地应用于一个节点,而不是同时应用于所有节点。这绝不是D3特有的问题,但任何计算机驱动的模拟面临的问题都必须找到解决方案。更糟糕的是,问题永远不会存在真正的解决方案,而是在计算的表现与其达到观众期望的程度之间进行权衡。

与自然相反,在我们的模拟中,约束很容易违反另一个约束或另一个约束的相同约束,而不会导致存在本质的湮灭。通常,与您习惯并因此期待的力量相比,这些结果可能会出乎意料。

这与您的特殊问题有什么关系?

进行碰撞检测时,您正在推动某些节点,以避免违反互斥约束。主要取决于算法的聪明程度,在尝试避开第三个节点时,存在将一个节点推入另一个节点的风险。同样,根据您进行计算的方式,在模拟的下一个滴答之前,可能无法解决此诱导违规。如果在碰撞检测之后计算的力将节点推到违反碰撞避免(或任何其他约束)的位置,则可能发生相同的情况。这甚至可能导致在其他节点上与其他力量或相同力量相关的一系列问题。

最常见的方法是在一个刻度内多次应用碰撞避免算法,从而迭代地接近真正的解决方案,希望如此。当我第一次遇到这种方法时,它看起来如此无助和可怜,我感到震惊,它应该是我们最好的镜头...但它不会坏。

由于您提供了自己的碰撞检测实现,即函数collide(),让我们首先尝试改进它。通过将整个计算重新组合成一个循环,例如十次,输出将显着改善(JSFiddle):

for (let i=10; i>0; i--) {   // approximation by iteration
  quadtree.visit(function(quad, x1, y1, x2, y2) {
      // heavy lifting omitted for brevity
  });
}

虽然效果非常好,但是如果没有像以前那样多,那么通知节点仍然会重叠。为了进一步改进这一点,我建议你放弃自己的实现,转而使用D3自己的collision detection算法,该算法可以d3.forceCollide()获得。该力的实现已经内置了上述迭代。您可以通过collide.iterations()设置迭代次数来控制迭代次数。除此之外,该实现还配备了一些更聪明的优化:

  

通过迭代松弛解决重叠节点。对于每个节点,确定预期在下一个时刻重叠的其他节点(使用预期位置⟨ x + vx y + vy ⟩);然后修改节点的速度以将节点推出每个重叠节点。速度的变化受到力的强度的抑制,使得同时重叠的分辨率可以混合在一起,以找到稳定的解决方案。

调整simulation可能如下所示(JSFiddle):

var simulation = d3.forceSimulation()
  .force("link",
    d3.forceLink()
      .id(function(d) { return d.id; })
      .distance(50)
      .strength(.5)            // weaken link strength
  )
  .force("charge", d3.forceManyBody())
  .force("center", d3.forceCenter(width / 2, height / 2))
  .force("gravity", gravity(0.25))
  .force("collide",
    d3.forceCollide()
      .strength(.9)            // strong collision avoidance
      .radius(radius)          // set your radius function
      .iterations(10)          // number of iterations per tick
  );

正如你所看到的,仍然存在重叠的节点,并且可能值得利用力的参数来产生一些令人愉悦的结果。鉴于大量的节点,它们相当大的圆圈以及你应用的力的数量,我不会指望它没有任何碰撞。