I’m trying to achieve a basic d3 network with a connection between two nodes, and I want to have the connection to end at the edge of the destination node (so end-marker is positioned properly) instead of the center of it. I use a getIntersection()
from Svg draw connection line between two rectangles.
On the Y axis the end point of the link seems to have the right value, but on the X axis an offset appears as I an going further from the origin.
Here is the code that fails:
graph = { "nodes": [{id: 0},{id: 1}], "links": [{source: 0,target: 1}] } const width = 800, height = 600 const nodeWidth = 80, nodeHeight = 20 const arrowDepth = 10 var svg = d3.select("#svg-body").append("svg").attr("viewBox", [0, 0, width, height]) svg.append("svg:defs").selectAll("marker") .data(["end"]) .enter().append("svg:marker") .attr("id", String) .attr("viewBox", `0 -5 ${arrowDepth} ${arrowDepth}`) .attr("refX", 0) .attr("refY", -0.5) .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("svg:polyline") .attr("points", `0,-5 ${arrowDepth},0 0,5`) ; let link = svg .selectAll(".link") .data(graph.links) .join("line") .classed("link", true) .attr("marker-end", "url(#end)") const node = svg.selectAll(".node") .data(graph.nodes) .enter().append("g") .classed("node", true) .on("click", click) .call( d3.drag() .on("start", dragstart) .on("drag", dragged) ) node.append("rect") .attr("height", nodeHeight) .attr("width", nodeWidth); const simulation = d3 .forceSimulation() .nodes(graph.nodes) .force("charge_force", d3.forceManyBody().strength(-100)) .force("center_force", d3.forceCenter(width / 2, height / 2)) .force("links", d3.forceLink(graph.links) .distance(80) ) .on("tick", tick) .tick(100) function getIntersection(dx, dy, cx, cy, w, h) { // Hit vertical edge of box1 if (Math.abs(dy / dx) < h / w) { return [cx + (dx > 0 ? w : -w), cy + dy * w / Math.abs(dx)]; } else { // Hit horizontal edge of box1 return [cx + dx * h / Math.abs(dy), cy + (dy > 0 ? h : -h)]; } }; function tick() { link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => getIntersection( d.source.x-d.target.x, d.source.y-d.target.y, d.target.x, d.target.y, nodeWidth, nodeHeight )[0]) .attr("y2", d => getIntersection( d.source.x-d.target.x, d.source.y-d.target.y, d.target.x, d.target.y, nodeWidth, nodeHeight )[1]); node.attr("transform", function(d) { return `translate(${d.x - nodeWidth/2}, ${d.y - nodeHeight/2})`; }); } function click(event, d) { delete d.fx; delete d.fy; d3.select(this).classed("fixed", false); simulation.alpha(1).restart() } function clamp(x, lo, hi) { return x < lo ? lo : x > hi ? hi : x; } function dragstart() { d3.select(this).classed("fixed", true); } function dragged(event, d) { d.fx = clamp(event.x, 0, width); d.fy = clamp(event.y, 0, height); simulation.alpha(1).restart() }
.link { stroke: #000; stroke-width: 1.5px; } .node { cursor: move; fill: #ccc; stroke: #000; stroke-width: 1.5px; } .node.fixed { fill: #f00; }
<script src="http://d3js.org/d3.v6.min.js"></script> <div id="svg-body"></div>
Answer
If you look at the linked example you’ll see that the answerer made clear that you have to divide the width and height by 2:
You also have the width and height divided by 2. (their emphasis)
Also, I’m changing refX
so the arrow head ends exactly at the end of the line.
Here is your code with these changes:
graph = { "nodes": [{ id: 0 }, { id: 1 }], "links": [{ source: 0, target: 1 }] } const width = 800, height = 600 const nodeWidth = 80, nodeHeight = 20 const arrowDepth = 10 var svg = d3.select("#svg-body").append("svg").attr("viewBox", [0, 0, width, height]) svg.append("svg:defs").selectAll("marker") .data(["end"]) .enter().append("svg:marker") .attr("id", String) .attr("viewBox", `0 -5 ${arrowDepth} ${arrowDepth}`) .attr("refX", 10) .attr("refY", 0) .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("svg:polyline") .attr("points", `0,-5 ${arrowDepth},0 0,5`); let link = svg .selectAll(".link") .data(graph.links) .join("line") .classed("link", true) .attr("marker-end", "url(#end)") const node = svg.selectAll(".node") .data(graph.nodes) .enter().append("g") .classed("node", true) .on("click", click) .call( d3.drag() .on("start", dragstart) .on("drag", dragged) ) node.append("rect") .attr("height", nodeHeight) .attr("width", nodeWidth); const simulation = d3 .forceSimulation() .nodes(graph.nodes) .force("charge_force", d3.forceManyBody().strength(-100)) .force("center_force", d3.forceCenter(width / 2, height / 2)) .force("links", d3.forceLink(graph.links) .distance(80) ) .on("tick", tick) .tick(100) function getIntersection(dx, dy, cx, cy, w, h) { // Hit vertical edge of box1 if (Math.abs(dy / dx) < h / w) { return [cx + (dx > 0 ? w : -w), cy + dy * w / Math.abs(dx)]; } else { // Hit horizontal edge of box1 return [cx + dx * h / Math.abs(dy), cy + (dy > 0 ? h : -h)]; } }; function tick() { link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => getIntersection( d.source.x - d.target.x, d.source.y - d.target.y, d.target.x, d.target.y, nodeWidth / 2, nodeHeight / 2 )[0]) .attr("y2", d => getIntersection( d.source.x - d.target.x, d.source.y - d.target.y, d.target.x, d.target.y, nodeWidth / 2, nodeHeight / 2 )[1]); node.attr("transform", function(d) { return `translate(${d.x - nodeWidth/2}, ${d.y - nodeHeight/2})`; }); } function click(event, d) { delete d.fx; delete d.fy; d3.select(this).classed("fixed", false); simulation.alpha(1).restart() } function clamp(x, lo, hi) { return x < lo ? lo : x > hi ? hi : x; } function dragstart() { d3.select(this).classed("fixed", true); } function dragged(event, d) { d.fx = clamp(event.x, 0, width); d.fy = clamp(event.y, 0, height); simulation.alpha(1).restart() }
.link { stroke: #000; stroke-width: 1.5px; } .node { cursor: move; fill: #ccc; stroke: #000; stroke-width: 1.5px; } .node.fixed { fill: #f00; }
<script src="http://d3js.org/d3.v6.min.js"></script> <div id="svg-body"></div>