How to changeLink Routing and Curve based on the flow positions

Hi,

I’m trying to implement the following UI.

The idea is I want to use the following properties for my link:

  • curve: go.Curve.Bezier
  • routing: go.Routing.Normal

However, if such link crosses the nodes it connects, I want to fall back to:

  • curve: go.Curve.Bezier
  • routing: go.Routing.Normal

I would appreciate if you could guide me 🙏

Did you want the link to fall back to no curve and Routing.Orthogonal or Routing.AvoidsNodes?

So is the question really: how can I tell whether a curved link crosses over an unrelated node (i.e., not Link.fromNode or toNode)?

It is both:

  1. How can I tell whether a curved link crosses over an unrelated node?
  2. What is the right way to assigned curve and routing based on the value we got on the 1st step.

Here’s the answer for #1. Just call the doesBezierLinkCrossOverAnotherNode predicate.
I’ll think about #2.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <button id="myTestButton">Test</button>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">
const myDiagram =
  new go.Diagram("myDiagramDiv", {
      layout: new go.LayeredDigraphLayout(),
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

myDiagram.linkTemplate =
  new go.Link({
      // routing: go.Link.Orthogonal, corner: 10,
      curve: go.Link.Bezier, fromEndSegmentLength: 35, toEndSegmentLength: 35
    })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "OpenTriangle" })
    );

myDiagram.model = new go.GraphLinksModel({
  linkKeyProperty: "key",
  nodeDataArray: [
    { key: 1, text: "Alpha", color: "lightblue" },
    { key: 3, text: "Gamma", color: "lightgreen" },
    { key: 4, text: "Delta", color: "pink" },
    { key: 2, text: "Beta", color: "orange" },
  ],
  linkDataArray: [
    { key: "1-2", from: 1, to: 2 },
    { key: "1-3", from: 1, to: 3 },
    { key: "1-4", from: 1, to: 4 },
    { key: "3-4", from: 3, to: 4 },
  ]
});

// P and INTERSECTS are optional Point and go.Set values,
// to potentially avoid unnecessary memory allocation, and
// to be able to tell which Nodes it crossed over.
function doesBezierLinkCrossOverAnotherNode(link, p, intersects) {
  if (link.curve !== go.Curve.Bezier) return false;
  if (!p) p = new go.Point();
  if (!intersects) intersects = new go.Set();
  const fromB = link.fromNode.actualBounds;
  const toB = link.toNode.actualBounds;
  const geo = link.geometry;
  const linkB = link.routeBounds;
  const intv = 1/(Math.sqrt(linkB.width * linkB.width + linkB.height * linkB.height) / 8);
  for (let frac = intv; frac < 1.0; frac += intv) {
    geo.getPointAlongPath(frac, p);
    p.offset(linkB.x, linkB.y);
    // crossing over the fromNode or the toNode is OK
    if (fromB.containsPoint(p)) continue;
    if (toB.containsPoint(p)) continue;
    // see if there are any other Parts at this point
    myDiagram.findPartsAt(p, false, intersects);
  }
  return intersects.any(part => part instanceof go.Node);
}

document.getElementById("myTestButton").addEventListener("click", e => {
  myDiagram.links.each(link => {
    if (doesBezierLinkCrossOverAnotherNode(link)) console.log(link.key);
  })
});
  </script>
</body>
</html>

Are you using GoJS v3 or an earlier version?

OK, I’ll assume you are using v3, which has support for Routers.

I think this does what I’m guessing you want:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">

class CurviRouter extends go.Router {
  constructor(init) {
    super();
    this.isRealtime = false;
    if (init) Object.assign(this, init);
  }

  routeLinks(links, coll) {
    const allLinks = (coll instanceof go.Diagram ? coll.links : coll.memberParts.filter(p => p instanceof go.Link));
    const p = new go.Point();
    const intersects = new go.Set();
    allLinks.each(link => {
      if (link.curve !== go.Curve.Bezier) {
        link.curve = go.Curve.Bezier;
        link.routing = go.Routing.Normal;
        link.fromEndSegmentLength= 35;
        link.toEndSegmentLength = 35;
        link.updateRoute();
      }
      intersects.clear();
      if (this.doesBezierLinkCrossOverAnotherNode(link, p, intersects)) {
        link.curve = go.Curve.None;
        link.routing = go.Routing.AvoidsNodes;
        link.fromEndSegmentLength= 10;
        link.toEndSegmentLength = 10;
        link.updateRoute();
      }
    });
  }

