javascript - 如何在 d3 中不同尺寸的矩形之间绘制有向箭头?

标签 javascript d3.js svg

我想在矩形(由矩形表示的节点)之间绘制有向弧,以使箭头尖端始终以优雅的方式击中边缘。我看过很多关于如何对圆(由圆表示的节点)执行此操作的帖子。非常有趣的是,大多数 d3 示例都处理圆形和方形(尽管方形的程度较小)。

我有一个示例代码 here 。现在我最好的尝试只能从中心点画到中心点。我可以移动终点(箭头应该在的位置),但是在尝试拖动矩形时,弧线的行为并不符合预期。

这就是我所得到的。 enter image description here

但我需要这样的东西。 enter image description here

关于如何在 d3 中轻松完成此操作,有什么想法吗?是否有一些内置库/函数可以帮助解决此类问题(例如拖动功能)?

最佳答案

解决您的问题的简单算法是

  • 当拖动节点时,对其每个传入/传出边缘执行以下操作
    • a是被拖动的节点并且 b通过传出/传入边缘到达的节点
    • lineSegmenta 中心之间的线段和b
    • 计算a的交点和lineSegment ,这是通过迭代构成盒子的 4 个段并检查每个段与 lineSegment 的交集来完成的。 ,让iaa 的线段之一的交点和lineSegment ,查找ib以类似的方式

我考虑过但尚未解决的极端情况

  • 当一个盒子的中心位于另一个盒子内部时,不会有 2 个线段相交
  • 当两个交点相同时! (在编辑中解决了这个问题)
  • 当你的图表是multigraph时边缘将渲染在彼此之上

plunkr demo

编辑:添加了检查 ia === ib为了避免从左上角创建边缘,您可以在 plunkr 演示中看到这一点

$(document).ready(function() {
  var graph = {
    nodes: [
      { id: 'n1', x: 10, y: 10, width: 200, height: 200 },
      { id: 'n2', x: 10, y: 270, width: 200, height: 250 },
      { id: 'n3', x: 400, y: 270, width: 200, height: 300 }
    ],
    edges: [
      { start: 'n1', stop: 'n2' },
      { start: 'n2', stop: 'n3' }
    ],
    node: function(id) {
      if(!this.nmap) {
        this.nmap = { };
        for(var i=0; i < this.nodes.length; i++) {
          var node = this.nodes[i];
          this.nmap[node.id] = node;
        }
      }
      return this.nmap[id];
    },
    mid: function(id) {
      var node = this.node(id);
      var x = node.width / 2.0 + node.x,
          y = node.height / 2.0 + node.y;
      return { x: x, y: y };
    }
  };
  
  var arcs = d3.select('#mysvg')
  .selectAll('line')
  .data(graph.edges)
  .enter()
    .append('line')
    .attr({
      'data-start': function(d) { return d.start; },
      'data-stop': function(d) { return d.stop; },
      x1: function(d) { return graph.mid(d.start).x; },
      y1: function(d) { return graph.mid(d.start).y; },
      x2: function(d) { return graph.mid(d.stop).x; },
      y2: function(d) { return graph.mid(d.stop).y },
      style: 'stroke:rgb(255,0,0);stroke-width:2',
      'marker-end': 'url(#arrow)'
    });
  
  var g = d3.select('#mysvg')
    .selectAll('g')
    .data(graph.nodes)
    .enter()
      .append('g')
      .attr({
        id: function(d) { return d.id; },
        transform: function(d) {
          return 'translate(' + d.x + ',' + d.y + ')';
        }
      });
  g.append('rect')
    .attr({
      id: function(d) { return d.id; },
      x: 0,
      y: 0,
      style: 'stroke:#000000; fill:none;',
      width: function(d) { return d.width; },
      height: function(d) { return d.height; },
      'pointer-events': 'visible'
    });
  
  function Point(x, y) {
    if (!(this instanceof Point)) {
      return new Point(x, y)
    }
    this.x = x
    this.y = y
  }
  Point.add = function (a, b) {
    return Point(a.x + b.x, a.y + b.y)
  }
  Point.sub = function (a, b) {
    return Point(a.x - b.x, a.y - b.y)
  }
  Point.cross = function (a, b) {
    return a.x * b.y - a.y * b.x;
  }
  Point.scale = function (a, k) {
    return Point(a.x * k, a.y * k)
  }
  Point.unit = function (a) {
    return Point.scale(a, 1 / Point.norm(a))
  }
  Point.norm = function (a) {
    return Math.sqrt(a.x * a.x + a.y * a.y)
  }
  Point.neg = function (a) {
    return Point(-a.x, -a.y)
  }
  
  function pointInSegment(s, p) {
    var a = s[0]
    var b = s[1]
    return Math.abs(Point.cross(Point.sub(p, a), Point.sub(b, a))) < 1e-6 &&
      Math.min(a.x, b.x) <= p.x && p.x <= Math.max(a.x, b.x) && 
      Math.min(a.y, b.y) <= p.y && p.y <= Math.max(a.y, b.y) 
  }
  
  function lineLineIntersection(s1, s2) {
    var a = s1[0]
    var b = s1[1]
    var c = s2[0]
    var d = s2[1]
    var v1 = Point.sub(b, a)
    var v2 = Point.sub(d, c)
    //if (Math.abs(Point.cross(v1, v2)) < 1e-6) {
    //  // collinear
    //  return null
    //}
    var kNum = Point.cross(
      Point.sub(c, a),
      Point.sub(d, c)
    )
    var kDen = Point.cross(
      Point.sub(b, a),
      Point.sub(d, c)
    )
    var ip = Point.add(
      a,
      Point.scale(
        Point.sub(b, a),
        Math.abs(kNum / kDen)
      )
    )
    return ip
  }
  
  function segmentSegmentIntersection(s1, s2) {
    var ip = lineLineIntersection(s1, s2)
    if (ip && pointInSegment(s1, ip) && pointInSegment(s2, ip)) {
      return ip
    }
  }
    
  function boxSegmentIntersection(box, lineSegment) {
    var data = box.data()[0]
    var topLeft = Point(data.x, data.y)
    var topRight = Point(data.x + data.width, data.y)
    var botLeft = Point(data.x, data.y + data.height)
    var botRight = Point(data.x + data.width, data.y + data.height)
    var boxSegments = [
      // top
      [topLeft, topRight],
      // bot
      [botLeft, botRight],
      // left
      [topLeft, botLeft],
      // right
      [topRight, botRight]
    ]
    var ip
    for (var i = 0; !ip && i < 4; i += 1) {
      ip = segmentSegmentIntersection(boxSegments[i], lineSegment)
    }
    return ip
  }
  
  function boxCenter(a) {
    var data = a.data()[0]
    return Point(
      data.x + data.width / 2,
      data.y + data.height / 2
    )
  }
  
  function buildSegmentThroughCenters(a, b) {
    return [boxCenter(a), boxCenter(b)]
  }
  
  // should return {x1, y1, x2, y2}
  function getIntersection(a, b) {
    var segment = buildSegmentThroughCenters(a, b)
    console.log(segment[0], segment[1])
    var ia = boxSegmentIntersection(a, segment)
    var ib = boxSegmentIntersection(b, segment)
    if (ia && ib) {
      
      // problem: the arrows are drawn after the intersection with the box
      // solution: move the arrow toward the other end
      
      var unitV = Point.unit(Point.sub(ib, ia))
      // k = the width of the marker
      var k = 18
      ib = Point.sub(ib, Point.scale(unitV, k))
      
      return {
        x1: ia.x,
        y1: ia.y,
        x2: ib.x,
        y2: ib.y
      }  
    }
  }
  
  var drag = d3.behavior.drag()
  .origin(function(d) { 
    return d;
  })
  .on('dragstart', function(e) {
    d3.event.sourceEvent.stopPropagation();
  })
  .on('drag', function(e) {
    e.x = d3.event.x;
    e.y = d3.event.y;
    
    var id = 'g#' + e.id
    var target = d3.select(id)
    target.data().x = e.x
    target.data().y = e.y
    target.attr({
      transform: 'translate(' + e.x + ',' + e.y + ')'
    });
    
    d3.selectAll('line[data-start=' + e.id + ']')
      .each(function (d) {
        var line = d3.select(this)
        var other = d3.select('g#' + line.attr('data-stop'))
        var intersection = getIntersection(target, other)
        intersection && line.attr(intersection)
      })
    
    d3.selectAll('line[data-stop=' + e.id + ']')
      .each(function (d) {
        var line = d3.select(this)
        var other = d3.select('g#' + line.attr('data-start'))
        var intersection = getIntersection(other, target)
        intersection && line.attr(intersection)
      })

  })
  .on('dragend', function(e) {
    
  });
  g.call(drag);
})
      svg#mysvg { border: 1px solid black;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

<svg id="mysvg" width="800" height="800">
  <defs>
    <marker id="arrow" markerWidth="10" markerHeight="10" refx="0" refy="3" orient="auto" markerUnits="strokeWidth">
      <path d="M0,0 L0,6 L9,3 z" fill="#f00" />
    </marker>
  </defs>
</svg>

关于javascript - 如何在 d3 中不同尺寸的矩形之间绘制有向箭头?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36783216/

相关文章:

html - 使用 HTML SVG 元素的更简洁方法?

javascript - 如何在twitter api中的关注按钮回调函数中检索用户详细信息?

javascript - 理解javascript函数调用和引用

javascript - 使用字典作为D3数据源

javascript - D3 select multiple tag- 提取值

javascript - SVG getPointAtLength() 返回无效坐标

javascript - 这个 CSS 菜单是如何创建的?

javascript - 如何在 Puppeteer 中查找 document.activeElement

javascript - 谁能解释 D3 中 delaunay 实现的 alpha 过滤?

css - SVG CSS 多个动画