Routing issue with AlignAll option

Hi, I noticed an issue with the AlignAll option introduced in GoJS 2.3.

As you could see in the screen recording below, adding a node causes a link over a node even I set routing: go.Link.AvoidsNodes in the linkTemplate.

addbox1

If I comment out the AlignAll option in groupTemplate, there is no link routing issue, as shown in the screen recording below.

addbox2

If I reload the browser with the updated nodeDataArray and linkDataArray, there is no routing issue either with alignOption: go.LayeredDigraphLayout.AlignAll, as shown below.

So, the routing issue only happens when a node is dynamically added to the diagram. Is there a way to solve this issue? I am posting the HTML + JS code below. You can copy it into an html file, open it in a browser, and click the Add Box button to check the issue.

Thanks!

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<style>
		      #file-upload {
        display: none;
      }	  

	</style>
    <title>Document</title>
  </head>
  <body>
    <div
      id="myDiagramDiv"
      style="border: solid 1px black; width: 100%; height: 700px"
    ></div>
    <div class="row">
        <button id="add">Add Box</button>
    </div>
    <script src="../../site/release/go-debug.js"></script>
    <script>
    function init() {
        const $ = go.GraphObject.make;
        myDiagram = $(go.Diagram, "myDiagramDiv", {
            layout: $(go.LayeredDigraphLayout, {
                layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
                packOption: go.LayeredDigraphLayout.PackStraighten,
                aggressiveOption: go.LayeredDigraphLayout.AggressiveNone,
                layerSpacing: 50,
                linkSpacing: 20,
                alignOption: go.LayeredDigraphLayout.AlignAll,
            }),
        });

        myDiagram.nodeTemplateMap.add("global", $(
            go.Node,
            "Spot",
            $(go.Shape, "Circle", {
            fill: "transparent",
            stroke: '#01778e',
            width: 50,
            height: 50,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer",
            }),
            $(
            go.TextBlock,
            new go.Binding("text", "key"),
            {
                verticalAlignment: go.Spot.Center,
                textAlign: "center",
                stroke: '#1d2024',
            },
            ),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(0, 0.5, 0, 0),
                portId: "Left",
                fromSpot: go.Spot.Left,
                toSpot: go.Spot.Left,
            }),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(1, 0.5, 0, 0),
                portId: "Right",
                fromSpot: go.Spot.Right,
                toSpot: go.Spot.Right,
            }),
        ));

        myDiagram.nodeTemplateMap.add("", $(
            go.Node,
            "Spot",
            $(go.Shape, "Rectangle", {
                fill: "transparent",
                stroke: "transparent",
                width: 124,
                height: 152,
                portId: "",
                fromLinkable: true,
                toLinkable: true,
                cursor: "pointer",
            }),
            $(
            go.Panel,
            "Position",
            $(go.Shape, "RoundedRectangle", {
                fill: "transparent",
                width: 64,
                height: 64,
                position: new go.Point(0, 12),
            }),
            $(
                go.TextBlock,
                new go.Binding("text", "key"),
                {
                width: 64,
                position: new go.Point(0, 84),
                verticalAlignment: go.Spot.Center,
                textAlign: "center",
                },
            ),
            ),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(0, 0.5, 30, 0),
                portId: "Left",
                fromSpot: go.Spot.Left,
                toSpot: go.Spot.Left,
            }),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(1, 0.5, -30, 0),
                portId: "Right",
                fromSpot: go.Spot.Right,
                toSpot: go.Spot.Right,
            }),
        ));

        myDiagram.nodeTemplateMap.add("group-end", $(go.Node, 'Spot',
            $(go.Shape, "Circle", {
                width: 0,
                height: 0,
                alignment: go.Spot.Center,
                fill: null,
                stroke: null,
            }),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(0, 0.5),
                portId: "Left",
                fromSpot: go.Spot.Left,
                toSpot: go.Spot.Left,
            }),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(1, 0.5),
                portId: "Right",
                fromSpot: go.Spot.Right,
                toSpot: go.Spot.Right,
            }),
        ));

        myDiagram.linkTemplate = $(
            go.Link,
            go.Link.Orthogonal,
            {
                relinkableFrom: true,
                relinkableTo: true,
                selectable: true,
                reshapable: true,
                corner: 10,
                toShortLength: 8,
                fromEndSegmentLength: 40,
                toEndSegmentLength: 40,
                routing: go.Link.AvoidsNodes,
            },
            $(go.Shape, { isPanelMain: true, strokeWidth: 1 }),
            $(go.Shape, {
                toArrow: "Standard",
            }),
        );

        myDiagram.groupTemplate = $(
            go.Group,
            "Spot",
            {
            layout: $(go.LayeredDigraphLayout, {
                layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
                packOption: go.LayeredDigraphLayout.PackStraighten,
                aggressiveOption: go.LayeredDigraphLayout.AggressiveNone,  
                layerSpacing: 50,
                linkSpacing: 20,
                alignOption: go.LayeredDigraphLayout.AlignAll,
            }),
            },
            $(go.Panel, "Horizontal",
                $(go.Shape, "Rectangle", {
                    width: 30,
                    fill: "transparent",
                    strokeWidth: 0,
                    stretch: go.GraphObject.Fill
                }),
                $(go.Panel, "Auto",
                    $(go.Shape, "Rectangle", {
                    fill: "transparent",
                    strokeWidth: 0,
                    stretch: go.GraphObject.Fill
                    }),
                    $(go.Panel, "Auto", 
                    $(
                        go.Shape,
                        "RoundedRectangle", // surrounds everything
                        {
                            parameter1: 10,
                            fill: "white",
                            stroke: '#999999',
                            minSize: new go.Size(240, 220),
                            spot1: go.Spot.TopLeft,
                            spot2:go.Spot.BottomRight
                        }
                    ),
                    $(
                        go.Panel,
                        "Vertical", // position header above the subgraph
                        $(go.Panel, "Auto", {
                        height: 28,
                        alignment: go.Spot.Left,
                        padding: new go.Margin(8, 12, 0, 12),
                        }, $(go.TextBlock, // stage title near top, next to button
                        new go.Binding("text", "key")
                        )),
                        $(
                        go.Placeholder, // represents area for all member parts
                        {
                            padding: new go.Margin(4, 0, 4, 0),
                            background: "transparent",
                            minSize: new go.Size(240, 160),
                        }
                        ),
                        $(go.Panel, "Auto", {
                        height: 28,
                        stretch: go.GraphObject.Fill
                        })
                    )
                    )
                ),
                $(go.Shape, "Rectangle", {
                    width: 30,
                    fill: "transparent",
                    strokeWidth: 0,
                    stretch: go.GraphObject.Fill
                })
            ),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(0, 0.5, 30, 0),
                portId: "Left",
                fromSpot: go.Spot.Left,
                toSpot: go.Spot.Left,
            }),
            $(go.Shape, "Rectangle", {
                fill: null,
                stroke: null,
                desiredSize: new go.Size(0, 0),
                alignment: new go.Spot(1, 0.5, -30, 0),
                portId: "Right",
                fromSpot: go.Spot.Right,
                toSpot: go.Spot.Right,
            }
        ));

        const nodeDataArray = [
            {
                key: "Start",
                category: "global",
            },
            {
                key: "End",
                category: "global",
            },
            {
                key: "Group1",
                isGroup: true,
            },
            {
                key: "Group1-start",
                group: "Group1",
                category: "group-end"
            },
            {
                key: "Group1-end",
                group: "Group1",
                category: "group-end"
            },
            {
                key: "Box1",
                group: "Group1",                
            },
            {
                key: "Box2",
                group: "Group1",                
            },
            {
                key: "Box3",
                group: "Group1",                
            },
            {
                key: "Box4",
                group: "Group1",                
            }
        ];

        const linkDataArray = [
            {
                from: "Start",
                to: "Group1",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Group1",
                to: "End",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Group1-start",
                to: "Box1",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Box1",
                to: "Box2",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Box2",
                to: "Box3",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Box2",
                to: "Box4",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Box3",
                to: "Group1-end",
                fromPort: "Right",
                toPort: "Left",
            },
            {
                from: "Box4",
                to: "Group1-end",
                fromPort: "Right",
                toPort: "Left",
            }
        ];

        const model = new go.GraphLinksModel();
        model.nodeDataArray = nodeDataArray;
        model.linkDataArray = linkDataArray;
        model.linkFromPortIdProperty = "fromPort";
        model.linkToPortIdProperty = "toPort";
        model.nodeGroupKey = "group";
        myDiagram.model = model;
    }

    window.addEventListener("DOMContentLoaded", init);  
    document.getElementById('add').addEventListener('click', e => {
        const groupKey = 'Group1'
        const endKey = `${groupKey}-end`;

        const existingKey = 'Box1';
        const newKey = `Box5`;
        myDiagram.model.addNodeData({ key: newKey, group: groupKey });
        myDiagram.model.addLinkData({
            from: existingKey,
            fromPort: "Right",
            to: newKey,
            toPort: "Left",
        });
        myDiagram.model.addLinkData({
            from: newKey,
            fromPort: "Right",
            to: endKey,
            toPort: "Left",
        });        
    });   
    </script>
  </body>
