LayeredDigraph - Vertically aligned nodes in specific band

HI Team,
How to render the child nodes under specific parent node or specific band in vertical order as like below

If you’re referring to the “Sub lock” nodes and their parents, that is usually accomplished with a TreeLayout like in the Tree View sample. Maybe you want to treat those parents and children as subgraphs that use their own layout by making them members of a Group. GoJS SubGraphs -- Northwoods Software

Let me check

The Group need not be visible – just use a template like:

$(go.Group,
  {
     layout: $(go.TreeLayout, ...)
  },
  $(go.Placeholder)
)

Note that this template has no Shape or TextBlock or Picture in it, so nothing is drawn for the Group itself.

I have tried adding group template and below is the code for that

private setupGroupTemplate(): go.Group {
return $(go.Group, ‘Vertical’, { background: ‘yellow’ },
{
layout: $(go.TreeLayout,
{
alignment: go.TreeLayout.AlignmentBottomRightBus,
angle: 90,
layerSpacing: 90,
nodeSpacing: 30,
setsPortSpot: false,
})
},
$(go.Placeholder, { padding: 5 })
);
}

And my data is as below
image

It is not even rendering.

But it works with default layout on diagram and group template with tree layout.

With LayeredDigraph layout on diagram, it is not working. Could you please help me on this

Does each node data object have a “band” property that identifies which band it belongs in? Or does only the first node in a new band have that property, and we have to figure out the band for each node without that property?

Yes. Each object in nodeDataArray has band property, except object having isGroup

Normally, using bands with a Layout that produces layers like TreeLayout or LayeredDigraphLayout will assign exactly one band per layer. You clearly want to have potentially multiple layers within a band.

So I just tried to re-create your model, but simpler, since most things don’t matter to your question. Here’s my sample:

<!DOCTYPE html>
<html>
<head>
  <title>BandedTreeLayout</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
  <meta name="description" content="A TreeLayout that supports multiple layers of nodes within each band">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="sample">
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:1000px;"></div>
  </div>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">

// this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
const HORIZONTAL = false;  // this constant parameter can only be set here, not dynamically

// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make;

const myDiagram = $(go.Diagram, "myDiagramDiv",
  {
    initialAutoScale: go.Diagram.Uniform,
    initialDocumentSpot: go.Spot.Center,
    layout: $(go.TreeLayout,
      {
        compaction: go.TreeLayout.CompactionNone,
        layerStyle: go.TreeLayout.LayerUniform,
        treeStyle: go.TreeLayout.StyleLastParents,
        arrangement: HORIZONTAL ? go.TreeLayout.ArrangementVertical : go.TreeLayout.ArrangementHorizontal,
        // properties for most of tree
        angle: HORIZONTAL ? 0 : 90,
        alignment: go.TreeLayout.AlignmentStart,
        layerSpacing: 30,
        // properties for the "last parents"
        alternateAngle: HORIZONTAL ? 0 : 90,
        alternateAlignment: go.TreeLayout.AlignmentBottomRightBus,
        alternateLayerSpacing: 30,
      })
  });

myDiagram.linkTemplate =
  $(go.Link,
    { routing: go.Link.Orthogonal },
    $(go.Shape, { strokeWidth: 1.5 }));

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      width: 100, height: 60,
      fromSpot: HORIZONTAL ? go.Spot.Right : go.Spot.Bottom,
      toSpot: HORIZONTAL ? go.Spot.Left : go.Spot.Top,
      movable: false,
      resizable: false,
    },
    $(go.Shape, "RoundedRectangle",
      { fill: "white", strokeWidth: 1.5 }),
    $(go.Panel, "Vertical",
      { margin: 5 },
      $(go.TextBlock,  // don't bother showing any real information
        new go.Binding("text", "band")),
      $(go.TextBlock, { font: "8pt sans-serif" },
        new go.Binding("text", "key"))
    )
  );

myDiagram.model = new go.GraphLinksModel([
  { key: 0, band: "ZERO" },
  { key: 1, band: "ONE" },
  { key: 11, band: "ONE" },
  { key: 12, band: "ONE" },
  { key: 13, band: "ONE" },
  { key: 14, band: "ONE" },
  { key: 210, band: "TWO" },
  { key: 211, band: "TWO" },
  { key: 212, band: "TWO" },
  { key: 213, band: "TWO" },
  { key: 220, band: "TWO" },
  { key: 221, band: "TWO" },
  { key: 222, band: "TWO" },
  { key: 223, band: "TWO" },
], [
  { from: 0, to: 1 },
  { from: 1, to: 11 },
  { from: 1, to: 12 },
  { from: 1, to: 13 },
  { from: 13, to: 14 },
  { from: 11, to: 210 },
  { from: 210, to: 211 },
  { from: 210, to: 212 },
  { from: 210, to: 213 },
  { from: 12, to: 220 },
  { from: 220, to: 221 },
  { from: 220, to: 222 },
  { from: 220, to: 223 },
]);
  </script>
</body>
</html>

This produces:

The layout that you want will need to shift nodes apart vertically, to make sure that each node is in its declared band. Is that correct? So all of the “TWO” nodes would need to be shifted down so that none of them were at the same vertical level as any of the “ONE” nodes.

