我想在矩形(由矩形表示的节点)之间绘制有向弧,以使箭头尖始终以优美的方式击中边缘。我已经看到很多关于如何为圈子(以圈子表示的节点)执行此操作的帖子。有趣的是,大多数d3
示例都涉及圆形和正方形(尽管正方形程度较小)。
我有一个示例代码here。现在,我最大的尝试只能从中心点画出。我可以移动终点(箭头应该在的位置),但是尝试在周围拖动矩形时,弧线的行为不符合预期。
这就是我所拥有的。
但是我需要这样的东西。
关于如何在d3
中轻松完成此操作的任何想法?是否有一些内置库/函数可以帮助这类事情(例如拖动功能)?
最佳答案
解决问题的一种简单算法是
拖动节点时,请对其节点的每个传入/传出边缘执行以下操作
设a
为拖动的节点,b
为通过传出/传入边缘到达的节点
设lineSegment
为a
和b
的中心之间的线段
计算a
和lineSegment
的交点,方法是迭代4个组成框的线段,并用lineSegment
检查它们的交点,令ia
是其中一个的交点。 a
和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>