</html>

Thank you very much for providing a stand-alone app that reproduces the problem.

I noticed that the “group-end” node template was rather complicated, so I simplified it:

        myDiagram.nodeTemplateMap.add("group-end", $(go.Node));

That seems to fix the problem that you reported.

A few unrelated comments:

  • You can delete the LayeredDigraphLayout.packOption property settings.
  • Clicking on “Add Box” more than once produces bad results. Normally when adding a node, instead of specifying its key with a hard-coded key value, one just adds the node and then gets whatever unique key that the model assigned to it:
    document.getElementById('add').addEventListener('click', e => {
        const groupKey = 'Group1'
        const endKey = `${groupKey}-end`;
        const existingKey = 'Box1';
        const newdata = { group: groupKey };
        myDiagram.model.addNodeData(newdata);
        const newKey = newdata.key;
        myDiagram.model.addLinkData({
            from: existingKey,
            fromPort: "Right",
            to: newKey,
            toPort: "Left",
        });
        myDiagram.model.addLinkData({
            from: newKey,
            fromPort: "Right",
            to: endKey,
            toPort: "Left",
        });        
    });   
  • You have some “Auto” Panels that have zero or one elements in them. That’s probably wasteful – you can either just delete the empty panel or unwrap the single element, moving some of the panel’s properties to that single element. But maybe you were simplifying the template for this demo app, so they are vestigial.

