Include nested groups inside SwimLaneLayout

I am using SwimLaneLayout in the diagram below. Each big box is a node right now. I am wondering if I could make each big box as a group to include a set of nodes (illustrated in red).

I know that the swimlanes are already groups and am wondering if nested groups are supported by SwimLaneLayout. If yes, how to do it?

My HTML + JS code is below.

<!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" />
    <title>Document</title>
  </head>
  <body>
    <div
      id="myDiagramDiv"
      style="border: solid 1px black; width: 100%; height: 800px"
    ></div>
    <script src="../../release/go-debug.js"></script>
    <script src="../../extensions/SwimLaneLayout.js"></script>
    <script>
      let myDiagram;
      function init() {
        const $ = go.GraphObject.make;
        myDiagram = $(go.Diagram, "myDiagramDiv", {
          layout: $(SwimLaneLayout, {
            laneProperty: "group", // needs to know how to assign vertexes/nodes into lanes/groups
            setsPortSpots: false,
            layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
            layerSpacing: 100,
            columnSpacing: 50,
            commitLayers: function (layerRects, offset) {
              if (layerRects.length === 0) return;

              var rect = layerRects[layerRects.length - 1];
              var totallength = rect.right;

              for (var i = 0; i < this.laneNames.length; i++) {
                var lane = this.laneNames[i];
                // assume lane names do not conflict with node names
                var group = this.diagram.findNodeForKey(lane);
                if (group === null) {
                  this.diagram.model.addNodeData({ key: lane, isGroup: true });
                  group = this.diagram.findNodeForKey(lane);
                }
                group.location = new go.Point(
                  -10,
                  this.lanePositions.get(lane) * this.columnSpacing + offset.y,
                );
                var ph = group.findObject("PLACEHOLDER"); // won't be a go.Placeholder, but just a regular Shape
                if (ph === null) ph = group;
                ph.desiredSize = new go.Size(
                  totallength,
                  this.laneBreadths.get(lane) * this.columnSpacing,
                );
              }
            },
          }),
        });

        myDiagram.nodeTemplate = $(
          go.Node,
          "Position",
          $(go.Shape, "RoundedRectangle", {
            name: "CONTAINER",
            fill: "transparent",
            width: 200,
            height: 150,
            stroke: null,
          }),
          $(go.Shape, "RoundedRectangle", {
            fill: "white",
            position: new go.Point(0, 0),
            width: 200,
            height: 120,
            cursor: "pointer",
          }),
          $(
            go.TextBlock, // the text label
            new go.Binding("text", "key"),
            {
              position: new go.Point(0, 120),
              width: 200,
              height: 30,
              verticalAlignment: go.Spot.Center,
              textAlign: "center",
            },
          ),
        );

        myDiagram.linkTemplate = $(
          go.Link,
          { relinkableFrom: true, relinkableTo: true },
          { routing: go.Link.AvoidsNodes, corner: 10 },
          $(go.Shape, { strokeWidth: 1.5 }),
          $(go.Shape, { toArrow: "Standard", stroke: null }),
        );

        myDiagram.groupTemplate = $(
          go.Group,
          "Horizontal",
          {
            layerName: "Background",
            movable: false,
            copyable: false,
            locationObjectName: "PLACEHOLDER",
            layout: null,
            avoidable: false,
          },
          $(
            go.TextBlock,
            {
              font: "bold 12pt sans-serif",
              angle: 270,
            },
            new go.Binding("text", "key"),
          ),
          $(
            go.Panel,
            "Auto",
            $(go.Shape, { fill: "transparent", stroke: "orange" }),
            $(go.Shape, {
              name: "PLACEHOLDER",
              fill: null,
              stroke: null,
              strokeWidth: 0,
            }),
          ),
        );

        const nodeDataArray = [
          {
            key: "A",
            isGroup: true,
          },
          {
            key: "B",
            isGroup: true,
          },
          {
            key: "Group1",
            group: "A",
          },
          {
            key: "Group2",
            group: "A",
          },
          {
            key: "Group6",
            group: "A",
          },
          {
            key: "Group3",
            group: "B",
          },
          {
            key: "Group4",
            group: "B",
          },
          {
            key: "Group5",
            group: "B",
          },
        ];

        const linkDataArray = [
          {
            from: "Group1",
            to: "Group2",
          },
          {
            from: "Group2",
            to: "Group6",
          },
          {
            from: "Group3",
            to: "Group4",
          },
          {
            from: "Group3",
            to: "Group5",
          },
        ];

        const model = new go.GraphLinksModel();
        model.nodeGroupKey = "group";
        model.nodeDataArray = nodeDataArray;
        model.linkDataArray = linkDataArray;
        myDiagram.layout.laneProperty = model.nodeGroupKey;
        myDiagram.layout.laneNames = ["A", "B"];
        myDiagram.model = model;
      }

      window.addEventListener("DOMContentLoaded", init);
    </script>
  </body>
</html>

Yes, Groups are just Nodes, so you can just add groups and their subgraphs as you do regular nodes.

If you are starting from the Swim Lanes sample, please note that there are two different categories of Groups in that implementation. There are Groups that act as the lanes, and there are Groups that act as pools (collections of lanes). You would need to add a third category that is neither lane nor pool.

In fact you might find it clearest if you changed the category/template name for the lane groups from “” (the empty string) to something like “Lane”, so that the groups that you want to have would use the default empty string name so that you don’t need to specify their category in the model data. If you want to do that, you’ll need to change:

      myDiagram.groupTemplate =
        $(go.Group, "Horizontal", groupStyle(),

to:

      myDiagram.groupTemplateMap.add("Lane",
        $(go.Group, "Horizontal", groupStyle(),

(and corresponding extra parenthesis after the template), and then change the model data declaring the lanes by adding this property to each one:

category: "Lane"

That way when you add your group data to the model, you just need to say isGroup: true and don’t need to specify a category.

In fact, maybe we should change the sample exactly that way, to make it easier for people to add groups to that sample (and perhaps to other samples that also didn’t think about adaptations that also wanted to use groups).

Thank you so much, Walter!