d3js力导向图中链接上的箭头

13

我正在使用力导向布局来表示一个有向无权网络。我的灵感来自以下示例: http://bl.ocks.org/mbostock/1153292

这里输入图片描述

我尝试让节点大小不同,但是遇到了一个小问题。用于绘制每个链接箭头的标记指向圆圈的中心。如果圆太大,则完全覆盖箭头。

我该如何解决这个问题?

5个回答

11

如果您使用<line>而不是<path>,以下内容应该适用于您,我已经在我的当前解决方案中使用了它。它基于@ɭɘ ɖɵʊɒɼɖ江戸的解决方案:

在您的tick事件监听器中:

linkElements.attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { 
             return getTargetNodeCircumferencePoint(d)[0];
        })
        .attr("y2", function(d) { 
             return getTargetNodeCircumferencePoint(d)[1];
        });

function getTargetNodeCircumferencePoint(d){

        var t_radius = d.target.nodeWidth/2; // nodeWidth is just a custom attribute I calculate during the creation of the nodes depending on the node width
        var dx = d.target.x - d.source.x;
        var dy = d.target.y - d.source.y;
        var gamma = Math.atan2(dy,dx); // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
        var tx = d.target.x - (Math.cos(gamma) * t_radius);
        var ty = d.target.y - (Math.sin(gamma) * t_radius);

        return [tx,ty]; 
}

我相信这个解决方案可以修改以适应<path>元素,但是我还没有尝试过。


对我来说效果很好,将链接移至圆圈外部。甚至在乘法中加入t_radius+2,使链接在圆的边缘之前结束-这样可以更好地移动标记refX,以便stroke-width不会干扰箭头边缘。 - Evgeny

7
您可以通过节点的半径来偏移链接的目标,即调整代码。
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
    dy = d.target.y - d.source.y,
    dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});

通过改变 d.target.xd.target.y 的值来考虑半径(需要成为数据的一部分,例如 d.target.radius)。也就是说,通过圆的半径来偏移箭头的末端。

我明白你的意思,但我无法确定应该在哪个点引入半径。 - riccardo.tasso
我猜移动箭头的末端并不容易,因为结果还取决于它来自哪个方向。也许我应该处理标记。 - riccardo.tasso
可能正确的更改将影响:...append("svg:marker") .attr("refX", 15).attr("refY", -1.5) - riccardo.tasso
当你计算dx和dy时,圆的半径会涉及其中--你需要根据箭头来自哪里来减去或加上半径。 - Lars Kotthoff
你对第二个答案有什么看法? - riccardo.tasso
你肯定可以这样做 - 这等同于同一件事情。无论你更喜欢哪种方式。 - Lars Kotthoff

3
有点迟来的回答,但综合之前所有的回答,我想出了一种适用于我在d3v4中使用的全面解决方案,使用TypeScript编写,因为Angular(以防你发现缺乏全局变量很奇怪)。以下是一个片段,包含需要包含的关键组件(因为我的整个生产代码太长且受NDA约束)。关键思想被注释为代码注释。最终结果如下:

示例输出

首先,由于您尝试制作不同大小的节点,因此我会假设您的nodes数据中有一个radius属性。假设它是这样的对象数组:

{
  id: input.name,
  type: input.type,
  radius: input.radius
}

接下来添加标记。请注意,每个箭头(或标记)的大小为10,一半大小为5。你可以像@ɭɘ-ɖɵʊɒɼɖ-江戸在他的答案中所做的那样将其分配为变量,但我太懒了。

let marker = svg.append("defs")
  .attr("class", "defs")
  .selectAll("marker")
  // Assign a marker per link, instead of one per class.
  .data(links, function (d) { return d.source.id + "-" + d.target.id; });
// Update and exit are omitted.
// Enter
marker = marker
  .enter()
  .append("marker")
  .style("fill", "#000")
  // Markers are IDed by link source and target's name.
  // Spaces stripped because id can't have spaces.
  .attr("id", function (d) { return (d.source.id + "-" + d.target.id).replace(/\s+/g, ''); })
  // Since each marker is using the same data as each path, its attributes can similarly be modified.
  // Assuming you have a "value" property in each link object, you can manipulate the opacity of a marker just like a path.
  .style("opacity", function (d) { return Math.min(d.value, 1); })
  .attr("viewBox", "0 -5 10 10")
  // refX and refY are set to 0 since we will use the radius property of the target node later on, not here.
  .attr("refX", 0) 
  .attr("refY", 0)
  .attr("markerWidth", 5)
  .attr("markerHeight", 5)
  .attr("orient", "auto")
  .append("path")
  .attr("d", "M0,-5L10,0L0,5")
  .merge(marker);

然后,路径可以使用其ID引用每个单独的标记:

let path = svg.append("g")
  .attr("class", "paths")
  .selectAll("path")
  .data(links, function (d) { return d.source.id + "-" + d.target.id; });
// Update and exit are omitted.
// Enter
path = path
  .enter()
  .append("path")
  .attr("class", "enter")
  .style("fill", "none")
  .style("stroke", "#000")
  .style("stroke-opacity", function (d) { return Math.min(d.value, 1); })
  // This is how to connect each path to its respective marker
  .attr("marker-end", function(d) { return "url(#" + (d.source.id + "-" + d.target.id).replace(/\s+/g, '') + ")"; })
  .merge(path);