The other question I have is how the layout is supposed to decide whether to line up the children vertically rather than horizontally. Note the position of node 14 under node 13, because the rule that the sample’s TreeLayout is using is treeStyle: go.TreeLayout.StyleLastParents. In that case, node 13 has children but no grandchildren. But that rule works for nodes 210 and 220.

Oh, a third question: will there be any cases of skipping bands? For example, where a link would go from node 0 to a new node 500 which was declared to be in band “FIVE”, or in any band other than the next band, “ONE”.

I made some assumptions, including that a TreeLayout would be sufficient for your needs. If you need LayeredDigraphLayout because your graphs are not tree-structured, we can give you a version of that too.

If you want nodes to be laid out in a “tree view” arrangement, like GoJS Tree View, you need to use a group to hold those nodes. The group needs to specify the data.band, just like the regular node data. However the member nodes of the group don’t need any data.band property.

Here are the results:

Here’s the complete code:

<!DOCTYPE html>
<html>
<head>
  <title>BandedTreeLayout</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
  <meta name="description" content="A TreeLayout that supports multiple layers of nodes within each band">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="sample">
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:1000px;"></div>
  </div>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">

// NOTE: "Swimlane" layouts have the natural flow and growth of the layout go in the direction of the lanes.
// Nodes need to be annotated to indicate which lane they should be in.
// "Banded" layouts have the bands around layers of nodes that are laid out perpendicular to the natural flow.
// Some banded layouts do not require any annotations on nodes -- each node is placed in a particular
// band based on its dependencies.  There is a one-to-one relationship between bands and layers.
// Other banded layouts depend on node annotations to indicate which band they should be in.
// The BandedTreeLayout, below, requires all of the nodes to specify the "band" it should be in.
// It also requires a list of bands defining all of the bands and their order.

// This performs a TreeLayout where each node is assigned to a "band" beforehand.
// Each band may consist of multiple laid-out layers of nodes.
// TreeLayout.commitLayers is overridden to modify the background Part whose key is "_BANDS".
// It is assumed that the _BANDS's data object's "itemArray" property will hold an Array of Objects, each describing a band.
// This Array determines the order of the bands, not the precedence order given by the nodes and links
// combined with the "band" property on each node data object identifying which band the node must be in.
// The node data "band" value should match one of the band descriptor's "text" property in the _BANDS itemArray.
// If the "band" property is not present or if it names a non-existent band, there will be a console warning,
// and the layout results might look odd.
class BandedTreeLayout extends go.TreeLayout {
  constructor() {
    super();
    this.layerStyle = go.TreeLayout.LayerUniform;
  }

  makeNetwork(coll) {
    const net = super.makeNetwork(coll);
    this.assignLevels(net);
    return net;
  }

  assignLevels(net) {
    // find complete list of band names in the model
    const bands = this.diagram.findPartForKey("_BANDS");
    if (bands) {
      this._bandnames = new go.Map();  // map band name to band descriptor holding name and layer ranges
      const bandvertexes = new go.Map();  // map band name to Set of vertexes
      const banddescs = bands.data.itemArray;
      if (!Array.isArray(banddescs)) return;
      // setup Map of band name to band descriptor
      banddescs.forEach(banddesc => {
        this._bandnames.set(banddesc.text, banddesc);
        banddesc.depth = 1;  // pretend each band has at least one vertex in it
        bandvertexes.set(banddesc.text, new go.Set());  // a set of vertexes
      });
      // collect all vertexes for each band
      let it = net.vertexes.iterator;
      while (it.next()) {
        const v = it.value;
        if (v.node !== null && v.node.data !== null) {
          const band = v.node.data.band;
          if (!this._bandnames.get(band)) {
            console.log("BandedTreeLayout: unknown band name on node data: " + band + " on node of key: " + v.node.key);
          }
          if (!bandvertexes.get(band)) {
            bandvertexes.set(band, new go.Set());  // a set of vertexes
          }
          bandvertexes.get(band).add(v);
        }
      }
      // for each band, determine how many layers are needed by looking for longest
      // chain of vertexes with that band name
      const allbanddepths = new go.Map();  // map vertex to relative depth within band
      bandvertexes.each(kvp => {
        const name = kvp.key;
        const verts = kvp.value;
        const depths = new go.Map();  // map vertex to relative depth within band NAME
        verts.each(v => this.walkAllWithinBand(name, v, depths));
        let max = 0;
        depths.each(kvp => {
          const v = kvp.key;
          const blevel = kvp.value-1;
          max = Math.max(max, kvp.value);
          allbanddepths.set(v, kvp.value);
        });
        const desc = this._bandnames.get(name);
        if (desc) desc.depth = max = Math.max(desc.depth, max);
        // insert dummy vertexes where there's a layer gap between a vertex and its successor vertex(es)
        verts.each(v => {
          if (!v.node) return;
          const vlevel = depths.get(v);
          new go.List(v.destinationEdges).each(e => {
            if (!e.link) return;
            const w = e.toVertex;
            if (!w || !w.node) return;
            if (w.node.data.band !== name) {
              // while (vertex blevel < max) splice in a dummy vertex into this edge 
              for (let i = vlevel; i < max; i++) {
                const dummy = net.createVertex();
                net.addVertex(dummy);
                const dummyedge = net.linkVertexes(v, dummy, null);
                v = dummy;
              }
              if (e.sourceVertex !== v) {
                e.sourceVertex = v;
                v.addDestinationEdge(e);
              }
            }
          });
        });
      });
    }
  }

