这是我的示例代码,它显示了一个简单的 d3 图,该图支持无需强制布局的节点拖动:
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #aaa;
}
.node text {
stroke:#333;
cursos:pointer;
}
.node circle{
stroke:#fff;
stroke-width:3px;
fill:#555;
}
</style>
<body>
<p id="first"><p>
<p id="second"><p>
<script>
var data = {
"nodes": [{
"id": "source1",
"x": 33,
"y": 133,
"width": 50,
"height": 50
},
{
"id": "target1",
"x": 166,
"y": 66,
"width": 50,
"height": 50
},
{
"id": "source2",
"x": 250,
"y": 40,
"width": 50,
"height": 50
},
{
"id": "target2",
"x": 350,
"y": 133,
"width": 50,
"height": 50
}
],
"links": [{
"source": "source1",
"target": "target1",
"weight": 1,
"id": "abc"
},
{
"source": "source2",
"target": "target2",
"weight": 3,
"id": "xyz"
}
]
};
var width = 1200,
height = 500
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var counterxOrtho = 0;
// Bootstrap the Drag Capability
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var dragInitiated = false
function dragstarted(d) {
d3.selectAll(".node").each(function(d) {
d3.select(this).classed("selectedNode", function(d) {
return d.selected = false;
})
})
d3.select(this).classed("selectedNode", function(d) {
d.previouslySelected = d.selected;
return d.selected = true;
});
dragInitiated = true
}
function dragged(d, i) {
if (dragInitiated) {
d3.event.sourceEvent.stopPropagation();
d3.selectAll(".linkInGraph").attr("d", function(l) {
var sourceNode = data.nodes.filter(function(d, i) {
return d.id == l.source
})[0];
var targetNode = data.nodes.filter(function(d, i) {
return d.id == l.target
})[0];
if (!(sourceNode.selected || targetNode.selected)) {
lineData.length = 0;
controlPointsArr = [];
l.controlPoints.forEach(function(d) {
controlPointsArr.push(d);
})
for (i = 0; i < controlPointsArr.length; i += 2) {
lineData.push({
"a": controlPointsArr[i],
"b": controlPointsArr[i + 1]
});
}
return lineFunction(lineData)
}
lineData.length = 0;
controlPointsArr = [];
var randomVal = 0;
randomVal = 25;
lineData.push({
"a": sourceNode.x + randomVal,
"b": sourceNode.y + 50
});
controlPointsArr.push(sourceNode.x + randomVal);
controlPointsArr.push(sourceNode.y + 50);
lineData.push({
"a": targetNode.x + randomVal,
"b": targetNode.y - 8
});
controlPointsArr.push(targetNode.x + randomVal);
controlPointsArr.push(targetNode.y - 8);
l.controlPoints = [];
for (i = 0; i < controlPointsArr.length; i++) {
l.controlPoints.push(controlPointsArr[i]);
}
return lineFunction(lineData)
})
nodes.filter(function(d) {
return d.selected;
})
.each(function(d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
var a = d.id;
var b = "\"";
var position = 0;
var output = [a.slice(0, position), b, a.slice(position)].join('');
output += "\"";
d3.select("[id=" + output + "]").attr("transform", "translate(" + (d.x) + "," + (d.y) + ")");
});
}
}
function dragended(d) {
if (d3.event.sourceEvent.which == 1) {
dragInitiated = false;
}
}
var nodes = svg.selectAll(".node")
.data(data.nodes)
.enter().append("g").attr("id", function(d) {
return d.id
})
.attr("class", "node").call(drag).attr("transform", function(d, i) {
return "translate(" + d.x + "," + d.y + ")";
});
nodes.append("rect")
.attr("width", "50").attr("height", "50").attr("fill", "lime").attr("rx", "5")
.attr("ry", "5").style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "").style("opacity", ".9");
nodes.append("text")
.attr("dx", 12)
.attr("dy", ".35em").attr("x", -12).attr("y", 25)
.text(function(d) {
return d.id
});
var LinkCurve = "linear";
var lineFunction = d3.svg.line()
.x(function(d) {
return d.a;
})
.y(function(d) {
return d.b;
})
.interpolate(LinkCurve);
// Marker elements for edges
var pathMarker = svg.append("marker").attr("id", "pathMarkerHead").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "0.1").attr("refY", "2");
pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy")
//
//The data for our line
var lineData = [];
function setupPolyLinks() {
d3.selectAll(".linkInGraph").remove();
edges = svg.selectAll("linkInGraph")
.data(data.links)
.enter()
.insert("path", ".node")
.attr("class", "linkInGraph").attr("id", function(l) {
return l.id;
}).attr("source", function(l) {
return l.source;
}).attr("target", function(l) {
return l.target;
}).attr("marker-end", "url(#pathMarkerHead)").attr("d", function(l) {
lineData.length = 0;
controlPointsArr = [];
var sourceNode = data.nodes.filter(function(d, i) {
return d.id == l.source
})[0];
var targetNode = data.nodes.filter(function(d, i) {
return d.id == l.target
})[0];
lineData.push({
"a": sourceNode.x + 25,
"b": sourceNode.y + 50
});
controlPointsArr.push(sourceNode.x + 25);
controlPointsArr.push(sourceNode.y + 50);
lineData.push({
"a": targetNode.x + 25,
"b": targetNode.y
});
controlPointsArr.push(targetNode.x + 25);
controlPointsArr.push(targetNode.y);
l.controlPoints = [];
for (i = 0; i < controlPointsArr.length; i++) {
l.controlPoints.push(controlPointsArr[i]);
}
return lineFunction(lineData)
}).style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none");
}
setupPolyLinks();
</script>
在此图中,拖动节点时,关联的链接始终在静态点开始和结束,即在这种情况下,链接从源的中下点开始,到目标的中上点结束。
我想要实现的是当拖动节点时,链接的起点和终点应该自动调整如下:
-如果目标在顶部,源在其下方,则链接应从源的顶部中间开始,到目标的底部中间结束。
但在我的例子中,它看起来像这样,我不希望这样:
-如果源和目标在一条水平线上,先是源再是目标,那么链接应该从源的右中开始,到目标的左中结束。
在我的例子中是这样的:
还有更多这样的案例......
这个想法是在拖动时链接永远不会与其自己的节点重叠。
最佳答案
这是一个将链接附加到被拖动节点右侧的解决方案:
var data = {
"nodes": [{
"id": "source1",
"x": 33,
"y": 133,
"width": 50,
"height": 50
},
{
"id": "target1",
"x": 166,
"y": 66,
"width": 50,
"height": 50
},
{
"id": "source2",
"x": 250,
"y": 40,
"width": 50,
"height": 50
},
{
"id": "target2",
"x": 350,
"y": 133,
"width": 50,
"height": 50
}
],
"links": [{
"source": "source1",
"target": "target1",
"weight": 1,
"id": "abc"
},
{
"source": "source2",
"target": "target2",
"weight": 3,
"id": "xyz"
}
]
};
let svg = d3.select("svg").attr("width", 1200).attr("height", 500);
// nodes:
let nodes = svg.selectAll(".node")
.data(data.nodes)
.enter().append("g")
.attr("id", d => d.id)
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.call(d3.drag().on("drag", dragged));
nodes.append("rect")
.attr("width", 50).attr("height", 50)
.attr("fill", "lime")
.attr("rx", 5).attr("ry", 5)
.style("stroke", "grey").style("stroke-width", "1.5").style("stroke-dasharray", "")
.style("opacity", ".9")
.style("cursor", "pointer");
nodes.append("text")
.attr("x", -12).attr("y", 25)
.attr("dx", 12).attr("dy", ".35em")
.text(d => d.id)
.style("cursor", "pointer");
// links:
var pathMarker = svg.append("marker").attr("id", "pathMarkerHead").attr("orient", "auto").attr("markerWidth", "8").attr("markerHeight", "12").attr("refX", "7").attr("refY", "2");
pathMarker.append("path").attr("d", "M0,0 V4 L7,2 Z").attr("fill", "navy");
svg.selectAll("linkInGraph")
.data(data.links)
.enter().append("path")
.attr("class", "linkInGraph")
.attr("id", d => d.id)
.attr("d", moveLink)
.style("stroke-width", "2").attr("stroke", "blue").attr("fill", "none")
.attr("marker-end", "url(#pathMarkerHead)");
// drag behavior:
function dragged(n) {
// Move the node:
d3.select(this)
.attr(
"transform",
d => "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")"
);
// Move the link:
d3.selectAll(".linkInGraph")
.filter(l => l.source == n.id || l.target == n.id)
.attr("d", moveLink)
}
// link position:
function moveLink(l) {
let nsid = data.nodes.filter(n => n.id == l.source)[0].id;
let ndid = data.nodes.filter(n => n.id == l.target)[0].id;
let ns = d3.select("#" + nsid).datum();
let nd = d3.select("#" + ndid).datum();
let min = Number.MAX_SAFE_INTEGER;
let best = {};
[[25, 0], [50, 25], [25, 50], [0, 25]].forEach(s =>
[[25, 0], [50, 25], [25, 50], [0, 25]].forEach(d => {
let dist = Math.hypot(
(nd.x + d[0]) - (ns.x + s[0]),
(nd.y + d[1]) - (ns.y + s[1])
);
if (dist < min) {
min = dist;
best = {
s: { x: ns.x + s[0], y: ns.y + s[1] },
d: { x: nd.x + d[0], y: nd.y + d[1] }
};
}
})
);
var lineFunction = d3.line().x(d => d.x).y(d => d.y).curve(d3.curveLinear);
return lineFunction([best.s, best.d]);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
由于目标在于避免拖动的节点与其链接之间的重叠,我们必须将链接附加到其节点的适当一侧。
对于给定的链接,最佳节点的边就是最小化链接长度的边。
因此,我们的想法是计算链接在连接到它的两个节点边的所有组合时可以获得的 16 种大小;在我们的例子中是 [[25, 0], [50, 25], [25, 50], [0, 25]]
与自身的笛卡尔积(其中节点的宽度/高度是50,这个列表的每个元素都是一个节点边的中间坐标)。
请注意链接末尾 svg 标记的变化。我必须在链接内稍微翻译一下,以使箭头的头部与链接的末端重合,从而避免箭头位于节点内。
另请注意,我转而使用 d3v5 以避免再制作一个 d3v3 遗留示例(如有必要,切换回 d3v3 应该不会那么困难)。
关于javascript - 根据D3图中的节点位置调整链路起点和终点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50990010/