Hi Walter, the sample code that I provided is a simplified version with the purpose to only illustrate the issue. For example, the Add Box button can only be clicked once since I hard-coded the values.

Changing the nodeTemplate of “group-end” to $(go.Node) does solve the solution. But we do have other use cases to give certain size to the end node. Once I updated the size to 10, I noticed the routing issue again.

        myDiagram.nodeTemplateMap.add("group-end", $(go.Node,
            $(go.Shape, "Circle", {
                width: 10,
                height: 10,
                alignment: go.Spot.Center,
                fill: "red",
                stroke: null,
            })
        ));

Is there any solution that deals with the end node with certain size? Thank you so much!

Hmm… even I just kept the size for the group start node, it still affect the routing to the end node :(

        myDiagram.nodeTemplateMap.add("group-start", $(go.Node,
            $(go.Shape, "Circle", {
                width: 10,
                height: 10,
                alignment: go.Spot.Center,
                fill: "red",
                stroke: null,
            })
        ));

        myDiagram.nodeTemplateMap.add("group-end", $(go.Node));

        const nodeDataArray = [
            {
                key: "Start",
                category: "global",
            },
            {
                key: "End",
                category: "global",
            },
            {
                key: "Group1",
                isGroup: true,
            },
            {
                key: "Group1-start",
                group: "Group1",
                category: "group-start"
            },
            {
                key: "Group1-end",
                group: "Group1",
                category: "group-end"
            },
            {
                key: "Box1",
                group: "Group1",                
            },
            {
                key: "Box2",
                group: "Group1",                
            },
            {
                key: "Box3",
                group: "Group1",                
            },
            {
                key: "Box4",
                group: "Group1",                
            }
        ];

Here is a simpler example:


const nodeDataArray = [
    {
        key: "Start",
        category: "global",
    },
    {
        key: "End",
        category: "global",
    },
    {
        key: "Group1",
        isGroup: true,
    },
    {
        key: "Group1-start",
        group: "Group1",
        category: "group-start"
    },
    {
        key: "Group1-end",
        group: "Group1",
        category: "group-end"
    },
    {
        key: "Box1",
        group: "Group1",                
    },
    {
        key: "Box2",
        group: "Group1",                
    },
    {
        key: "Box3",
        group: "Group1",                
    },
];

const linkDataArray = [
    {
        from: "Start",
        to: "Group1",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Group1",
        to: "End",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Group1-start",
        to: "Box2",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Group1-start",
        to: "Box1",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box1",
        to: "Box3",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box2",
        to: "Group1-end",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box3",
        to: "Group1-end",
        fromPort: "Right",
        toPort: "Left",
    }
];

Try setting Layout.isRouting to false on both layouts.
It should “work” because the Link.routing is AvoidsNodes.

Hi Walter, I found a problematic use case with isRouting: false when the diagram is initially loaded.

With isRouting: false set in the groupTemplate’s layout. Note that the link from Box 7 to the end and the link from Box 6 to the end are unnecessarily intersected. The routing issue is kept even I use $(go.Node) as the end node template.

isRouting: false is commented out.

const nodeDataArray = [
    {
        key: "Start",
        category: "global",
    },
    {
        key: "End",
        category: "global",
    },
    {
        key: "Group1",
        isGroup: true,
    },
    {
        key: "Group1-start",
        group: "Group1",
        category: "group-start"
    },
    {
        key: "Group1-end",
        group: "Group1",
        category: "group-start"
    },
    {
        key: "Box1",
        group: "Group1",                
    },
    {
        key: "Box2",
        group: "Group1",                
    },
    {
        key: "Box3",
        group: "Group1",                
    },
    {
        key: "Box4",
        group: "Group1",                
    },
    {
        key: "Box5",
        group: "Group1",                
    },
    {
        key: "Box6",
        group: "Group1",                
    },
    {
        key: "Box7",
        group: "Group1",                
    }
];

const linkDataArray = [
    {
        from: "Start",
        to: "Group1",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Group1",
        to: "End",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Group1-start",
        to: "Box1",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box1",
        to: "Box2",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box1",
        to: "Box7",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box2",
        to: "Box3",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box2",
        to: "Box6",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box3",
        to: "Box4",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box3",
        to: "Box5",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box4",
        to: "Group1-end",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box5",
        to: "Group1-end",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box6",
        to: "Group1-end",
        fromPort: "Right",
        toPort: "Left",
    },
    {
        from: "Box7",
        to: "Group1-end",
        fromPort: "Right",
        toPort: "Left",
    }
];

That route is correct – it doesn’t cross over any nodes.

I’m thinking you want routing like what ParallelLayout implements:
https://gojs.net/extensionsJSM/Parallel.html
In your case the blue diamond and blue circle nodes are zero sized “group-start” nodes stuck at the sides of the group.

Here you go:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<style>
      #file-upload {
        display: none;
      }	  
	</style>
    <title>Document</title>
  </head>
  <body>
    <div
      id="myDiagramDiv"
      style="border: solid 1px black; width: 100%; height: 700px"
    ></div>
    <div class="row">
        <button id="add">Add Box</button>
    </div>
    <script src="https://unpkg.com/gojs"></script>
    <script>