  walkAllWithinBand(name, v, depths) {
    if (depths.has(v)) return depths.get(v);
    let depth = 0;
    v.sourceVertexes.each(w => {
      if (!w.node || !w.node.data || w.node.data.band !== name) return; // ignore vertexes of different band
      depth = Math.max(depth, this.walkAllWithinBand(name, w, depths));
    });
    depths.set(v, depth + 1);
    return depth + 1;
  }

  commitLayers(layerRects, offset) {
    // update the background object holding the visual "bands"
    const bands = this.diagram.findPartForKey("_BANDS");
    if (bands) {
      const model = this.diagram.model;
      bands.location = this.arrangementOrigin.copy().add(offset);

      // set the bounds of each band via data binding of the "bounds" property
      const banddescs = bands.data.itemArray;
      let i = 0;
      let j = 0;  // index into layerRects
      for (; i < banddescs.length && j < layerRects.length; i++) {
        const banddesc = banddescs[i];
        let k = j;  // index into layerRects
        let thick = 0;
        for (; k < Math.min(j+banddesc.depth, layerRects.length); k++) {
          if (this.angle === 90 || this.angle === 270) {
            thick += layerRects[k].height;
          } else {
            thick += layerRects[k].width;
          }
        }
        const r = layerRects[j].copy();  // first rect gives shared bounds info
        if (this.angle === 90 || this.angle === 270) {
          if (this.angle === 270) r.y = layerRects[k-1].y;
          r.height = thick;
        } else {
          if (this.angle === 180) r.x = layerRects[k-1].x;
          r.width = thick;
        }
        model.set(banddesc, "visible", true);
        model.set(banddesc, "bounds", r); // an actual Rect, not stringified
        j = k;  // next layer/band
      }
      for (; i < banddescs.length; i++) {
        const banddesc = banddescs[i];
        model.set(banddesc, "visible", false);
      }
    }
    this._bandnames = null;  // cleanup
  }
} // end BandedTreeLayout


// this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
const HORIZONTAL = false;  // this constant parameter can only be set here, not dynamically

// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make;

myDiagram = $(go.Diagram, "myDiagramDiv",
  {
    initialAutoScale: go.Diagram.Uniform,
    initialDocumentSpot: go.Spot.Center,
    layout: $(BandedTreeLayout,
      {
        compaction: go.TreeLayout.CompactionNone,
        layerStyle: go.TreeLayout.LayerUniform,
        arrangement: HORIZONTAL ? go.TreeLayout.ArrangementVertical : go.TreeLayout.ArrangementHorizontal,
        // properties for most of tree
        angle: HORIZONTAL ? 0 : 90,
        alignment: go.TreeLayout.AlignmentStart,
        layerSpacing: 30,
      })
  });

myDiagram.linkTemplate =
  $(go.Link,
    { routing: go.Link.Orthogonal },
    $(go.Shape, { strokeWidth: 1.5 }));

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      width: 100, height: 60,
      fromSpot: HORIZONTAL ? go.Spot.Right : go.Spot.Bottom,
      toSpot: HORIZONTAL ? go.Spot.Left : go.Spot.Top,
      movable: false,
    },
    $(go.Shape, "RoundedRectangle",
      { fill: "white", strokeWidth: 1.5 }),
    $(go.Panel, "Vertical",
      { margin: 5 },
      $(go.TextBlock,  // don't bother showing any real information
        new go.Binding("text", "band")),
      $(go.TextBlock,
        new go.Binding("text", "key")),
    )
  );

myDiagram.groupTemplate =
  $(go.Group,
    {
      layout:
        $(go.TreeLayout,
          {
            angle: HORIZONTAL ? 0 : 90,
            alignment: go.TreeLayout.AlignmentBottomRightBus,
            layerSpacing: 30,
            rowSpacing: 30
          })
    },
    $(go.Placeholder)
  )

