How to create a grouped template with rectangle having different types of nodes

Hello, I wanted to create the diagram below with different custom templates with different nodes. The sample diagram will look like this… The point is, how do I prepare a node data having labels above and below the rectangle?

The easiest solution would be to define your node template so that it is a “Vertical” Panel holding a TextBlock and a “RoundedRectangle” Shape. (Or maybe there should be other things along with that Shape in your simplified sketch, so that second thing would be some sort of Panel that includes a “RoundedRectangle” Shape.)

Then you could add a Binding to the Panel.isOpposite property based on whether the node is on the bottom instead of the top. You’ll need to decide what to do when there are more than two rows of nodes.

Are those intermediate rounded-rectangle shapes Groups? If so, and if you want those text labels to be outside of the rounded-rectangle Shape, and if the Group is defined such that you have an “Auto” Panel so that the “RoundedRectangle” Shape surrounds the group’s Placeholder, then you should have a negative top and bottom Margin as the Placeholder.margin, to force the top and bottom of the Shape to overlap with the tops of the top row of Nodes and the bottoms of the bottom row of Nodes.

Here is what I’ve done so far and Still the labels within the rectangle. Please check my implementation and suggest any changes needed to achieve the original spec share in earlier comments.

private initGroupDigram() {
    const $ = go.GraphObject.make;
    
    this.myDiagram = $(go.Diagram, this.diagramDiv.nativeElement, {
      layout: new TableLayout(),
      "SelectionMoved": this.relayoutDiagram.bind(this),
      initialContentAlignment: go.Spot.Top
    });

    this.myDiagram.routers.add(new AvoidsLinksRouter());
    // Define a simple node template
    // Node template
      this.myDiagram.nodeTemplate =
        new go.Node("Auto", {
          selectable: false,
          avoidableMargin: new go.Margin(2),
        })
          .add(
            $(go.Panel, 'Vertical', {
              portId: '',
              cursor: 'pointer',
              background: 'pink',
              // desiredSize: new go.Size(30, 28),
            }),
            $(go.TextBlock, {
              font: '7pt sophos-icon',
              alignment: go.Spot.Top,
              margin: new go.Margin(0, 0, 10, 0),
              // stroke: 'white'
            })
            .bind('text', 'portName'),
            $(go.TextBlock, {
              font: '21pt sophos-icon',
              text: '\ue9ef',
              angle: 180,
              alignment: go.Spot.Top,
            })
          );

      // Group template (default style)
      this.myDiagram.groupTemplate =
      new go.Group("Auto", {
        selectable: false,
        width: 1200,
        avoidable: false,
        height: NaN,
        background: 'khaki',
        alignment: go.Spot.Center,
        layout: new go.GridLayout({
          wrappingColumn: 1,
          comparer: (a, b) => {
            // can re-order tasks within a lane
            const ay = a.location.y;
            const by = b.location.y;
            if (isNaN(ay) || isNaN(by)) return 0;
            if (ay < by) return -1;
            if (ay > by) return 1;
            return 0;
          }
        })
      })
      const twoColumnGroupTemplate =
        new go.Group("Auto", {
          layout: new go.GridLayout({
            wrappingColumn: 3
          })
        })
        .add(
          new go.Shape("Rectangle", { fill: "white", height:140, parameter1: -100 }),
          $(go.Panel, "Auto")
          .add(
            $(go.Placeholder, { padding: 2, minSize: new go.Size(1200, NaN) }), // ensures member nodes are centered
            // new go.Shape("Rectangle", { fill: "lightyellow", alignment: go.Spot.Right, minSize: new go.Size(300, 110)}),
            $(go.Panel, "Table", {
              minSize: new go.Size(300, 110),
              alignment: go.Spot.Right,
              itemTemplate:
                $(go.Panel, "TableRow",
                  $(go.TextBlock,
                  {
                    toolTip: $(go.Adornment, "Auto",
                      $(go.Shape, { fill: "#FFFFCC" }),
                      $(go.TextBlock, { margin: 4 },
                        new go.Binding("text", "tooltipText"),
                        new go.Binding("visible", "tooltipText", (t) => {
                          console.log('value of tooltipText ', t);
                          return !!t;
                        }) // Only show if text exists
                      )
                    ),
                    column: 0, alignment: go.Spot.Left, font: "bold 10pt sans-serif"
                  },
                  new go.Binding("text", "name")
                  ),
                  $(go.TextBlock, { column: 1, alignment: go.Spot.Left},
                    new go.Binding("text", "value")
                  ),
                ) // end itemTemplate
              },
              new go.Binding('itemArray', 'switchInfo'),
            )
            .add(
              new go.TextBlock("More info", { alignment: go.Spot.Left, font: 'bold 10pt Inter'}),
            )
          ),
          // new go.TextBlock({ font: "Bold 14pt Sans-Serif" }).bind("text")
        );
      this.myDiagram.groupTemplateMap.add("SwitchContainer", twoColumnGroupTemplate);
      this.myDiagram.groupTemplateMap.add("PortGroupTemplate",
        new go.Group("Auto", {
          selectable: false,
          layout: new go.GridLayout({
            wrappingColumn: 4,
            // cellSize: new go.Size(1, 1),
            spacing: new go.Size(2, 2)
          })
        })
        .add(
          new go.Shape("Rectangle", { fill: "grey" }),
          $(go.Placeholder, { padding: 5, alignment: go.Spot.Center }), // ensures member nodes are centered
          // new go.TextBlock({ font: "Bold 14pt Sans-Serif" }).bind("text")
        )
      );
    const nodeDataArray = [
      { key: 1, text: "Stack-A", portName: '', isGroup: true},
      { key: 2, text: "Switch-1", portName: '', isGroup: true, group: 1, category: "SwitchContainer",
        switchInfo: [
          { name: 'Serial', value: 'W13001QWV2PWV0D'},
          { name: 'Model', value: 'Some Model'},
          { name: 'State', value: 'Disconnected from last 3 days'},
          { name: 'Mac Address', value: 'ca:02:ca:12:12:dd'},
          { name: 'IP Address', value: '123.122.12.122'},
          { name: 'Firmware Version', value: '0.2.02332'},
          { name: 'More info', value: '', tooltipText: 'This will be shown in tooltip', },
        ]
       },
      { key: 3, text: "Switch-2", portName: '', isGroup: true, group: 1, category: "SwitchContainer",
        switchInfo: [
          { name: 'Serial', value: 'W13001QWV2PWV0D'},
          { name: 'Model', value: 'Some Model'},
          { name: 'State', value: 'Disconnected from last 3 days'},
          { name: 'Mac Address', value: 'ca:02:ca:12:12:dd'},
          { name: 'IP Address', value: '123.122.12.122'},
          { name: 'Firmware Version', value: '0.2.02332'},
        ]
      },
      { key: 4, text: "PortGroup-1", isGroup: true, group: 2, category: "PortGroupTemplate" },
      { key: 5, text: "Epsilon-1",  group: 4, portName: '1' },
      { key: 6, text: "Epsilon-2",  group: 4, portName: '2' },
      { key: 7, text: "Epsilon-1",  group: 4, portName: '3' },
      { key: 8, text: "Epsilon-1",  group: 4, portName: '4' },
      { key: 9, text: "Epsilon-1",  group: 4, portName: '5' },
      { key: 10, text: "Epsilon-1", group: 4, portName: '6' },
      { key: 11, text: "Epsilon-1", group: 4, portName: '7' },
      { key: 12, text: "Epsilon-1", group: 4, portName: '8' },
      { key: 13, text: "PortGroup-2", isGroup: true, group: 2, category: "PortGroupTemplate" },
      { key: 14, text: "Epsilon-1", group: 13, portName: '9' },
      { key: 15, text: "Epsilon-2", group: 13, portName: '10' },
      { key: 16, text: "Epsilon-1", group: 13, portName: '11' },
      { key: 17, text: "Epsilon-1", group: 13, portName: '12' },
      { key: 18, text: "Epsilon-1", group: 13, portName: '13' },
      { key: 19, text: "Epsilon-1", group: 13, portName: '14' },
      { key: 20, text: "Epsilon-1", group: 13, portName: '15' },
      { key: 21, text: "Epsilon-1", group: 13, portName: '16' },
      { key: 22, text: "PortGroup-1", isGroup: true, group: 2, category: "PortGroupTemplate" },
      { key: 23, text: "Epsilon-3", portName: '17', group: 22},
      { key: 24, text: "Epsilon-3", portName: '18', group: 22},
      { key: 25, text: "Epsilon-3", portName: '19', group: 22},
      { key: 26, text: "Epsilon-3", portName: '20', group: 22},
      { key: 27, text: "PortGroup-1", isGroup: true, group: 3, category: "PortGroupTemplate" },
      { key: 28, text: "Epsilon-1",  group: 27, portName: '1' },
      { key: 29, text: "Epsilon-2",  group: 27, portName: '2' },
      { key: 30, text: "Epsilon-1",  group: 27, portName: '3' },
      { key: 31, text: "Epsilon-1",  group: 27, portName: '5' },
      { key: 32, text: "Epsilon-1",  group: 27, portName: '6' },
      { key: 33, text: "Epsilon-1",  group: 27, portName: '7' },
      { key: 34, text: "Epsilon-1",  group: 27, portName: '8' },
      { key: 35, text: "PortGroup-2", isGroup: true, group: 3, category: "PortGroupTemplate" },
      { key: 36, text: "Epsilon-1", group: 35, portName: '9' },
      { key: 37, text: "Epsilon-2", group: 35, portName: '10' },
      { key: 38, text: "Epsilon-1", group: 35, portName: '11' },
      { key: 39, text: "Epsilon-1", group: 35, portName: '12' },
      { key: 40, text: "Epsilon-1", group: 35, portName: '13' },
      { key: 41, text: "Epsilon-1", group: 35, portName: '14' },
      { key: 42, text: "Epsilon-1", group: 35, portName: '15' },
      { key: 43, text: "Epsilon-1", group: 35, portName: '16' },
      { key: 44, text: "PortGroup-1", isGroup: true, group: 3, category: "PortGroupTemplate" },
      { key: 45, text: "Epsilon-3", portName: '17', group: 44},
      { key: 46, text: "Epsilon-3", portName: '18', group: 44},
      { key: 47, text: "Epsilon-3", portName: '19', group: 44},
      { key: 48, text: "Epsilon-3", portName: '20', group: 44},
      { key: 49, text: "Switch-3", portName: '', isGroup: true, group: 1, category: "SwitchContainer",
        switchInfo: [
          { name: 'Serial', value: 'W13001QWV2PWV0D'},
          { name: 'Model', value: 'Some Model'},
          { name: 'State', value: 'Disconnected from last 3 days'},
          { name: 'Mac Address', value: 'ca:02:ca:12:12:dd'},
          { name: 'IP Address', value: '123.122.12.122'},
          { name: 'Firmware Version', value: '0.2.02332'},
        ]
      }, // Switch Container Template
      { key: 52, text: "PortGroup-1", isGroup: true, group: 49, category: "PortGroupTemplate" },
      { key: 53, text: "Epsilon-1",  group: 52, portName: '1' },
      { key: 54, text: "Epsilon-2",  group: 52, portName: '2' },
      { key: 55, text: "Epsilon-1",  group: 52, portName: '3' },
      { key: 56, text: "Epsilon-1",  group: 52, portName: '6' },
      { key: 57, text: "Epsilon-1",  group: 52, portName: '7' },
      { key: 58, text: "Epsilon-1",  group: 52, portName: '8' },
      { key: 59, text: "PortGroup-2", isGroup: true, group: 49, category: "PortGroupTemplate" },
      { key: 60, text: "Epsilon-1", group: 59, portName: '9' },
      { key: 61, text: "Epsilon-2", group: 59, portName: '10' },
      { key: 62, text: "Epsilon-1", group: 59, portName: '11' },
      { key: 63, text: "Epsilon-1", group: 59, portName: '12' },
      { key: 64, text: "Epsilon-1", group: 59, portName: '13' },
      { key: 65, text: "Epsilon-1", group: 59, portName: '14' },
      { key: 66, text: "Epsilon-1", group: 59, portName: '15' },
      { key: 67, text: "Epsilon-1", group: 59, portName: '16' },
      { key: 68, text: "Switch-3", portName: '', isGroup: true, group: 1, category: "SwitchContainer" }, // Switch Container Template
      { key: 69, text: "PortGroup-2", isGroup: true, group: 68, category: "PortGroupTemplate" },
      { key: 70, text: "Epsilon-1", group: 69, portName: '9' },
      { key: 71, text: "Epsilon-2", group: 69, portName: '10' },
      { key: 72, text: "Epsilon-1", group: 69, portName: '11' },
      { key: 73, text: "Epsilon-1", group: 69, portName: '12' },
      { key: 74, text: "Epsilon-1", group: 69, portName: '13' },
      { key: 75, text: "Epsilon-1", group: 69, portName: '14' },
      { key: 76, text: "Epsilon-1", group: 69, portName: '15' },
      { key: 77, text: "Epsilon-1", group: 69, portName: '16' },
    ];
    // Get structural data
    this.myDiagram.model = $(go.GraphLinksModel, {
      nodeDataArray: nodeDataArray,
      linkDataArray: [
        { from: 5, to: 77 },
        { from: 10, to: 30 },
        { from: 6, to: 54 },
        { from: 8, to: 57 }
      ]
    });
    this.myDiagram.linkTemplate = 
      $(go.Link, {
          routing: go.Routing.AvoidsNodes,
          corner: 5, 
        })
        .bindTwoWay("points") 
        .add( 
          $(go.Shape, { stroke: '#005bc8', strokeWidth: 2 }) 
          .bind("stroke", "isSelected", function(isSelected) {
            return isSelected ? "#005bc8" : "#ffd02c"; // Highlight selected link in blue
          }),
          $(go.Shape, { stroke: '#005bc8', toArrow: "OpenTriangle", strokeWidth: 2 })
            .bind("stroke", "color")
        );
  }