// Route all orthogonal links coming into a "group-end" category node to
// turn at the same x point. This only works for direction: 0.
class ParallelLDLayout extends go.LayeredDigraphLayout {
  constructor() {
    super();
    this.alignOption = go.LayeredDigraphLayout.AlignAll;
    this.layeringOption = go.LayeredDigraphLayout.LayerLongestPathSource;
    this.layerSpacing = 50;
    this.linkSpacing = 20;
  }

  commitLinks() {
    super.commitLinks();
    this.network.vertexes.each(v => {
      if (v.node && v.node.category === "group-end") {
        // assume this.direction === 0
        let x = v.node.port.getDocumentBounds().x - 30;
        v.sourceEdges.each(e => {
          const link = e.link;
          if (link && link.isOrthogonal) {
            if (link.pointsCount >= 6) {
              const pts = link.points.copy();
              const p3 = pts.elt(3);
              pts.setElt(3, new go.Point(x, p3.y));
              const p2 = pts.elt(2);
              pts.setElt(2, new go.Point(x, p2.y));
              link.points = pts;
            }
          }
        });
      }
    });
  }
}


function init() {
    const $ = go.GraphObject.make;

    myDiagram = $(go.Diagram, "myDiagramDiv",
      {
        layout: new ParallelLDLayout()
      });

    myDiagram.nodeTemplateMap.add("global", $(go.Node, "Spot",
      { background: "magenta" },
      $(go.Shape, "Circle", {
        fill: "transparent",
        stroke: '#01778e',
        width: 50,
        height: 50,
        portId: "",
        fromLinkable: true,
        toLinkable: true,
        cursor: "pointer",
        }),
      $(go.TextBlock,
        new go.Binding("text", "key"),
        {
            verticalAlignment: go.Spot.Center,
            textAlign: "center",
            stroke: '#1d2024',
        }),
      $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(0, 0.5, 0, 0),
            portId: "Left",
            fromSpot: go.Spot.Left,
            toSpot: go.Spot.Left,
        }),
      $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(1, 0.5, 0, 0),
            portId: "Right",
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Right,
        }),
    ));

    myDiagram.nodeTemplate = $(go.Node, "Spot",
        $(go.Shape, "Rectangle", {
            fill: "magenta", //"transparent",
            stroke: "transparent",
            width: 124,
            height: 152,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer",
        }),
        $(go.Panel, "Position",
          $(go.Shape, "RoundedRectangle", {
              fill: "transparent",
              width: 64,
              height: 64,
              position: new go.Point(0, 12),
          }),
          $(go.TextBlock,
              new go.Binding("text", "key"),
              {
              width: 64,
              position: new go.Point(0, 84),
              verticalAlignment: go.Spot.Center,
              textAlign: "center",
              },
          ),
        ),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(0, 0.5, 30, 0),
            portId: "Left",
            fromSpot: go.Spot.Left,
            toSpot: go.Spot.Left,
        }),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(1, 0.5, -30, 0),
            portId: "Right",
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Right,
        }),
    );

    myDiagram.nodeTemplateMap.add("group-start",
      $(go.Node, { fromSpot: go.Spot.Left, toSpot: go.Spot.Left }));

    myDiagram.nodeTemplateMap.add("group-end",
      $(go.Node, { fromSpot: go.Spot.Left, toSpot: go.Spot.Left }));

    myDiagram.linkTemplate = $(go.Link,
        //go.Link.Orthogonal,
        {
            relinkableFrom: true,
            relinkableTo: true,
            selectable: true,
            reshapable: true,
            corner: 10,
            toShortLength: 8,
            fromEndSegmentLength: 40,
            toEndSegmentLength: 40,
            routing: go.Link.Orthogonal, //go.Link.AvoidsNodes,
        },
        $(go.Shape, { strokeWidth: 1 }),
        $(go.Shape, { toArrow: "Standard" }),
    );

    myDiagram.groupTemplate = $(go.Group, "Spot",
        {
          layout: new ParallelLDLayout()
        },
        $(go.Panel, "Horizontal",
            $(go.Shape, "Rectangle", {
                width: 30,
                fill: "cyan", //"transparent",
                strokeWidth: 0,
                stretch: go.GraphObject.Fill
            }),
            $(go.Panel, "Auto",
              $(go.Shape, "Rectangle", {
                fill: "transparent",
                strokeWidth: 0,
                stretch: go.GraphObject.Fill
              }),
              $(go.Panel, "Auto", 
                $(go.Shape, "RoundedRectangle", // surrounds everything
                    {
                        parameter1: 10,
                        fill: "white",
                        stroke: '#999999',
                        minSize: new go.Size(240, 220),
                        spot1: go.Spot.TopLeft,
                        spot2:go.Spot.BottomRight
                    }
                ),
                $(go.Panel, "Vertical", // position header above the subgraph
                  $(go.Panel, "Auto", {
                      height: 28,
                      alignment: go.Spot.Left,
                      padding: new go.Margin(8, 12, 0, 12),
                    },
                    $(go.TextBlock, // stage title near top, next to button
                      new go.Binding("text", "key")
                    )
                  ),
                  $(go.Placeholder, // represents area for all member parts
                    {
                        padding: new go.Margin(4, 0, 4, 0),
                        background: "lightyellow", //"transparent",
                        minSize: new go.Size(240, 160),
                    }),
                  $(go.Panel, "Auto", {
                    background: "lime",
                    height: 28,
                    stretch: go.GraphObject.Fill
                    })
                )
              )
            ),
            $(go.Shape, "Rectangle", {
                width: 30,
                fill: "cyan", //"transparent",
                strokeWidth: 0,
                stretch: go.GraphObject.Fill
            })
        ),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(0, 0.5, 30, 0),
            portId: "Left",
            fromSpot: go.Spot.Left,
            toSpot: go.Spot.Left,
        }),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(1, 0.5, -30, 0),
            portId: "Right",
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Right,
        }
    ));

    const nodeDataArray = [
        {
            key: "Start",
            category: "global",
        },
        {
            key: "End",
            category: "global",
        },
        {
            key: "Group1",
            isGroup: true,
        },
        {
            key: "Group1-start",
            group: "Group1",
            category: "group-start"
        },
        {
            key: "Group1-end",
            group: "Group1",
            category: "group-end"
        },
        {
            key: "Box1",
            group: "Group1",                
        },
        {
            key: "Box2",
            group: "Group1",                
        },
        {
            key: "Box3",
            group: "Group1",                
        },
        {
            key: "Box4",
            group: "Group1",                
        }
// {
//     key: "Start",
//     category: "global",
// },
// {
//     key: "End",
//     category: "global",
// },
// {
//     key: "Group1",
//     isGroup: true,
// },
// {
//     key: "Group1-start",
//     group: "Group1",
//     category: "group-start"
// },
// {
//     key: "Group1-end",
//     group: "Group1",
//     category: "group-end"
// },
// {
//     key: "Box1",
//     group: "Group1",                
// },
// {
//     key: "Box2",
//     group: "Group1",                
// },
// {
//     key: "Box3",
//     group: "Group1",                
// },
// {
//     key: "Box4",
//     group: "Group1",                
// },
// {
//     key: "Box5",
//     group: "Group1",                
// },
// {
//     key: "Box6",
//     group: "Group1",                
// },
// {
//     key: "Box7",
//     group: "Group1",                
// }
    ];

    const linkDataArray = [
        {
            from: "Start",
            to: "Group1",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Group1",
            to: "End",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Group1-start",
            to: "Box1",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Box1",
            to: "Box2",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Box2",
            to: "Box3",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Box2",
            to: "Box4",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Box3",
            to: "Group1-end",
            fromPort: "Right",
            toPort: "Left",
        },
        {
            from: "Box4",
            to: "Group1-end",
            fromPort: "Right",
            toPort: "Left",
        }
// {
//     from: "Start",
//     to: "Group1",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Group1",
//     to: "End",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Group1-start",
//     to: "Box1",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box1",
//     to: "Box2",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box1",
//     to: "Box7",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box2",
//     to: "Box3",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box2",
//     to: "Box6",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box3",
//     to: "Box4",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box3",
//     to: "Box5",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box4",
//     to: "Group1-end",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box5",
//     to: "Group1-end",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box6",
//     to: "Group1-end",
//     fromPort: "Right",
//     toPort: "Left",
// },
// {
//     from: "Box7",
//     to: "Group1-end",
//     fromPort: "Right",
//     toPort: "Left",
// }
    ];

    const model = new go.GraphLinksModel();
    model.nodeDataArray = nodeDataArray;
    model.linkDataArray = linkDataArray;
    model.linkFromPortIdProperty = "fromPort";
    model.linkToPortIdProperty = "toPort";
    model.nodeGroupKey = "group";
    myDiagram.model = model;
}