// There should be at most a single object of this category.
// This Part will be modified by commitLayers to display visual "bands"
// where each "layer" is a layer of the graph.
// This template is parameterized at load time by the HORIZONTAL parameter.
// You also have the option of showing rectangles for the layer bands or
// of showing separator lines between the layers, but not both at the same time,
// by commenting in/out the indicated code.
myDiagram.nodeTemplateMap.add("Bands",
  $(go.Part, "Position",
    new go.Binding("itemArray"),  // Array of band descriptor objects
    {
      isLayoutPositioned: false,  // but still in document bounds
      locationSpot: new go.Spot(0, 0, HORIZONTAL ? 0 : 146, HORIZONTAL ? 46 : 0),  // account for header height/width
      layerName: "Background",
      pickable: false,
      selectable: false,
      itemTemplate:
        $(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
          new go.Binding("visible"),
          new go.Binding("position", "bounds", b => b.position),
          $(go.Panel, "Auto",  // the header
            new go.Binding("desiredSize", "bounds", r => HORIZONTAL ? new go.Size(r.width, 46) : new go.Size(146, r.height)),
            $(go.Shape, { fill: "#AAAAAA", strokeWidth: 0 }),
            $(go.TextBlock,
              { font: "bold 14pt sans-serif", stroke: "white" },
              new go.Binding("text"))
          ),
          // option 1: rectangular bands:
          $(go.Shape,
            { strokeWidth: 0 },
            new go.Binding("desiredSize", "bounds", r => r.size),
            new go.Binding("fill", "itemIndex", i => i % 2 == 0 ? "#F8F8F8" : "#F0F0F0").ofObject())
          // option 2: separator lines:
          // (HORIZONTAL
          //  ? $(go.Shape, "LineV",
          //      { stroke: "gray", alignment: go.Spot.TopLeft, width: 1 },
          //      new go.Binding("height", "bounds", r => r.height),
          //      new go.Binding("visible", "itemIndex", i => i > 0).ofObject())
          //  : $(go.Shape, "LineH",
          //      { stroke: "gray", alignment: go.Spot.TopLeft, height: 1 },
          //      new go.Binding("width", "bounds", r => r.width),
          //      new go.Binding("visible", "itemIndex", i => i > 0).ofObject())
          // )
        )
    }
  ));

myDiagram.model = new go.GraphLinksModel([
  { // this is the information needed for the headers of the bands
    key: "_BANDS",
    category: "Bands",
    itemArray: [
      { text: "ZERO" },
      { text: "ONE" },
      { text: "TWO" },
      { text: "THREE" },
      { text: "FOUR" },
    ]
  },
  { key: 0, band: "ZERO" },
  { key: 1, band: "ONE" },
  { key: 11, band: "ONE" },
  { key: 12, band: "ONE" },

  // choice 1: vertical chain
  { key: 13, band: "ONE" },
  { key: 14, band: "ONE" },
  { key: 15, band: "ONE" },
  // choice 2: subtree in group
  // { key: -13, isGroup: true, band: "ONE" },
  // { key: 13, group: -13 },
  // { key: 14, group: -13 },
  // { key: 15, group: -13 },

  { key: -210, isGroup: true, band: "TWO" },
  { key: 210, group: -210 },
  { key: 211, group: -210 },
  { key: 212, group: -210 },
  { key: 213, group: -210 },

  { key: -220, isGroup: true, band: "TWO" },
  { key: 220, group: -220 },
  { key: 221, group: -220 },
  { key: 222, group: -220 },
  { key: 223, group: -220 },
  { key: 224, group: -220 },
], [
  { from: 0, to: 1 },
  { from: 1, to: 11 },
  { from: 1, to: 12 },
  { from: 1, to: 13 },
  { from: 13, to: 14 },
  { from: 14, to: 15 },

  { from: 11, to: 210 },
  { from: 210, to: 211 },
  { from: 210, to: 212 },
  { from: 210, to: 213 },

  { from: 12, to: 220 },
  { from: 220, to: 221 },
  { from: 220, to: 222 },
  { from: 220, to: 223 },
  { from: 220, to: 224 },
]);
  </script>
</body>
</html>

Ya above issue got resolved, after adding band data in group object. Thanks walter

One more question… My child nodes are rendering vertical to the parent node inside the group. But the child nodes are aligned straight to parent node. How to indent the node or vertically align the child nodes to the bottom right side of parent side
image

Below is my group Template code
image

Are all of those child nodes members of the same (unseen) group?

Hmmm, that yellow background suggests that you may have implemented yet another group around all of the children. That would mean all of those child nodes are not actually children of the “LG2” node, since they would be members of that yellow-background group. However, I am just speculating how you have organized the data model and thus the nodes and groups in the diagram.

Nodes inside the group are the child nodes of ‘LG2’. Those child nodes are having ‘group’ in their node data to identify the group it belongs to

And does the “LG2” node data also have a “group” property that is the same as its child nodes’ data?

‘LG2’ node does not have ‘group’ property. Below is my nodeDataArray and linkDataArray

