简体   繁体   English

根据D3图中的节点位置调整链接起点和终点

[英]Adjust link start-end point according to node position in D3 graph

Here is my sample code that shows a simple d3 graph which supports node dragging without force layout: 这是我的示例代码,显示了一个简单的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) { dx += d3.event.dx; dy += 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(" + (dx) + "," + (dy) + ")"); }); } } 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(" + dx + "," + dy + ")"; }); 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 da; }) .y(function(d) { return db; }) .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> 

In this graph, while dragging nodes, the associated link always starts and ends at static points ie in this case, the link starts from lower middle point of source and end at top middle point of target. 在此图中,在拖动节点时,关联的链接始终在静态点处开始和结束,即,在这种情况下,链接从源的下中点开始,并在目标的上中点处结束。

What I want to achieve is when dragging node, the links start and end point should auto adjust like: 我要实现的是拖动节点时,链接的起点和终点应自动调整,例如:

-In a case where target is at top and source just below it, then link should start at from top middle of source and end at bottom middle of target. -如果目标位于源的顶部,源位于其下,则链接应从源的顶部开始,并在目标的底部结束。

But in my case it appears like this, which I don't want: 但就我而言,它看起来像这样,我不想要:

错误的情况1

-In a case where source and target are in a horizontal line where first is source and then target, then link should start at from right middle of source and end at left middle of target. -如果源和目标位于一条首先是源然后是目标的水平线中,则链接应从源的右中间开始,并在目标的左中间结束。

In my case it is like this: 在我的情况下是这样的:

错误的情况2

And more cases like this... 还有更多类似这样的情况...

The idea is for a link to never overlap with its own node while dragging. 这个想法是使链接在拖动时永远不会与其自己的节点重叠

Here is a solution which attaches links to the right side of the node being dragged: 这是一个将链接附加到被拖动节点右侧的解决方案:

 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(" + dx + "," + dy + ")") .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(" + (dx = d3.event.x) + "," + (dy = 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 => dx).y(d => dy).curve(d3.curveLinear); return lineFunction([best.s, best.d]); } 
 <script src="https://d3js.org/d3.v5.min.js"></script> <svg></svg> 

Since the goal consists in avoiding overlap between the dragged node and its link, we have to attach links to the appropriate side of its nodes. 由于目标是避免被拖动的节点与其链接之间出现重叠,因此我们必须将链接附加到其节点的适当一侧。

For a given link, the optimal node's sides are simply the ones which minimize the length of the link. 对于给定的链接,最佳节点的边仅是使链接长度最小化的边。

The idea is thus to compute the 16 sizes the link can get if it was attached to all combinations of its couple of nodes' sides; 因此,其思想是计算如果链接附加到其节点对端的所有组合上,则链接可获得的16种大小; which is in our case the cartesian product of [[25, 0], [50, 25], [25, 50], [0, 25]] with itself (where a node's width/height is 50 and each elements of this list is the coordinates of the middle of a node's side). 在我们的例子中是[[25, 0], [50, 25], [25, 50], [0, 25]] 25,0 [[25, 0], [50, 25], [25, 50], [0, 25]]与其自身的笛卡尔积(其中节点的宽/高为50,并且每个元素的宽度/高度list是节点边中间的坐标)。


Note the change in the svg marker at the end of links. 注意链接末尾的svg标记中的更改。 I had to translate it a bit within the link in order to have the head of the arrow coincide with the end of the link and thus avoid having the arrow within the node. 为了使箭头的头部与链接的末端重合,我不得不在链接内进行一些翻译,从而避免了箭头在节点内。


Also note that I switched to using d3v5 to avoid making one more d3v3 legacy example (the switch back to d3v3 shouldn't be that hard if necessary). 还要注意,我改用d3v5以避免再做一个d3v3旧示例(如果需要,切换回d3v3并不难)。

Here is another solution I found myself, it uses the angle calculation of dragged nodes to find the best position of links. 这是我发现的另一个解决方案,它使用拖动节点的角度计算来找到链接的最佳位置。

 <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": 200, "y": 300, "width": 50, "height": 50 }, { "id": "target1", "x": 500, "y": 200, "width": 50, "height": 50 }, { "id": "source2", "x": 600, "y": 120, "width": 50, "height": 50 }, { "id": "target2", "x": 900, "y": 300, "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(".linksOnUi").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]; // Angle calculation to check the position of target/source node with respective of it's source/target node // to find where the link should start/end to make it look better while dragging var dy = targetNode.y - sourceNode.y; var dx = targetNode.x - sourceNode.x; var theta = Math.atan2(dy, dx); theta *= 180 / Math.PI; var SourceMX = 0; var SourceMY = 0; var TargetMX = 0; var TargetMY = 0; if (theta <= 170 && theta >= 10) { SourceMX = 0; SourceMY = 0; TargetMX = 0; TargetMY = 0; } else if ((theta <= 180 && theta >= 170) || (theta <= -150 && theta >= -180)) { SourceMX = -sourceNode.width / 2; SourceMY = -sourceNode.height / 2; TargetMX = targetNode.width / 2 + 8; TargetMY = targetNode.height / 2; } else if (theta <= -45 && theta >= -150) { SourceMX = 0; SourceMY = -sourceNode.height; TargetMX = 0; TargetMY = targetNode.height + 14; } else { SourceMX = sourceNode.width / 2; SourceMY = -sourceNode.height / 2; TargetMX = -targetNode.width / 2 - 8; TargetMY = targetNode.height / 2; } 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 + SourceMX, "b": sourceNode.y + 50 + SourceMY }); controlPointsArr.push(sourceNode.x + randomVal + SourceMX); controlPointsArr.push(sourceNode.y + 50 + SourceMY); lineData.push({ "a": targetNode.x + randomVal + TargetMX, "b": targetNode.y - 8 + TargetMY }); controlPointsArr.push(targetNode.x + randomVal + TargetMX); controlPointsArr.push(targetNode.y - 8 + TargetMY); counterxOrtho = counterxOrtho + .9; if (counterxOrtho > 20) { counterxOrtho = 20 } 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) { dx += d3.event.dx; dy += 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(" + (dx) + "," + (dy) + ")"); }); } } 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(" + dx + "," + dy + ")"; }); 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 da; }) .y(function(d) { return db; }) .interpolate(LinkCurve); // Marker elements for edges var pathMarker = svg.append("marker").attr("id", "arrowHeadMarker").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 linkSetupFuncn() { d3.selectAll(".linksOnUi").remove(); edges = svg.selectAll("linksOnUi") .data(data.links) .enter() .insert("path", ".node") .attr("class", "linksOnUi").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(#arrowHeadMarker)").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"); } linkSetupFuncn(); </script> 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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