Your code isn’t following my suggestion. Above, I was suggesting this kind of structure for your node template:

Node, "Vertical"
    TextBlock
    Panel, "Auto"
        Shape
        Picture or Shape of Icon

After adding the -ve top margin to the rectangle shape I’m able to see the number labels showing outside the box. But I still see them showing it top row only.

this.myDiagram.groupTemplateMap.add("PortGroupTemplate",
        new go.Group("Auto", {
          selectable: false,
          layout: new go.GridLayout({
            wrappingColumn: 4,
            // cellSize: new go.Size(1, 1),
            spacing: new go.Size(2, 2)
          })
        })
        .add(
          new go.TextBlock({ font: "10pt Sans-Serif" }).bind("text"),
          $(go.Panel, 'Auto')
          .add(
            $(go.Shape, "Rectangle", { fill: "grey", margin: new go.Margin(-32, 0, 0, 0) }),
            $(go.Placeholder, { padding: 5, alignment: go.Spot.Center }), // ensures member nodes are centered
          )
          // new go.TextBlock({ font: "Bold 14pt Sans-Serif" }).bind("text")
        )
      );

You need a Binding on the “Vertical” Panel’s “isOpposite” property so that the Panel with the icon is on top and the TextBlock label is on the bottom, when you want them that way. I’m not sure how you compute that given your screenshot.

Also the gray background shape seems too tall, even if the bottom row’s TextBlock was indeed below the icon.

Walter, I did add the Binding on the Vertical Panel, after that I’m able to get the labels displayed on top/bottom of the rectangle. Now the challenge is the third group (17-20), the rectangle needs to be aligned bottom instead of the top. How can I do that?

These set of rectangles I’m adding w/ groupTemplateMap

this.myDiagram.groupTemplateMap.add("FourByOneTemplate",
        new go.Group("Auto", {
          selectable: false,
          layout: new go.GridLayout({
            wrappingColumn: 4,
            cellSize: new go.Size(1, 1),
            spacing: new go.Size(2, 2)
          })
        })
        .add(
          $(go.Panel, 'Auto', {})
          .add(
            $(go.Shape, "Rectangle", { fill: "#DADCE0", margin: new go.Margin(0, 0, 0, 0), desiredSize: new go.Size(140, 40)}),
            $(go.Placeholder, { padding: 5, alignment: go.Spot.Bottom }), // ensures member nodes are centered
          )
        )
      );

If you are going to have Links with AvoidsNodes routing connecting any of those Nodes, you’ll probably want to increase the spacing, as we discussed previously.

What is the Group.layout of those green groups?

I agree I will provide extra spacing as we discussed earlier. The green Group layout looks like this