const nodeDataArray = [
    {
        "key": "_BANDS",
        "category": "BANDS",
        "itemArray": [
            {
                "text": "CATALOG GROUP",
                "start": 0,
                "depth": 1,
                "bounds": {
                    "x": 0,
                    "y": 0,
                    "width": 825,
                    "height": 132.67108562161593,
                    "_isFrozen": false
                }
            },
            {
                "text": "INSTANCE GROUP",
                "start": 1,
                "depth": 2,
                "bounds": {
                    "x": 0,
                    "y": 132.67108562161593,
                    "width": 825,
                    "height": 304.62714030911593,
                    "_isFrozen": false
                }
            },
            {
                "text": "LAYOUT GROUP",
                "start": 3,
                "depth": 2,
                "bounds": {
                    "x": 0,
                    "y": 437.29822593073186,
                    "width": 825,
                    "height": 369.0620971824237,
                    "_isFrozen": false
                }
            }
        ]
    },
    {
        "key": "63e1c856527a508af4bbf7d0",
        "category": "CATALOG_GROUP",
        "band": "CATALOG GROUP",
        "parentId": "63e1c849527a508af4bbf68e",
        "errorMessage": "",
        "technologyMatrixId": "63e1c856527a508af4bbf7d0",
        "groupId": "63e1c855527a508af4bbf7cf",
        "actualName": "CG1",
        "name": "CG1"
    },
    {
        "key": "63e1c85d527a508af4bbf7d6",
        "category": "INSTANCE_GROUP",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c856527a508af4bbf7d0",
        "errorMessage": "",
        "technologyMatrixId": "63e1c85d527a508af4bbf7d6",
        "groupId": "63e1c85c527a508af4bbf7d3",
        "actualName": "IG1-CG1",
        "name": "IG1-CG1"
    },
    {
        "key": "63e1c89c527a508af4bbf928",
        "category": "INSTANCE_TECHNOLOGY",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c85d527a508af4bbf7d6",
        "errorMessage": "",
        "technologyMatrixId": "63e1c89c527a508af4bbf928",
        "technologyId": "637c6ed5fe29ba5aa564f5f3",
        "technologyName": "Coil Technology"
    },
    {
        "key": "63e1c89d527a508af4bbf930",
        "category": "INSTANCE_TECHNOLOGY",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c85d527a508af4bbf7d6",
        "errorMessage": "",
        "technologyMatrixId": "63e1c89d527a508af4bbf930",
        "technologyId": "639603245a6c0c62ed4c8653",
        "technologyName": "abc-tech-detail-new"
    },
    {
        "key": "63e1c89b527a508af4bbf921",
        "category": "INSTANCE_TECHNOLOGY",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c85d527a508af4bbf7d6",
        "errorMessage": "",
        "technologyMatrixId": "63e1c89b527a508af4bbf921",
        "technologyId": "6399bbeb6343d661a70ed256",
        "technologyName": "2-new"
    },
    {
        "key": "63e1c8ab527a508af4bbf940",
        "category": "LAYOUT_GROUP",
        "band": "LAYOUT GROUP",
        "parentId": "63e1c89b527a508af4bbf921",
        "errorMessage": "",
        "technologyMatrixId": "63e1c8ab527a508af4bbf940",
        "groupId": "63e1c8ab527a508af4bbf935",
        "actualName": "LG2",
        "name": "LG2",
        "isBuildLayoutSelected": true
    },
    {
        "key": "LG2",
        "isGroup": true,
        "band": "LAYOUT GROUP"
    },
    {
        "key": "63e1c8b5527a508af4bbf948",
        "category": "LAYOUT_TECHNOLOGY",
        "band": "LAYOUT GROUP",
        "parentId": "63e1c8ab527a508af4bbf940",
        "errorMessage": "",
        "technologyMatrixId": "63e1c8b5527a508af4bbf948",
        "technologyId": "63c4f1b9f40c80f56efc33ee",
        "technologyName": "layout technology3648",
        "group": "LG2"
    },
    {
        "key": "63e1c8b7527a508af4bbf951",
        "category": "LAYOUT_TECHNOLOGY",
        "band": "LAYOUT GROUP",
        "parentId": "63e1c8ab527a508af4bbf940",
        "errorMessage": "",
        "technologyMatrixId": "63e1c8b7527a508af4bbf951",
        "technologyId": "63d74ab04d0940f3e8684566",
        "technologyName": "layout technology4071",
        "group": "LG2"
    }
]


const linkDataArray = [
    {
        "from": "63e1c849527a508af4bbf68e",
        "to": "63e1c856527a508af4bbf7d0"
    },
    {
        "from": "63e1c856527a508af4bbf7d0",
        "to": "63e1c85d527a508af4bbf7d6"
    },
    {
        "from": "63e1c85d527a508af4bbf7d6",
        "to": "63e1c89c527a508af4bbf928"
    },
    {
        "from": "63e1c85d527a508af4bbf7d6",
        "to": "63e1c89d527a508af4bbf930"
    },
    {
        "from": "63e1c85d527a508af4bbf7d6",
        "to": "63e1c89b527a508af4bbf921"
    },
    {
        "from": "63e1c89b527a508af4bbf921",
        "to": "63e1c8ab527a508af4bbf940"
    },
    {
        "from": "63e1c8ab527a508af4bbf940",
        "to": "63e1c8b5527a508af4bbf948"
    },
    {
        "from": "63e1c8ab527a508af4bbf940",
        "to": "63e1c8b7527a508af4bbf951"
    }
]

That is the problem. The group’s layout needs both parent and children if you want it to lay out a tree.

I couldn’t understand. I just prepared nodeDataArray based on this example

The following sample app produces this diagram:


Ignoring the details of each node, is this not what you are expecting?

<!DOCTYPE html>
<html>
<head>
  <title>BandedTreeLayout</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
  <meta name="description" content="A TreeLayout that supports multiple layers of nodes within each band">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <div id="sample">
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:1000px;"></div>
  </div>

  <script src="../release/go.js"></script>
  <script id="code">

// NOTE: "Swimlane" layouts have the natural flow and growth of the layout go in the direction of the lanes.
// Nodes need to be annotated to indicate which lane they should be in.
// "Banded" layouts have the bands around layers of nodes that are laid out perpendicular to the natural flow.
// Some banded layouts do not require any annotations on nodes -- each node is placed in a particular
// band based on its dependencies.  There is a one-to-one relationship between bands and layers.
// Other banded layouts depend on node annotations to indicate which band they should be in.
// The BandedTreeLayout, below, requires all of the nodes to specify the "band" it should be in.
// It also requires a list of bands defining all of the bands and their order.