window.addEventListener("DOMContentLoaded", init);  
document.getElementById('add').addEventListener('click', e => {
    const groupKey = 'Group1'
    const endKey = `${groupKey}-end`;
    const existingKey = 'Box1';
    const newdata = { group: groupKey };
    myDiagram.model.addNodeData(newdata);
    const newKey = newdata.key;
    myDiagram.model.addLinkData({
        from: existingKey,
        fromPort: "Right",
        to: newKey,
        toPort: "Left",
    });
    myDiagram.model.addLinkData({
        from: newKey,
        fromPort: "Right",
        to: endKey,
        toPort: "Left",
    });        
});   
    </script>
  </body>
</html>

Hi Walter, thank you so much for the code. However, since you did not use this.isRouting = false in ParallelLDLayout, the routing associated with the dynamic added node has the same issue.

If I add this.isRouting = false in ParallelLDLayout, I still see the link crossing with initial loading.

Note that the link-over-node issue ONLY happens when a node is dynamically added to the diagram. When I reload the diagram with the updated nodes and links, the diagram looks good. What is difference between dynamic update and initial load? Are they triggering different layout / routing calculation?

Yes, I know that the routing problem happens when adding a node to an existing diagram.

Here’s my latest version based on your code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div
      id="myDiagramDiv"
      style="border: solid 1px black; width: 100%; height: 700px"
    ></div>
    <div class="row">
        <button id="add">Add Box</button>
        <button onclick="load(0)">Reload 0</button>
        <button onclick="load(1)">Reload 1</button>
    </div>
    <script src="https://unpkg.com/gojs"></script>
    <script>

// Route all orthogonal links coming into a "group-end" category node to
// turn at the same x point. This only works for direction: 0.
class ParallelLDLayout extends go.LayeredDigraphLayout {
  constructor() {
    super();
    this.alignOption = go.LayeredDigraphLayout.AlignAll;
    this.layeringOption = go.LayeredDigraphLayout.LayerLongestPathSource;
    this.layerSpacing = 50;
    this.linkSpacing = 20;
  }

  commitLinks() {
    super.commitLinks();
    this.network.vertexes.each(v => {
      if (v.node && v.node.category === "group-end") {
        // assume this.direction === 0
        let x = v.node.port.getDocumentBounds().x - 30;
        v.sourceEdges.each(e => {
          const link = e.link;
          if (link && link.isOrthogonal) {
            if (link.pointsCount >= 6) {
              const pts = link.points.copy();
              const p3 = pts.elt(3);
              pts.setElt(3, new go.Point(x, p3.y));
              const p2 = pts.elt(2);
              pts.setElt(2, new go.Point(x, p2.y));
              link.points = pts;
            }
          }
        });
      }
    });
  }
}


function init() {
    const $ = go.GraphObject.make;

    myDiagram = $(go.Diagram, "myDiagramDiv",
      {
        layout: new ParallelLDLayout()
      });

    myDiagram.nodeTemplateMap.add("global", $(go.Node, "Spot",
      $(go.Shape, "Circle", {
        fill: "transparent",
        stroke: '#01778e',
        width: 50,
        height: 50,
        portId: "",
        fromLinkable: true,
        toLinkable: true,
        cursor: "pointer",
        }),
      $(go.TextBlock,
        new go.Binding("text", "key"),
        {
            verticalAlignment: go.Spot.Center,
            textAlign: "center",
            stroke: '#1d2024',
        }),
      $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(0, 0.5, 0, 0),
            portId: "Left",
            fromSpot: go.Spot.Left,
            toSpot: go.Spot.Left,
        }),
      $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(1, 0.5, 0, 0),
            portId: "Right",
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Right,
        }),
    ));

    myDiagram.nodeTemplate = $(go.Node, "Spot",
        $(go.Shape, "Rectangle", {
            fill: "transparent",
            stroke: "transparent",
            width: 124,
            height: 152,
            portId: "",
            fromLinkable: true,
            toLinkable: true,
            cursor: "pointer",
        }),
        $(go.Panel, "Position",
          $(go.Shape, "RoundedRectangle", {
              fill: "transparent",
              width: 64,
              height: 64,
              position: new go.Point(0, 12),
          }),
          $(go.TextBlock,
              new go.Binding("text", "key"),
              {
              width: 64,
              position: new go.Point(0, 84),
              verticalAlignment: go.Spot.Center,
              textAlign: "center",
              },
          ),
        ),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(0, 0.5, 30, 0),
            portId: "Left",
            fromSpot: go.Spot.Left,
            toSpot: go.Spot.Left,
        }),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(1, 0.5, -30, 0),
            portId: "Right",
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Right,
        }),
    );

    myDiagram.nodeTemplateMap.add("group-start",
      $(go.Node, { fromSpot: go.Spot.Left, toSpot: go.Spot.Left }));

    myDiagram.nodeTemplateMap.add("group-end",
      $(go.Node, { fromSpot: go.Spot.Left, toSpot: go.Spot.Left }));

    myDiagram.linkTemplate = $(go.Link,
        //go.Link.Orthogonal,
        {
            relinkableFrom: true,
            relinkableTo: true,
            selectable: true,
            reshapable: true,
            corner: 10,
            toShortLength: 8,
            fromEndSegmentLength: 40,
            toEndSegmentLength: 40,
            routing: go.Link.Orthogonal, //go.Link.AvoidsNodes,
        },
        $(go.Shape, { strokeWidth: 1 }),
        $(go.Shape, { toArrow: "Standard" }),
    );

    myDiagram.groupTemplate = $(go.Group, "Spot",
        {
          layout: new ParallelLDLayout()
        },
        $(go.Panel, "Horizontal",
            $(go.Shape, "Rectangle", {
                width: 30,
                fill: "transparent",
                strokeWidth: 0,
                stretch: go.GraphObject.Fill
            }),
            $(go.Panel, "Auto",
              $(go.Shape, "Rectangle", {
                fill: "transparent",
                strokeWidth: 0,
                stretch: go.GraphObject.Fill
              }),
              $(go.Panel, "Auto", 
                $(go.Shape, "RoundedRectangle", // surrounds everything
                    {
                        parameter1: 10,
                        fill: "white",
                        stroke: '#999999',
                        minSize: new go.Size(240, 220),
                        spot1: go.Spot.TopLeft,
                        spot2:go.Spot.BottomRight
                    }
                ),
                $(go.Panel, "Vertical", // position header above the subgraph
                  $(go.Panel, "Auto", {
                      height: 28,
                      alignment: go.Spot.Left,
                      padding: new go.Margin(8, 12, 0, 12),
                    },
                    $(go.TextBlock, // stage title near top, next to button
                      new go.Binding("text", "key")
                    )
                  ),
                  $(go.Placeholder, // represents area for all member parts
                    {
                        padding: new go.Margin(4, 0, 4, 0),
                        background: "transparent",
                        minSize: new go.Size(240, 160),
                    }),
                  $(go.Panel, "Auto", {
                    height: 28,
                    stretch: go.GraphObject.Fill
                    })
                )
              )
            ),
            $(go.Shape, "Rectangle", {
                width: 30,
                fill: "transparent",
                strokeWidth: 0,
                stretch: go.GraphObject.Fill
            })
        ),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(0, 0.5, 30, 0),
            portId: "Left",
            fromSpot: go.Spot.Left,
            toSpot: go.Spot.Left,
        }),
        $(go.Shape, "Rectangle", {
            fill: null,
            stroke: null,
            desiredSize: new go.Size(0, 0),
            alignment: new go.Spot(1, 0.5, -30, 0),
            portId: "Right",
            fromSpot: go.Spot.Right,
            toSpot: go.Spot.Right,
        }
    ));

    load(0);
}

