javascript - How do I draw directed arrows between rectangles of different dimensions in d3? -
i draw directed arcs between rectangles (nodes represented rectangles) in such way arrow-tip hits edge in graceful way. have seen plenty of posts on how circles (nodes represented circles). quite interestingly, d3
examples deal circles , squares (though squares lesser extent).
i have example code here. right best attempt can draw center-point center-point. can shift end point (where arrow should be), upon experimenting dragging rectangles around, arcs don't behave intended.
any ideas on how can in d3
? there built-in library/function can type of thing (like dragging capabilities)?
a simple algorithm solve problem is
- when node dragged following each of incoming/outgoing edges
- let
a
node dragged ,b
node reached through outgoing/incoming edge - let
linesegment
line segment between centers ofa
,b
- compute intersection point of
a
,linesegment
, done iterating 4 segments make box , checking intersection of each of themlinesegment
, letia
intersection point of 1 of segments ofa
,linesegment
, findib
in similar fashion
- let
corner cases have considered haven't solved
- when box's center inside other box there won't 2 segment intersections
- when both intersections points same! (solved 1 in edit)
- when graph multigraph edges render on top of each other
edit: added check ia === ib
avoid creating edge top left corner, can see on 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; < 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 = 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 = 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 (var = 0; !ip && < 4; += 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: arrows drawn after intersection box // solution: move arrow toward other end var unitv = point.unit(point.sub(ib, ia)) // k = width of 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>
Comments
Post a Comment