简体   繁体   中英

How do I draw directed arrows between rectangles of different dimensions in d3?

I would like to draw directed arcs between rectangles (nodes that are represented by rectangles) in such a way that the arrow-tip always hits the edge in a graceful way. I have seen plenty of SO posts on how to do this for circles (nodes represented by circles). Quite interestingly, most d3 examples deal with circles and squares (though squares to a lesser extent).

I have an example code here . Right now my best attempt can only draw from center-point to center-point. I can shift the end point (where the arrow should be), but upon experimenting with dragging the rectangles around, the arcs don't behave as intended.

Here's what I've got. 在此输入图像描述

But I need something like this. 在此输入图像描述

Any ideas on how I can easily do this in d3 ? Is there some built-in library/function that can help with this type of thing (like with the dragging capabilities)?

A simple algorithm to solve your problem is

  • when a node is dragged do the following for each of its incoming/outgoing edges
    • let a be the node dragged and b the node reached through the outgoing/incoming edge
    • let lineSegment be a line segment between the centers of a and b
    • compute the intersection point of a and lineSegment , this is done by iterating the 4 segments that make the box and checking the intersection of each of them with lineSegment , let ia be the intersection point of one of the segments of a and lineSegment , find ib in a similar fashion

Corner cases that I have considered but haven't solved

  • when a box's center is inside the other box there won't be 2 segment intersections
  • when both intersections points are the same! (solved this one in an edit)
  • when your graph is a multigraph edges would render on top of each other

plunkr demo

EDIT: added the check ia === ib to avoid creating an edge from the top left corner, you can see this on the plunkr demo

 $(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(' + dx + ',' + dy + ')'; } }); 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(ax + bx, ay + by) } Point.sub = function (a, b) { return Point(ax - bx, ay - by) } Point.cross = function (a, b) { return ax * by - ay * bx; } Point.scale = function (a, k) { return Point(ax * k, ay * k) } Point.unit = function (a) { return Point.scale(a, 1 / Point.norm(a)) } Point.norm = function (a) { return Math.sqrt(ax * ax + ay * ay) } Point.neg = function (a) { return Point(-ax, -ay) } 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(ax, bx) <= px && px <= Math.max(ax, bx) && Math.min(ay, by) <= py && py <= Math.max(ay, by) } 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) { ex = d3.event.x; ey = d3.event.y; var id = 'g#' + e.id var target = d3.select(id) target.data().x = ex target.data().y = ey target.attr({ transform: 'translate(' + ex + ',' + ey + ')' }); 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> 

Here's the result: https://jsfiddle.net/he0f4u23/2/

For source arrows I just filled rectangles with white to paint the arrow.

For target it is a little bit more trickier than you think. You have to calculate source and target rectangles positions and draw your arrow accordingly.

I've made a tarmid function with addition to you mid function. Your mid function calculates the arrows source point which is fine. But for target point I used the tarmid function which is:

tarmid: function(d) {

            var startnode = this.node(d.start);
            var endnode = this.node(d.stop);
            if(startnode.x == endnode.x && startnode.y <= endnode.y){
              var x = endnode.width / 2.0 + endnode.x,
                y = endnode.y -17;
            }else if(startnode.x < endnode.x && startnode.y <= endnode.y){
              var x = endnode.x-17,
                y = endnode.y + startnode.height / 2.0;
            }
            return { x: x, y: y };
          }

see how I calculated the target point according to the rectangle placement. Also notice that these two are not the only cases for all rectangle placement and you must update your function accordingly so I'm leaving the rest to you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM