Group objects and auto layout

Here’s an example:

<!DOCTYPE html>
<html>
<head>
  <title>Simple Shifting of Nodes to Avoid Overlapping with Groups</title>
  <!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  After each layout, the <code>shiftNodes</code> function is called on each group to make sure no
  nodes overlap with the group.  The function moves overlapping nodes rightwards or downwards or both.
  A layout normally happens whenever any node changes size, so groups do not need to be expanded
  in order for any overlapping nodes to be shifted.
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
function init() {
  const $ = go.GraphObject.make;
  myDiagram =
    $(go.Diagram, "myDiagramDiv",
      {
        "LayoutCompleted": e => { e.diagram.findTopLevelGroups().each(shiftNodes); },
        "undoManager.isEnabled": true,
        "ModelChanged": e => {
          if (e.isTransactionFinished) document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      });

  function makePort(name, spot) {
    return $(go.Shape, "Circle",
      { width: 6, height: 6, strokeWidth: 0, fill: "green",
        alignment: spot, alignmentFocus: spot.opposite() },
      { portId: name, fromLinkable: true, toLinkable: true,
        fromSpot: spot, toSpot: spot });
  }

  myDiagram.nodeTemplate =
    $(go.Node, "Spot",
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      $(go.Panel, "Auto",
        { width: 100, height: 80 },
        $(go.Shape, { fill: "lightgreen" }),
        $(go.TextBlock, { editable: true },
          new go.Binding("text").makeTwoWay())
      ),
      makePort("T", go.Spot.Top),
      makePort("R", go.Spot.Right),
      makePort("B", go.Spot.Bottom),
      makePort("L", go.Spot.Left),
    );

  myDiagram.groupTemplate =
    $(go.Group, "Vertical",  // title above Placeholder
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      new go.Binding("isSubGraphExpanded").makeTwoWay(),
      { background: "transparent" },
      $(go.Panel, "Horizontal",  // button next to TextBlock
        { stretch: go.GraphObject.Horizontal },
        $("SubGraphExpanderButton",
          { alignment: go.Spot.Right, margin: 5 }),
        $(go.TextBlock,
          {
            alignment: go.Spot.Left,
            editable: true,
            margin: 5
          },
          new go.Binding("text").makeTwoWay())
      ),  // end Horizontal Panel
      $(go.Panel, "Auto",
        { alignment: go.Spot.Left },
        $(go.Shape, { fill: "whitesmoke" }),
        $(go.Placeholder,
          { padding: 10 })
      )
    );

  myDiagram.linkTemplate =
    $(go.Link,
      { routing: go.Link.AvoidsNodes },
      $(go.Shape),
      $(go.Shape, { toArrow: "Standard" })
    );

  // This function moves any nodes that overlap with the given Part, either rightwards or downwards.
  // Currently it is only called on Groups, but it could work for any Node.
  function shiftNodes(part) {
    const diagram = part.diagram;
    if (diagram === null) return;
    part.ensureBounds();
    const b = part.actualBounds;
    const overlaps = diagram.findObjectsIn(b,
          x => { const p = x.part; return (p.isTopLevel && p instanceof go.Node) ? p : null; },
          node => node !== part && !node.isMemberOf(part),
          true);
    let dx = 0;
    let dy = 0;
    const shiftsXY = new go.Set();
    const shiftsX = new go.Set();
    const shiftsY = new go.Set();
    overlaps.each(node => {
      const r = node.actualBounds;
      if (r.contains(b.right, b.bottom)) {
        dx = Math.max(dx, b.right - r.left);
        dy = Math.max(dy, b.bottom - r.top);
        shiftsXY.add(node);
      } else if (b.contains(r.left, r.bottom)) {
        dx = Math.max(dx, b.right - r.left);
        shiftsX.add(node);
      } else if (b.contains(r.right, r.top)) {
        dy = Math.max(dy, b.bottom - r.top);
        shiftsY.add(node);
      }
    });
    if (dx > 0) diagram.moveParts(shiftsX, new go.Point(dx+10, 0), false);
    if (dy > 0) diagram.moveParts(shiftsY, new go.Point(0, dy+10), false);
    if (dx > 0 && dy > 0) diagram.moveParts(shiftsXY, new go.Point(dx+10, dy+10), false);
  }


  myDiagram.model = $(go.GraphLinksModel,
    {
      linkFromPortIdProperty: "fpid",
      linkToPortIdProperty: "tpid",
      nodeDataArray:
        [
          { key: 0, isGroup: true, text: "Group", isSubGraphExpanded: false, loc: "0 0" },
          { key: 1, text: "Alpha", group: 0, loc: "10 10" },
          { key: 2, text: "Beta", group: 0, loc: "180 40" },
          { key: 3, text: "Gamma", group: 0, loc: "60 130" },
          { key: 4, text: "Delta", loc: "200 70" }  // in the way when the group is expanded
        ],
      linkDataArray:
        [
          { from: 1, fpid: "R", to: 2, tpid: "L" },
          { from: 1, fpid: "B", to: 3, tpid: "T" }
        ]
    });
}
window.addEventListener('DOMContentLoaded', init);
  </script>
</body>
</html>