const twoColumnGroupTemplate =
        new go.Group("Auto", {
          width: 1300,
          layout: new go.GridLayout({
            // wrappingColumn: 3, // We don't need this
          })
        })
        .add(
          new go.Shape("Rectangle", { fill: "lightgreen", height:140, parameter1: 1 }),
          $(go.Panel, "Auto")
          .add(
            $(go.Placeholder, { padding: 2, minSize: new go.Size(1200, 110) }), // ensures member nodes are centered
          ),
      );
      this.myDiagram.groupTemplateMap.add("SwitchContainer", twoColumnGroupTemplate);

There’s never a reason to have an “Auto” Panel with only one element in it.

Try setting the inner group’s Group.locationSpot to go.Spot.BottomLeft. That way the outer group’s GridLayout will align the parts in each row according to their locations that will be at the bottom of their Placeholders.

The reason for having the “Auto” Panel is, we have some other content to be rendered on the right side of these rectangles.


I did tried setting the inner groups for the Group.locationSpot as BottomLeft but still it show up the top.

const twoColumnGroupTemplate =
        new go.Group("Auto", {
          width: 1300,
          locationSpot: go.Spot.BottomLeft,
          layout: new go.GridLayout({
            // wrappingColumn: 3, // We don't need this
          })
        })
        .add(
          new go.Shape("Rectangle", { fill: "lightgreen", height:140, parameter1: 1 }),
          $(go.Panel, "Auto")
          .add(
            $(go.Placeholder, { padding: 2, minSize: new go.Size(1200, NaN) }), // ensures member nodes are centered
            $(go.Panel, "Table", {
              minSize: new go.Size(300, 110),
              alignment: go.Spot.Right,
              itemTemplate:
                $(go.Panel, "TableRow",
                  $(go.TextBlock,
                  {
                    toolTip: $(go.Adornment, "Auto",
                      $(go.Shape, { fill: "#FFFFCC" }),
                      $(go.TextBlock, { margin: 4 },
                        new go.Binding("text", "tooltipText"),
                        new go.Binding("visible", "tooltipText", (t) => {
                          return !!t;
                        }) // Only show if text exists
                      )
                    ),
                    column: 0, alignment: go.Spot.Left, font: "bold 10pt sans-serif"
                  },
                  new go.Binding("text", "name")
                  ),
                  $(go.TextBlock, { column: 1, alignment: go.Spot.Left},
                    new go.Binding("text", "value")
                  ),
                ) // end itemTemplate
              },
              new go.Binding('itemArray', 'switchInfo'),
            )
            .add(
              new go.TextBlock("More info", { alignment: go.Spot.Left, font: 'bold 10pt Inter'}),
            )
          ),
        );
      this.myDiagram.groupTemplateMap.add("SwitchContainer", twoColumnGroupTemplate);

No, I said to set that property on the inner Group, not on the outer Group.