如果您想要更多功能,可以选择修改的一个选项是:允许您的.on("tick", ticked)监听器接收更多变量以测试边界。例如,svg的宽度和高度。

.on("tick", function () { ticked(node, path, width, height) })

这里是基于@ɭɘ-ɖɵʊɒɼɖ-江戸的答案改进后的新的勾选函数:

ticked(node, path, width, height) {
  node
    .attr("transform", function(d){return "translate(" + Math.max(d.radius, Math.min(width - d.radius, d.x)) + "," + Math.max(d.radius, Math.min(height - d.radius, d.y)) + ")"});

  path
    .attr("d", d => {
      let dx = d.target.x - d.source.x,
          dy = d.target.y - d.source.y,
          dr = Math.sqrt(dx * dx + dy * dy),
          gamma = Math.atan2(dy, dx), // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
          sx = Math.max(d.source.radius, Math.min(width - d.source.radius,  d.source.x + (Math.cos(gamma) * d.source.radius)  )),
          sy = Math.max(d.source.radius, Math.min(height - d.source.radius,  d.source.y + (Math.sin(gamma) * d.source.radius)  )),
          // Recall that 10 is the size of the arrow
          tx = Math.max(d.target.radius, Math.min(width - d.target.radius,  d.target.x - (Math.cos(gamma) * (d.target.radius + 10))  )), 
          ty = Math.max(d.target.radius, Math.min(height - d.target.radius,  d.target.y - (Math.sin(gamma) * (d.target.radius + 10))  ));
      // If you like a tighter curve, you may recalculate dx dy dr:
      //dx = tx - sx;
      //dy = ty - sy;
      //dr = Math.sqrt(dx * dx + dy * dy);
      return "M" + sx + "," + sy + "A" + dr + "," + dr + " 0 0,1 " + tx + "," + ty;
    });
  }

正如@joshua-comeau所提到的,计算sx和sy时应该使用加号。

2

最终我决定为每个链接创建一个标记(而不是每个类别创建一个)。 这种解决方案的优点是根据目标节点定义每个标记的偏移量,而在我的情况下,目标节点是refX。

  // One marker for link...
  svg.append("svg:defs").selectAll("marker")
      .data(force.links())
    .enter().append("svg:marker")
      .attr("id", function(link, idx){ return 'marker-' + idx})
      .attr("viewBox", "0 -5 10 10")
      .attr("refX", function(link, idx){
        return  10 + link.target.size;
      })
      .attr("refY", 0)
      .attr("markerWidth", 6)
      .attr("markerHeight", 6)
      .attr("orient", "auto")
    .append("svg:path")
      .attr("d", "M0,-5L10,0L0,5")
      .attr("fill", function(link){
        if(link.type == 'in')
          return "green";
        return "blue";
      });

现在有一个小问题,就是这条线是弯曲的。这意味着标记/箭头不仅应该在X轴上移动,还应该在Y轴上移动一个值,该值可能取决于曲线的半径...


1

这是我的解决方案:

首先,我计算路径与水平轴的夹角(gamma)。然后,我获取半径的X分量(Math.cos(gamma) * radius)和Y分量(Math.sin(gamma) * radius)。接下来,通过这些分量偏移路径的两端。

function linkArc(d) {
    var t_radius = calcRadius(d.target.size);
    var s_radius = calcRadius(d.source.size);
    var dx = d.target.x - d.source.x;
    var dy = d.target.y - d.source.y;
    var gamma = Math.atan(dy / dx);
    var tx = d.target.x - (Math.cos(gamma) * t_radius);
    var ty = d.target.y - (Math.sin(gamma) * t_radius);
    var sx = d.source.x - (Math.cos(gamma) * s_radius);
    var sy = d.source.y - (Math.sin(gamma) * s_radius);

    return "M" + sx + "," + sy + "L" + tx + "," + ty;
}

首先,您会注意到我没有使用弧线,但原则应该是相同的。 此外,我的节点具有大小属性,我可以从中计算出圆的直径。
最后,我的标记定义如下:
var arrowsize = 10;
var asHalf = arrowsize / 2;
svg.append("defs").selectAll("marker")
        .data(["arrowhead"])
        .enter().append("marker")
        .attr("id", function (d) {
            return d;
        })
        .attr("viewBox", "0 -5 " + arrowsize + " " + arrowsize)
        .attr("refX", arrowsize)
        .attr("refY", 0)
        .attr("markerWidth", 9)
        .attr("markerHeight", 9)
        .attr("orient", "auto")
        .attr("class", "arrowhead-light")
        .append("path")
        .attr("d", "M 0," + (asHalf * -1) + " L " + arrowsize + ",0 L 0," + asHalf);

我还没有找到一种方法来控制每一个标记的副本。

1
我认为你的代码有误:对于我来说,我必须将gamma*radius添加到源坐标中:var sx = d.source.x + (Math.cos(gamma) * s_radius);var sy = d.source.y + (Math.sin(gamma) * s_radius); - Joshua Comeau

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