function load(choice) {
  let nda = [];
  let lda = [];
  if (choice === 0) {    
    nda = [
      {
          key: "Start",
          category: "global",
      },
      {
          key: "End",
          category: "global",
      },
      {
          key: "Group1",
          isGroup: true,
      },
      {
          key: "Group1-start",
          group: "Group1",
          category: "group-start"
      },
      {
          key: "Group1-end",
          group: "Group1",
          category: "group-end"
      },
      {
          key: "Box1",
          group: "Group1",                
      },
      {
          key: "Box2",
          group: "Group1",                
      },
      {
          key: "Box3",
          group: "Group1",                
      },
      {
          key: "Box4",
          group: "Group1",                
      }
    ];
    lda = [
      {
          from: "Start",
          to: "Group1",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Group1",
          to: "End",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Group1-start",
          to: "Box1",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box1",
          to: "Box2",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box2",
          to: "Box3",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box2",
          to: "Box4",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box3",
          to: "Group1-end",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box4",
          to: "Group1-end",
          fromPort: "Right",
          toPort: "Left",
      }
    ];
  } else if (choice === 1) {
    nda = [
      {
          key: "Start",
          category: "global",
      },
      {
          key: "End",
          category: "global",
      },
      {
          key: "Group1",
          isGroup: true,
      },
      {
          key: "Group1-start",
          group: "Group1",
          category: "group-start"
      },
      {
          key: "Group1-end",
          group: "Group1",
          category: "group-end"
      },
      {
          key: "Box1",
          group: "Group1",                
      },
      {
          key: "Box2",
          group: "Group1",                
      },
      {
          key: "Box3",
          group: "Group1",                
      },
      {
          key: "Box4",
          group: "Group1",                
      },
      {
          key: "Box5",
          group: "Group1",                
      },
      {
          key: "Box6",
          group: "Group1",                
      },
      {
          key: "Box7",
          group: "Group1",                
      }
    ];
    lda = [
      {
          from: "Start",
          to: "Group1",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Group1",
          to: "End",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Group1-start",
          to: "Box1",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box1",
          to: "Box2",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box1",
          to: "Box7",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box2",
          to: "Box3",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box2",
          to: "Box6",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box3",
          to: "Box4",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box3",
          to: "Box5",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box4",
          to: "Group1-end",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box5",
          to: "Group1-end",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box6",
          to: "Group1-end",
          fromPort: "Right",
          toPort: "Left",
      },
      {
          from: "Box7",
          to: "Group1-end",
          fromPort: "Right",
          toPort: "Left",
      }
    ];
  }

    const model = new go.GraphLinksModel();
    model.nodeDataArray = nda;
    model.linkDataArray = lda;
    model.linkFromPortIdProperty = "fromPort";
    model.linkToPortIdProperty = "toPort";
    model.nodeGroupKey = "group";
    myDiagram.model = model;
}

window.addEventListener("DOMContentLoaded", init);  

document.getElementById('add').addEventListener('click', e => {
    const groupKey = 'Group1'
    const endKey = `${groupKey}-end`;
    const existingKey = 'Box1';
    const newdata = { group: groupKey };
    myDiagram.model.addNodeData(newdata);
    const newKey = newdata.key;
    myDiagram.model.addLinkData({
        from: existingKey,
        fromPort: "Right",
        to: newKey,
        toPort: "Left",
    });
    myDiagram.model.addLinkData({
        from: newKey,
        fromPort: "Right",
        to: endKey,
        toPort: "Left",
    });        
});   
    </script>
  </body>
</html>

Thank you so much Walter! commitLinks solves all the routing issues :)