How to update connection when using Routing.AvoidsNodes

We have identified an issue that can be easily reproduced using the interactive flowchart sample.

Initially, when the yellow “note” element is on the right side, the link looks in the following way

When I move this note on the link, link’s route will be adjusted

However, when I move it back, it will not be adjusted

So, my question is, what is the right way to configure my diagram to make sure the route comes into the initial position after the “obstacle” has been either moved away or removed?

Yes, that’s intentional behavior. That link that was rerouted because a node overlapped it still has a valid route when that area becomes empty. Perhaps the link had been manually reshaped (or resegmented), and as a policy we did not want to lose that information unnecessarily.

One way to do that is in a “SelectionMoved” DiagramEvent listener to invalidate the link routes of the links near where the node(s) had been. The simple but brute-force way to do that is to just invalidate all link routes:

    myDiagram = new go.Diagram('myDiagramDiv', {
      'SelectionMoved': e => {
        e.diagram.links.each(l => l.invalidateRoute());
      },

Note how, as with many but not all DiagramEvent listeners, the listener is called within the transaction that is occurring anyway.

If you want a more precise route invalidation of only the links that need to be re-routed, I don’t think there’s an easy way to determine which links really ought to be rerouted just because they would want to go through a given area. You would either have to guess using heuristics, or actually compute the new route for each link to see if it goes through the vacated area, and if it didn’t then restore its original route.

@walter Thank you, I will check whether it works and come back to you 👍

@walter for moving nodes it works.

However, in one place in our product when we modify diagram

diagram.links.each(l => l.invalidateRoute());

doesn’t cause the link routes to update as expected.

The modification includes three steps:

  1. Remove the link between Step 1 and Step 2
    I iterate through all links, find the one with { from: 1, to: 2 }, and remove it from the model.

  2. Add a new link between Step 1 and Step 3
    I call model.addLinkData({ from: 1, to: 3 }).

  3. Remove the entire Step 2 node
    I locate the node with key 2 and remove it, which also removes any links connected to it.

After these changes, the diagram updates correctly in terms of data, but the link routing remains stale. Calling invalidateRoute() on all links doesn’t trigger a route recalculation — the link visually stays in its old position until I manually move one of the nodes.

I’m looking for guidance on why invalidateRoute() doesn’t reroute in this scenario, and what the recommended way is to force all links to recompute their paths after programmatic diagram changes.

We cannot currently change the order of the steps mentioned above. Here is the example to reproduce the issue: when you click on “Modify Diagram”, I expect the connection to be sstraight; however,it stays curved.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>GoJS Diagram Sample</title>
  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>

  <style>
    body {
      font-family: sans-serif;
      margin: 0;
      padding: 20px;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    #diagramDiv {
      width: 800px;
      height: 400px;
      border: 2px solid #ccc;
      border-radius: 8px;
      margin-top: 20px;
    }
    button {
      padding: 10px 18px;
      font-size: 16px;
      cursor: pointer;
      border-radius: 6px;
      border: 1px solid #999;
      background: #eee;
      margin-right: 10px;
    }
    button:hover {
      background: #e3e3e3;
    }
  </style>
</head>

<body>
  <div style="text-align:center; max-width:800px;">
    <h1>Go.js Diagram Sample</h1>
    <p>Click the buttons to modify or reset the diagram.</p>

    <button id="modifyBtn">Modify Diagram</button>
    <button id="resetBtn">Reset Diagram</button>

    <div id="diagramDiv"></div>
  </div>

  <script>
    const $ = go.GraphObject.make;
    let diagram = null;

    const initialNodes = [
      { key: 1, text: "Step 1", loc: "0 0" },
      { key: 2, text: "Step 2", loc: "200 0" },
      { key: 3, text: "Step 3", loc: "400 0" },
    ];

    const initialLinks = [
      { from: 1, to: 2 },
      { from: 2, to: 3 }
    ];

    function initDiagram() {
      diagram = $(go.Diagram, "diagramDiv", {
        "undoManager.isEnabled": true
      });

      diagram.nodeTemplate = $(
        go.Node, "Auto",
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(go.Shape, "RoundedRectangle", {
          fill: "lightblue",
          strokeWidth: 2,
          stroke: "#2563eb",
          width: 120,
          height: 60
        }),
        $(go.TextBlock,
          {
            margin: 10,
            font: "bold 14px sans-serif",
            stroke: "#1e40af"
          },
          new go.Binding("text", "text")
        )
      );

      diagram.linkTemplate = $(
        go.Link,
        {
          routing: go.Link.AvoidsNodes,
          curve: go.Link.JumpOver,
          corner: 20
        },
        $(go.Shape, { strokeWidth: 2, stroke: "#64748b" }),
        $(go.Shape, { toArrow: "Standard", fill: "#64748b", stroke: "#64748b" })
      );

      resetModel();
    }

    function resetModel() {
      diagram.model = new go.GraphLinksModel(
        JSON.parse(JSON.stringify(initialNodes)),
        JSON.parse(JSON.stringify(initialLinks))
      );
    }

    function modifyDiagram() {
      if (!diagram) return;

      const model = diagram.model;

      // 1. Remove link 1 → 2
      diagram.links.each(link => {
        const data = link.data;
        if (data.from === 1 && data.to === 2) {
          model.removeLinkData(data);
        }
      });

      // 2. Add link 1 → 3
      model.addLinkData({ from: 1, to: 3 });

      // 3. Remove node 2
      const node2 = diagram.findNodeForKey(2);
      if (node2) diagram.remove(node2);

      diagram.links.each(l => l.invalidateRoute());
    }

    document.addEventListener("DOMContentLoaded", () => {
      initDiagram();
      document.getElementById("modifyBtn").onclick = modifyDiagram;
      document.getElementById("resetBtn").onclick = resetModel;
    });
  </script>
</body>
</html>

Thanks again for the sample.

The first thing I noticed was that you were not conducting a Transaction. So I wrapped the modifying code that is in your modifyDiagram function with a transaction. But that was not sufficient to fix the problem. Instead I think it’s a bug in GoJS. Here’s one way to get around it:

    function modifyDiagram() {
      if (!diagram) return;

      const model = diagram.model;
      model.commit(model => {

        // 1. Remove link 1 → 2
        new go.List(diagram.links).each(link => {
          const data = link.data;
          if (data.from === 1 && data.to === 2) {
            model.removeLinkData(data);
          }
        });

        // 2. Add link 1 → 3
        model.addLinkData({ from: 1, to: 3 });

        // 3. Remove node 2
        const node2 = diagram.findNodeForKey(2);
        if (node2) {
          // temporary work-around for bug fixed in 3.1.3
          node2.avoidableMargin = new go.Margin();
          diagram.remove(node2);
        }

        diagram.links.each(l => l.invalidateRoute());

      });
    }

This should be fixed in 3.1.3; I’m not sure when we’ll release it, but maybe later this week.

@walter your proposed solution by using node2.avoidableMargin = new go.Margin(); worked for us.

Thanks a lot

Try using 3.1.3. GoJS 3.1.3 released

I hope you can remove that work-around of setting the Node.avoidableMargin.

@walter I think it will take time for us to upgrade to 3.1.3, so we will stay with this workaround for a while. Thank you for the help 👍