Line and Rect intersection not calculated properly on one axis

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.

enter image description here enter image description here enter image description here

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>

Leave a Reply

Your email address will not be published. Required fields are marked *