// This performs a TreeLayout where each node is assigned to a "band" beforehand.
// Each band may consist of multiple laid-out layers of nodes.
// TreeLayout.commitLayers is overridden to modify the background Part whose key is "_BANDS".
// It is assumed that the _BANDS's data object's "itemArray" property will hold an Array of Objects, each describing a band.
// This Array determines the order of the bands, not the precedence order given by the nodes and links
// combined with the "band" property on each node data object identifying which band the node must be in.
// The node data "band" value should match one of the band descriptor's "text" property in the _BANDS itemArray.
// If the "band" property is not present or if it names a non-existent band, there will be a console warning,
// and the layout results might look odd.
class BandedTreeLayout extends go.TreeLayout {
  constructor() {
    super();
    this.layerStyle = go.TreeLayout.LayerUniform;
  }

  makeNetwork(coll) {
    const net = super.makeNetwork(coll);
    this.assignLevels(net);
    return net;
  }

  assignLevels(net) {
    // find complete list of band names in the model
    const bands = this.diagram.findPartForKey("_BANDS");
    if (bands) {
      this._bandnames = new go.Map();  // map band name to band descriptor holding name and layer ranges
      const bandvertexes = new go.Map();  // map band name to Set of vertexes
      const banddescs = bands.data.itemArray;
      if (!Array.isArray(banddescs)) return;
      // setup Map of band name to band descriptor
      banddescs.forEach(banddesc => {
        this._bandnames.set(banddesc.text, banddesc);
        banddesc.depth = 1;  // pretend each band has at least one vertex in it
        bandvertexes.set(banddesc.text, new go.Set());  // a set of vertexes
      });
      // collect all vertexes for each band
      let it = net.vertexes.iterator;
      while (it.next()) {
        const v = it.value;
        if (v.node !== null && v.node.data !== null) {
          const band = v.node.data.band;
          if (!this._bandnames.get(band)) {
            console.log("BandedTreeLayout: unknown band name on node data: " + band + " on node of key: " + v.node.key);
          }
          if (!bandvertexes.get(band)) {
            bandvertexes.set(band, new go.Set());  // a set of vertexes
          }
          bandvertexes.get(band).add(v);
        }
      }
      // for each band, determine how many layers are needed by looking for longest
      // chain of vertexes with that band name
      const allbanddepths = new go.Map();  // map vertex to relative depth within band
      bandvertexes.each(kvp => {
        const name = kvp.key;
        const verts = kvp.value;
        const depths = new go.Map();  // map vertex to relative depth within band NAME
        verts.each(v => this.walkAllWithinBand(name, v, depths));
        let max = 0;
        depths.each(kvp => {
          const v = kvp.key;
          const blevel = kvp.value-1;
          max = Math.max(max, kvp.value);
          allbanddepths.set(v, kvp.value);
        });
        const desc = this._bandnames.get(name);
        if (desc) desc.depth = max = Math.max(desc.depth, max);
        // insert dummy vertexes where there's a layer gap between a vertex and its successor vertex(es)
        verts.each(v => {
          if (!v.node) return;
          const vlevel = depths.get(v);
          new go.List(v.destinationEdges).each(e => {
            if (!e.link) return;
            const w = e.toVertex;
            if (!w || !w.node) return;
            if (w.node.data.band !== name) {
              // while (vertex blevel < max) splice in a dummy vertex into this edge 
              for (let i = vlevel; i < max; i++) {
                const dummy = net.createVertex();
                net.addVertex(dummy);
                const dummyedge = net.linkVertexes(v, dummy, null);
                v = dummy;
              }
              if (e.sourceVertex !== v) {
                e.sourceVertex = v;
                v.addDestinationEdge(e);
              }
            }
          });
        });
      });
    }
  }

  walkAllWithinBand(name, v, depths) {
    if (depths.has(v)) return depths.get(v);
    let depth = 0;
    v.sourceVertexes.each(w => {
      if (!w.node || !w.node.data || w.node.data.band !== name) return; // ignore vertexes of different band
      depth = Math.max(depth, this.walkAllWithinBand(name, w, depths));
    });
    depths.set(v, depth + 1);
    return depth + 1;
  }

  commitLayers(layerRects, offset) {
    // update the background object holding the visual "bands"
    const bands = this.diagram.findPartForKey("_BANDS");
    if (bands) {
      const model = this.diagram.model;
      bands.location = this.arrangementOrigin.copy().add(offset);

      // set the bounds of each band via data binding of the "bounds" property
      const banddescs = bands.data.itemArray;
      let i = 0;
      let j = 0;  // index into layerRects
      for (; i < banddescs.length && j < layerRects.length; i++) {
        const banddesc = banddescs[i];
        let k = j;  // index into layerRects
        let thick = 0;
        for (; k < Math.min(j+banddesc.depth, layerRects.length); k++) {
          if (this.angle === 90 || this.angle === 270) {
            thick += layerRects[k].height;
          } else {
            thick += layerRects[k].width;
          }
        }
        const r = layerRects[j].copy();  // first rect gives shared bounds info
        if (this.angle === 90 || this.angle === 270) {
          if (this.angle === 270) r.y = layerRects[k-1].y;
          r.height = thick;
        } else {
          if (this.angle === 180) r.x = layerRects[k-1].x;
          r.width = thick;
        }
        model.set(banddesc, "visible", true);
        model.set(banddesc, "bounds", r); // an actual Rect, not stringified
        j = k;  // next layer/band
      }
      for (; i < banddescs.length; i++) {
        const banddesc = banddescs[i];
        model.set(banddesc, "visible", false);
      }
    }
    this._bandnames = null;  // cleanup
  }
} // end BandedTreeLayout


