我想在矩形(由矩形表示的节点)之间绘制有向弧,以使箭头尖端始终以优雅的方式击中边缘。我看过很多关于如何对圆(由圆表示的节点)执行此操作的帖子。非常有趣的是,大多数 d3 示例都处理圆形和方形(尽管方形的程度较小)。
我有一个示例代码 here 。现在我最好的尝试只能从中心点画到中心点。我可以移动终点(箭头应该在的位置),但是在尝试拖动矩形时,弧线的行为并不符合预期。
关于如何在 d3
中轻松完成此操作,有什么想法吗?是否有一些内置库/函数可以帮助解决此类问题(例如拖动功能)?
最佳答案
解决您的问题的简单算法是
- 当拖动节点时,对其每个传入/传出边缘执行以下操作
- 让
a
是被拖动的节点并且b
通过传出/传入边缘到达的节点 - 让
lineSegment
是a
中心之间的线段和b
- 计算
a
的交点和lineSegment
,这是通过迭代构成盒子的 4 个段并检查每个段与lineSegment
的交集来完成的。 ,让ia
是a
的线段之一的交点和lineSegment
,查找ib
以类似的方式
- 让
我考虑过但尚未解决的极端情况
- 当一个盒子的中心位于另一个盒子内部时,不会有 2 个线段相交
- 当两个交点相同时! (在编辑中解决了这个问题)
- 当你的图表是multigraph时边缘将渲染在彼此之上
编辑:添加了检查 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/