  // P and INTERSECTS are optional Point and go.Set values,
  // to potentially avoid unnecessary memory allocation, and
  // to be able to tell which Nodes it crossed over.
  doesBezierLinkCrossOverAnotherNode(link, p, intersects) {
    if (link.curve !== go.Curve.Bezier) return false;
    if (!p) p = new go.Point();
    if (!intersects) intersects = new go.Set();
    const fromB = link.fromNode.actualBounds;
    const toB = link.toNode.actualBounds;
    const geo = link.geometry;
    const linkB = link.routeBounds;
    const intv = 1/(Math.sqrt(linkB.width * linkB.width + linkB.height * linkB.height) / 8);
    for (let frac = intv; frac < 1.0; frac += intv) {
      geo.getPointAlongPath(frac, p);
      p.offset(linkB.x, linkB.y);
      // crossing over the fromNode or the toNode is OK
      if (fromB.containsPoint(p)) continue;
      if (toB.containsPoint(p)) continue;
      // see if there are any other Parts at this point
      myDiagram.findPartsAt(p, false, intersects);
    }
    return intersects.any(part => part instanceof go.Node);
  }
}

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      layout: new go.LayeredDigraphLayout(),
      "undoManager.isEnabled": true
    });
myDiagram.routers.insertAt(0, new CurviRouter());  // takes precedence over AvoidsNodesRouter

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white" })
        .bind("fill", "color"),
      new go.TextBlock({ margin: 8 })
        .bind("text")
    );

myDiagram.linkTemplate =
  new go.Link({
      corner: 10,  // used when orthogonal
      curve: go.Curve.Bezier, fromEndSegmentLength: 35, toEndSegmentLength: 35
    })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "OpenTriangle" })
    );

myDiagram.model = new go.GraphLinksModel({
  linkKeyProperty: "key",
  nodeDataArray: [
    { key: 1, text: "Alpha", color: "lightblue" },
    { key: 3, text: "Gamma", color: "lightgreen" },
    { key: 4, text: "Delta", color: "pink" },
    { key: 2, text: "Beta", color: "orange" },
  ],
  linkDataArray: [
    { key: "1-2", from: 1, to: 2 },
    { key: "1-3", from: 1, to: 3 },
    { key: "1-4", from: 1, to: 4 },
    { key: "3-4", from: 3, to: 4 },
  ]
});
  </script>
</body>
</html>

@walter thanks a lot, it seems exactly what I need. Yes I’m using 3rd version.

Could you just explain what the following code does?

const intv = 1 / (Math.sqrt(linkB.width * linkB.width + linkB.height * linkB.height) / 8);

And I need your help about one more thing.

In my code, I have a different arrow design that looks in the following way.

Look at the Beta node. Is it possible to make arrow aligned with the connection direction. Currently it loos so weird.

That’s just estimating the frequency at which it should sample to see whether there’s a Node intersecting it.

As I said before the angle at the end point with “Beta” is zero – horizontal. It’s just that the toEndSegmentLength isn’t big enough to make it near zero farther away from the end point.

Actually, an alternative that might work in your situation is to make the link path stop before the node, but draw it further to make it look like it does. And the arrowhead has to be shifted equally.

  new go.Link({
      corner: 10,  // used when orthogonal
      curve: go.Curve.Bezier, fromEndSegmentLength: 30, toEndSegmentLength: 30,
      fromSpot: go.Spot.Right, toSpot: new go.Spot(0, 0.5, -10, 0), toShortLength: -10
    })
    .add(
      new go.Shape(),
      new go.Shape({ toArrow: "OpenTriangle", alignmentFocus: new go.Spot(1, 0.5, -10, 0) })
    );

I changed the layout so it doesn’t set the fromSpot or toSpot:

  new go.Diagram("myDiagramDiv", {
      layout: new go.LayeredDigraphLayout({ setsPortSpots: false }),
      . . .