// this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
const HORIZONTAL = false;  // this constant parameter can only be set here, not dynamically

// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make;

myDiagram = $(go.Diagram, "myDiagramDiv",
  {
    initialAutoScale: go.Diagram.Uniform,
    initialDocumentSpot: go.Spot.Center,
    layout: $(BandedTreeLayout,
      {
        compaction: go.TreeLayout.CompactionNone,
        layerStyle: go.TreeLayout.LayerUniform,
        arrangement: HORIZONTAL ? go.TreeLayout.ArrangementVertical : go.TreeLayout.ArrangementHorizontal,
        // properties for most of tree
        angle: HORIZONTAL ? 0 : 90,
        alignment: go.TreeLayout.AlignmentStart,
        layerSpacing: 30,
      })
  });

myDiagram.linkTemplate =
  $(go.Link,
    { routing: go.Link.Orthogonal },
    $(go.Shape, { strokeWidth: 1.5 }));

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      width: 100, height: 60,
      fromSpot: HORIZONTAL ? go.Spot.Right : go.Spot.Bottom,
      toSpot: HORIZONTAL ? go.Spot.Left : go.Spot.Top,
      movable: false,
    },
    $(go.Shape, "RoundedRectangle",
      { fill: "white", strokeWidth: 1.5 }),
    $(go.Panel, "Vertical",
      { margin: 5 },
      $(go.TextBlock,  // don't bother showing any real information
        new go.Binding("text", "band")),
      $(go.TextBlock,
        new go.Binding("text", "key")),
    )
  );

myDiagram.groupTemplate =
  $(go.Group,
    {
      layout:
        $(go.TreeLayout,
          {
            angle: HORIZONTAL ? 0 : 90,
            alignment: go.TreeLayout.AlignmentBottomRightBus,
            layerSpacing: 30,
            rowSpacing: 30
          })
    },
    $(go.Placeholder)
  )

// There should be at most a single object of this category.
// This Part will be modified by commitLayers to display visual "bands"
// where each "layer" is a layer of the graph.
// This template is parameterized at load time by the HORIZONTAL parameter.
// You also have the option of showing rectangles for the layer bands or
// of showing separator lines between the layers, but not both at the same time,
// by commenting in/out the indicated code.
myDiagram.nodeTemplateMap.add("Bands",
  $(go.Part, "Position",
    new go.Binding("itemArray"),  // Array of band descriptor objects
    {
      isLayoutPositioned: false,  // but still in document bounds
      locationSpot: new go.Spot(0, 0, HORIZONTAL ? 0 : 146, HORIZONTAL ? 46 : 0),  // account for header height/width
      layerName: "Background",
      pickable: false,
      selectable: false,
      itemTemplate:
        $(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
          new go.Binding("visible"),
          new go.Binding("position", "bounds", b => b.position),
          $(go.Panel, "Auto",  // the header
            new go.Binding("desiredSize", "bounds", r => HORIZONTAL ? new go.Size(r.width, 46) : new go.Size(146, r.height)),
            $(go.Shape, { fill: "#AAAAAA", strokeWidth: 0 }),
            $(go.TextBlock,
              { font: "bold 14pt sans-serif", stroke: "white" },
              new go.Binding("text"))
          ),
          // option 1: rectangular bands:
          $(go.Shape,
            { strokeWidth: 0 },
            new go.Binding("desiredSize", "bounds", r => r.size),
            new go.Binding("fill", "itemIndex", i => i % 2 == 0 ? "#F8F8F8" : "#F0F0F0").ofObject())
          // option 2: separator lines:
          // (HORIZONTAL
          //  ? $(go.Shape, "LineV",
          //      { stroke: "gray", alignment: go.Spot.TopLeft, width: 1 },
          //      new go.Binding("height", "bounds", r => r.height),
          //      new go.Binding("visible", "itemIndex", i => i > 0).ofObject())
          //  : $(go.Shape, "LineH",
          //      { stroke: "gray", alignment: go.Spot.TopLeft, height: 1 },
          //      new go.Binding("width", "bounds", r => r.width),
          //      new go.Binding("visible", "itemIndex", i => i > 0).ofObject())
          // )
        )
    }
  ));

  const nodeDataArray = [
    {
        "key": "_BANDS",
        "category": "Bands",
        "itemArray": [
            {
                "text": "CATALOG GROUP",
                "start": 0,
                "depth": 1,
                "bounds": {
                    "x": 0,
                    "y": 0,
                    "width": 825,
                    "height": 132.67108562161593,
                    "_isFrozen": false
                }
            },
            {
                "text": "INSTANCE GROUP",
                "start": 1,
                "depth": 2,
                "bounds": {
                    "x": 0,
                    "y": 132.67108562161593,
                    "width": 825,
                    "height": 304.62714030911593,
                    "_isFrozen": false
                }
            },
            {
                "text": "LAYOUT GROUP",
                "start": 3,
                "depth": 2,
                "bounds": {
                    "x": 0,
                    "y": 437.29822593073186,
                    "width": 825,
                    "height": 369.0620971824237,
                    "_isFrozen": false
                }
            }
        ]
    },
    {
        "key": "63e1c856527a508af4bbf7d0",
        "category": "CATALOG_GROUP",
        "band": "CATALOG GROUP",
        "parentId": "63e1c849527a508af4bbf68e",
        "errorMessage": "",
        "technologyMatrixId": "63e1c856527a508af4bbf7d0",
        "groupId": "63e1c855527a508af4bbf7cf",
        "actualName": "CG1",
        "name": "CG1"
    },
    {
        "key": "63e1c85d527a508af4bbf7d6",
        "category": "INSTANCE_GROUP",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c856527a508af4bbf7d0",
        "errorMessage": "",
        "technologyMatrixId": "63e1c85d527a508af4bbf7d6",
        "groupId": "63e1c85c527a508af4bbf7d3",
        "actualName": "IG1-CG1",
        "name": "IG1-CG1"
    },
    {
        "key": "63e1c89c527a508af4bbf928",
        "category": "INSTANCE_TECHNOLOGY",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c85d527a508af4bbf7d6",
        "errorMessage": "",
        "technologyMatrixId": "63e1c89c527a508af4bbf928",
        "technologyId": "637c6ed5fe29ba5aa564f5f3",
        "technologyName": "Coil Technology"
    },
    {
        "key": "63e1c89d527a508af4bbf930",
        "category": "INSTANCE_TECHNOLOGY",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c85d527a508af4bbf7d6",
        "errorMessage": "",
        "technologyMatrixId": "63e1c89d527a508af4bbf930",
        "technologyId": "639603245a6c0c62ed4c8653",
        "technologyName": "abc-tech-detail-new"
    },
    {
        "key": "63e1c89b527a508af4bbf921",
        "category": "INSTANCE_TECHNOLOGY",
        "band": "INSTANCE GROUP",
        "parentId": "63e1c85d527a508af4bbf7d6",
        "errorMessage": "",
        "technologyMatrixId": "63e1c89b527a508af4bbf921",
        "technologyId": "6399bbeb6343d661a70ed256",
        "technologyName": "2-new"
    },
    {
        "key": "63e1c8ab527a508af4bbf940",
        "category": "LAYOUT_GROUP",
        "band": "LAYOUT GROUP",
        "parentId": "63e1c89b527a508af4bbf921",
        "errorMessage": "",
        "technologyMatrixId": "63e1c8ab527a508af4bbf940",
        "groupId": "63e1c8ab527a508af4bbf935",
        "actualName": "LG2",
        "name": "LG2",
        "isBuildLayoutSelected": true,
        "group": "LG2"
    },
    {
        "key": "LG2",
        "isGroup": true,
        "band": "LAYOUT GROUP"
    },
    {
        "key": "63e1c8b5527a508af4bbf948",
        "category": "LAYOUT_TECHNOLOGY",
        "band": "LAYOUT GROUP",
        "parentId": "63e1c8ab527a508af4bbf940",
        "errorMessage": "",
        "technologyMatrixId": "63e1c8b5527a508af4bbf948",
        "technologyId": "63c4f1b9f40c80f56efc33ee",
        "technologyName": "layout technology3648",
        "group": "LG2"
    },
    {
        "key": "63e1c8b7527a508af4bbf951",
        "category": "LAYOUT_TECHNOLOGY",
        "band": "LAYOUT GROUP",
        "parentId": "63e1c8ab527a508af4bbf940",
        "errorMessage": "",
        "technologyMatrixId": "63e1c8b7527a508af4bbf951",
        "technologyId": "63d74ab04d0940f3e8684566",
        "technologyName": "layout technology4071",
        "group": "LG2"
    }
]

const linkDataArray = [
    {
        "from": "63e1c849527a508af4bbf68e",
        "to": "63e1c856527a508af4bbf7d0"
    },
    {
        "from": "63e1c856527a508af4bbf7d0",
        "to": "63e1c85d527a508af4bbf7d6"
    },
    {
        "from": "63e1c85d527a508af4bbf7d6",
        "to": "63e1c89c527a508af4bbf928"
    },
    {
        "from": "63e1c85d527a508af4bbf7d6",
        "to": "63e1c89d527a508af4bbf930"
    },
    {
        "from": "63e1c85d527a508af4bbf7d6",
        "to": "63e1c89b527a508af4bbf921"
    },
    {
        "from": "63e1c89b527a508af4bbf921",
        "to": "63e1c8ab527a508af4bbf940"
    },
    {
        "from": "63e1c8ab527a508af4bbf940",
        "to": "63e1c8b5527a508af4bbf948"
    },
    {
        "from": "63e1c8ab527a508af4bbf940",
        "to": "63e1c8b7527a508af4bbf951"
    }
]

myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
  </script>
</body>
</html>

After adding ‘group’ to parent node data, Indentation is working. Thanks walter.