Column span binding not working

That is a valid thought, however, I believe that this is not the case in my example. Here is the screenshot (the group in the second row, named “RolesGuard”, needs to stretch full width of its parent):

The column sizes for the rest of the group have been determined already by the first row.

Also, can you please also tell me why the node data object for first level group (type: SourceFile, Name: roles.guard.ts) does not contain the “size” property, even though its children groups following the same template do have that exact property?

Could you provide the model data?

Sizing is determined by the container’s layout (i.e. Group.layout or Diagram.layout). So for a top-level node, such as that “Type: SourceFile Name: roles.guard.ts” group, it depends on whether the Diagram.layout is changing the size of those top-level nodes. Probably not.

Here is the data for the node which I wanted to span 5 columns. I hope you can get some idea of the type of data I am dealing with, from this example, as it isn’t possible to share the entire modelData here

{
    "key": "codemapsrunner-roles.guard.ts-257-220",
    "isSubGraphExpanded": true,
    "isGroup": true,
    "group": "codemapsrunner-roles.guard.ts-305-0",
    "text": "RolesGuard",
    "kind": 257,
    "kindName": "ClassDeclaration",
    "fileName": "roles.guard.ts",
    "appName": "codemapsrunner",
    "color": "#c3eff7",
    "columnSpan": 4,
    "col": 0,
    "row": 2,
    "loc": "-1447.499999999999 1386.6916970786172",
    "size": "473 256.52284749830795",
    "__gohashid": 1091
}

The container for my top-level node follows a verticalLayout type template, but nothing that tampers with the size of its children. Regardless of that, I still do think that as defined in the template, my level 1 group must at least have the size property in its node data.

But if you want me to try to reproduce the problem, I need all of the data – the columns of the other nodes matter!

And this one node cannot span 5 columns – the value of columnSpan is 4, not 5.

I have cherry picked the data for the relevant portion of the diagram.

 const nodeDataArray = [
{
    "key": "codemapsrunner-request-context.service.ts-305-0",
    "isSubGraphExpanded": true,
    "isGroup": true,
    "group": "codemapsrunner",
    "text": "request-context.service.ts",
    "kind": 305,
    "kindName": "SourceFile",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#c3eff7",
    "columnSpan": 1,
    "col": 3,
    "row": 36,
    "loc": "-1455.499999999999 5992.997409057993",
    "__gohashid": 1200
},
{
    "key": "codemapsrunner-request-context.service.ts-266-0",
    "isSubGraphExpanded": false,
    "isGroup": true,
    "group": "codemapsrunner-request-context.service.ts-305-0",
    "text": "'@nestjs/common'",
    "kind": 266,
    "kindName": "ImportDeclaration",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#c3eff7",
    "columnSpan": 1,
    "col": 0,
    "row": 0,
    "loc": "-1447.499999999999 6082.717189539699",
    "size": "473 204.52284749830795",
    "__gohashid": 1201
},
{
    "key": "codemapsrunner-request-context.service.ts-270-8",
    "isSubGraphExpanded": false,
    "isGroup": false,
    "group": "codemapsrunner-request-context.service.ts-266-0",
    "text": "Inject",
    "kind": 270,
    "kindName": "ImportSpecifier",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#ffd4f5",
    "columnSpan": 1,
    "col": 0,
    "row": 0,
    "size": "152 52",
    "loc": "-1439.499999999999 6090.717189539699",
    "__gohashid": 1202
},
{
    "key": "codemapsrunner-request-context.service.ts-270-16",
    "isSubGraphExpanded": false,
    "isGroup": false,
    "group": "codemapsrunner-request-context.service.ts-266-0",
    "text": "Injectable",
    "kind": 270,
    "kindName": "ImportSpecifier",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#ffd4f5",
    "columnSpan": 1,
    "col": 1,
    "row": 0,
    "size": "152 52",
    "loc": "-1287.499999999999 6090.717189539699",
    "__gohashid": 1203
},
{
    "key": "codemapsrunner-request-context.service.ts-270-28",
    "isSubGraphExpanded": false,
    "isGroup": false,
    "group": "codemapsrunner-request-context.service.ts-266-0",
    "text": "Scope",
    "kind": 270,
    "kindName": "ImportSpecifier",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#ffd4f5",
    "columnSpan": 1,
    "col": 2,
    "row": 0,
    "size": "152 52",
    "loc": "-1135.499999999999 6090.717189539699",
    "__gohashid": 1204
},
{
    "key": "codemapsrunner-request-context.service.ts-266-59",
    "isSubGraphExpanded": false,
    "isGroup": true,
    "group": "codemapsrunner-request-context.service.ts-305-0",
    "text": "'@nestjs/core'",
    "kind": 266,
    "kindName": "ImportDeclaration",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#c3eff7",
    "columnSpan": 1,
    "col": 1,
    "row": 0,
    "loc": "-966.4999999999989 6082.717189539699",
    "size": "266 204.52284749830795",
    "__gohashid": 1205
},
{
    "key": "codemapsrunner-request-context.service.ts-270-68",
    "isSubGraphExpanded": false,
    "isGroup": false,
    "group": "codemapsrunner-request-context.service.ts-266-59",
    "text": "REQUEST",
    "kind": 270,
    "kindName": "ImportSpecifier",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#ffd4f5",
    "columnSpan": 1,
    "col": 0,
    "row": 0,
    "size": "152 52",
    "loc": "-958.4999999999989 6090.717189539699",
    "__gohashid": 1206
},
{
    "key": "codemapsrunner-request-context.service.ts-266-99",
    "isSubGraphExpanded": false,
    "isGroup": true,
    "group": "codemapsrunner-request-context.service.ts-305-0",
    "text": "'express'",
    "kind": 266,
    "kindName": "ImportDeclaration",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#c3eff7",
    "columnSpan": 1,
    "col": 2,
    "row": 0,
    "loc": "-692.499999999999 6082.717189539699",
    "size": "266 204.52284749830795",
    "__gohashid": 1207
},
{
    "key": "codemapsrunner-request-context.service.ts-270-113",
    "isSubGraphExpanded": false,
    "isGroup": false,
    "group": "codemapsrunner-request-context.service.ts-266-99",
    "text": "Request",
    "kind": 270,
    "kindName": "ImportSpecifier",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#ffd4f5",
    "columnSpan": 1,
    "col": 0,
    "row": 0,
    "size": "152 52",
    "loc": "-684.499999999999 6090.717189539699",
    "__gohashid": 1208
},
{
    "key": "codemapsrunner-request-context.service.ts-257-139",
    "isSubGraphExpanded": true,
    "isGroup": true,
    "group": "codemapsrunner-request-context.service.ts-305-0",
    "text": "RequestContext",
    "kind": 257,
    "kindName": "ClassDeclaration",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#c3eff7",
    "columnSpan": 4,
    "col": 0,
    "row": 2,
    "loc": "-1447.499999999999 6295.240037038004",
    "size": "473 204.52284749830795",
    "__gohashid": 1209
},
{
    "key": "codemapsrunner-request-context.service.ts-171-261",
    "isSubGraphExpanded": true,
    "isGroup": false,
    "group": "codemapsrunner-request-context.service.ts-257-139",
    "text": "Constructor",
    "kind": 171,
    "kindName": "Constructor",
    "fileName": "request-context.service.ts",
    "appName": "codemapsrunner",
    "color": "#e1f7c1",
    "columnSpan": 1,
    "col": 0,
    "row": 0,
    "size": "152 52",
    "loc": "-1439.499999999999 6303.240037038004",
    "__gohashid": 1210
}
].

Here are my group and node templates:

const tableLayout = $(go.Group, "Vertical",
    { 
      alignment: go.Spot.TopLeft,  // affects TableLayout of containing group
      stretch: go.GraphObject.Horizontal,
      margin: 4,  // affects TableLayout of containing group
      layout: $(TableLayout),  // affects arrangement of member nodes
      minSize: new go.Size(100, 50),  // affects this panel (i.e. whole group)
      defaultStretch: go.GraphObject.Horizontal  // affects elements of this panel
    },
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding("column", "col"),
    new go.Binding("columnSpan", "colSpan"),
    new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
    // the header
    $(go.Panel, "Table",
        { padding: new go.Margin(8, 4) },
        $(go.Panel, "Vertical",
            $("SubGraphExpanderButton", {margin: new go.Margin(0,0,3,0)}),
            $(go.Panel, "Horizontal",
                    $(go.TextBlock,
                    { font: "bold 16pt sans-serif", margin: new go.Margin(0, 4),desiredSize: new go.Size(250,50)},
                    new go.Binding("text", "", data => `Type: ${data.kindName}`)
                    ),
            ), 
            $(go.Panel, "Horizontal",
                $(go.TextBlock,
                { font: "bold 12pt sans-serif", margin: new go.Margin(0, 4) , desiredSize: new go.Size(250,50)},
                new go.Binding("text", "", data => `Table: ${data.key}`)
                )
            )
        )
        ),
        // the body holding the member nodes
        $(go.Panel, "Auto",
            $(go.Shape, { fill: null }),
            $(go.Placeholder, { padding: 8, alignment: go.Spot.TopLeft })
        )
  );



// NODE TEMPLATE

const defaultNode =
        $(go.Node, "Auto",
            {
                stretch: go.GraphObject.Horizontal,
                defaultStretch: go.GraphObject.Horizontal,
                alignment: go.Spot.Left,
                padding: 1,
                desiredSize: new go.Size(150,50),
            },
            new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
            new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
            new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
            new go.Binding('column', 'col'),
            $(go.Panel, "Auto" , {desiredSize: new go.Size(150,50)},
                $(go.Shape, "RoundedRectangle", { parameter1: 4}),
                $(go.TextBlock, new go.Binding("text", "text"),
                    {
                        //stretch: go.GraphObject.Fill,
                        desiredSize: new go.Size(150,50),
                        //verticalAlignment: go.Spot.Center,
                        font: 'bold 12pt sans-serif', stroke: "#424242"
                    })
            )
        );

What is supposed to be the name of the data property holding the column span number? Is it “colSpan” or “columnSpan”? I think your template doesn’t match your data.

the property name that I am getting in the modelData is “columnSpan”. Now this model data is sent to the client side, where I have defined a similar group layout, that uses the location of nodes computed in the backend, to place the nodes in their relevant positions. Here is that client-side template:

const tableLayout = $(go.Group, "Vertical",

      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      new go.Binding("isSubGraphExpanded", "isSubGraphExpanded"),
      new go.Binding("desiredSize", "size", go.Size.parse),
      new go.Binding('columnSpan', 'columnSpan'),
      {defaultStretch: go.GraphObject.Horizontal, alignment: go.Spot.TopLeft},
      
      // { 
      //   alignment: go.Spot.Left,  // affects TableLayout of containing group
      //   margin: 4,  // affects TableLayout of containing group
      //   //layout: $(TableLayout),  // affects arrangement of member nodes
      //   minSize: new go.Size(100, 50),  // affects this panel (i.e. whole group)
      //   defaultStretch: go.GraphObject.Horizontal  // affects elements of this panel
      // },
      // new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
      // new go.Binding('column', 'col'),
      // new go.Binding('columnSpan', 'colSpan'),
      // the header
      $(go.Panel, "Table",
        { padding: new go.Margin(8, 4), background: "#74a2f2"},
        //new go.Binding("background", "color"),
        $(go.Panel, "Vertical",
        $("SubGraphExpanderButton", {margin: new go.Margin(0,0,3,0)}),
          $(go.Panel, "Horizontal",
            $(go.TextBlock,
              { font: "bold 16pt sans-serif", margin: new go.Margin(0, 4), maxSize: new go.Size(350,50)},
              new go.Binding("text", "", data => `Type: ${data.kindName}`)
            ),
          ), 
          $(go.Panel, "Horizontal",
            
            $(go.TextBlock, 
              { font: "bold 16pt sans-serif", margin: new go.Margin(0, 4),maxSize: new go.Size(350,50)},
              new go.Binding("text", "", data => `Name: ${data.text}`)
            )
          )
        )
        
      ),
      // the body holding the member nodes
      $(go.Panel, "Auto", {background:"#fff",},
        //$(go.Shape, { fill: null }),
        $(go.Placeholder, { padding: 8, alignment: go.Spot.TopLeft })
      ),
      {
        toolTip:  // define a tooltip for each node that displays the color as text
          $("ToolTip",
            $(go.TextBlock, { margin: 4 },
              new go.Binding("text", "", data=>`Name: ${data.text} \n Type: ${data.kindName} \n Parent File: ${data.fileName}`))
          )  // end of Adornment
      }
    );

Please note that this is essentially the same template that I am using in my server to perform layouts, except that the layout performing parts have been commented out,

Except that on the client side there’s no need for bindings on row, column, nor columnSpan, since there won’t be any TableLayout on the client.

Anyway, as I pointed out before, the problem with the layout on the server is that your template’s property names don’t match your model data property names.

Thanks, that seemed to be it. Since we have already established the context behind my layout, I do have another question though, which concerns the computed locations of nodes in the backend server.
From the model data that I sent to you above, you might see that many nodes have locations that contain negative values. I see that all of my nodes (and groups) are assuming absolute locations relative to document size or viewport size or something similar, but my goal was for each of them to have locations relative to the top-left part of their immediate parents. I can not find some helpful reference that would explain this, although I did learn that locations for nested items are, in fact, calculated relative to their parents, so I don’t understand why I get absolute locations.

All Parts have position/location/actualBounds in document coordinates. Think of how complicated Link routes would be if they were somehow in some coordinate system that Groups had – which coordinate system should they use when connecting different Groups.

Yeah, that makes sense. So just to be clear, the locations are supposed to be relative to document size? Imagine the case where we manually add location as “0 0” for a child node of a group in the modelData, and then bind the location on the template, why does the node end up being positioned in the correct place (top-left of its parent)??? Or is it that internally the node’s position gets converted relative to the document size?

If the Group has a Placeholder, that would explain the positioning and sizing of the group.

I understand. The groups in my diagram do have placeholders. What I don’t understand, however, is that if the nested groups/nodes have absolute positions, then how do they maintain their locations if their containers are arranged by the client, depending on the overall diagram layout? For example, consider the screenshot that I sent below, where the containers enclosed in red dotted lines are my top level containers, whose locations are not being computed on the backend NodeJs, but is left open to be determined by the client side depending on whichever type of layout is set for the whole diagram (circular, grid, tree etc). All the children nodes of these containers have pre-calculated locations, and the client side merely has a two-way binding on that location and places each node/group in its correct place. Now, if I have a grid layout, these two top-level nodes (enclosed in red) are stacked in a grid manner, if I have a circular layout, then they assume a different arrangement, so would that mean that the locations of all internal nodes/groups are re-calculated by the client to maintain the internal structure of these top-level groups? My idea was that if these internal (or nested) groups/nodes had their positions relative to their container group, then it wouldn’t matter wherever we pushed the containers, but I don’t see how it all works with absolute positioning system.

I don’t understand the problem. The Diagram.layout will position each of the top-level nodes, which seem to be Groups in your case. The diagram layout will move those groups in whatever manner it wants, as if each group is an atomic object. That diagram layout does not care about how the member nodes of the groups are positioned relative to each other. Whether or not groups have Placeholders doesn’t matter. Each Group.layout similarly should move its member nodes around as it wishes.

Yes, the position of each node is always in document coordinates. And most layouts, whether or not they are the Diagram.layout or a Group.layout will position nodes starting at the origin (0,0). But having a group layout position its member nodes starting at (0,0) is OK since if the group needs to be elsewhere according to the layout that is responsible for positioning the group, it will move the group including all of its members.

The context behind this issue is that I am performing my rendering at the backend, in isolation for each “top-level” group, storing the nodeData in a database, and then multiple such top-level groups are fetched, and aggregated on the client side to display the diagram, with the client side having a diagram layout, but no Group.layout, as it places all “member nodes/groups” in their positions that it gets from the backend. Now image a “top-level” group, named Group1, which was rendered in isolation on the backend, so that it was the only group in the diagram, with a single member node, so the location for Group1 is computed as (0,0) and it’s member, named Node1 as (20,20). On the client side, we bring together similar such groups laid out independently (the client side has no Group.layout, but two-way location bindings with nodeData, and some Diagram.layout). Imagine this Group1 is now pushed to the position (100,100) because of Diagram.layout on the client, and its member node had the data for location as (20,20). I am confused as to how gojs tackles this. Does it place that member 20 units from the top-left of Group1, or is my location binding rendered useless here and gojs will re-make the entire diagram (which is something that I do not want, as our large layouts become extremely slow).

I was under the impression that by doing this laying out on the back-end would give me locations of nodes that start from the top-left of their container, and not the “document”, but that wasn’t the case, as I was getting many locations in negative units.

Yes, I’m aware of the group layouts being computed on the server and the diagram layout being computed on the client.

Here’s my latest version of the sample, intended to run on the client using the model data shown in the textarea which does have location information, even though the node templates do not have TwoWay Bindings on the “location”.

Change the value of CLIENT to be false when wanting to pretend to run the code on a server and save the serialized model to the textarea to be loaded on the client.

<!DOCTYPE html>
<html>
<head>
  <title>Loading Graph from Server Data</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button id="myTestButton">Change Diagram.layout</button>
  <!-- this model includes "loc" and "size" and "expanded" property values,
       and thus does not require Group.layout being a TableLayout nor any TwoWay Bindings -->
  <textarea id="mySavedModel" style="width:100%;height:250px">
{ "class": "GraphLinksModel",
  "nodeDataArray": [
{"key":1,"isGroup":true,"loc":"0 33.81052598953247"},
{"key":2,"isGroup":true,"group":1,"row":0,"col":0,"loc":"8 75.62105197906494","size":"220 81.61052598953248"},
{"key":3,"isGroup":true,"group":1,"row":0,"col":1,"loc":"236 75.62105197906494","size":"327 81.61052598953248"},
{"key":4,"isGroup":true,"group":1,"row":0,"col":2,"loc":"571 75.62105197906494","size":"433 81.61052598953248"},
{"key":5,"isGroup":true,"group":1,"row":0,"col":3,"loc":"1012 75.62105197906494","size":"115 81.61052598953248"},
{"key":6,"isGroup":true,"row":0,"col":4,"loc":"281.5 305.3421039581298"},
{"key":7,"isGroup":true,"group":6,"row":0,"col":0,"loc":"289.5 347.1526299476623","size":"221 81.61052598953248"},
{"key":8,"isGroup":true,"group":6,"row":0,"col":1,"loc":"518.5 347.1526299476623","size":"327 81.61052598953248"},
{"key":9,"isGroup":true,"group":1,"row":1,"col":0,"loc":"8 165.2315779685974","size":"220 81.61052598953248"},
{"key":10,"isGroup":true,"group":1,"row":1,"col":1,"colSpan":3,"loc":"236 165.2315779685974","size":"891 50.31052598953247"},
{"group":2,"row":0,"col":0,"key":-11,"loc":"16 83.62105197906494"},
{"group":2,"row":0,"col":1,"key":-12,"loc":"121 83.62105197906494"},
{"group":3,"row":0,"col":0,"key":-13,"loc":"244 83.62105197906494"},
{"group":3,"row":0,"col":1,"key":-14,"loc":"350 83.62105197906494"},
{"group":3,"row":0,"col":2,"key":-15,"loc":"456 83.62105197906494"},
{"group":4,"row":0,"col":0,"key":-16,"loc":"579 83.62105197906494"},
{"group":4,"row":0,"col":1,"key":-17,"loc":"685 83.62105197906494"},
{"group":4,"row":0,"col":2,"key":-18,"loc":"791 83.62105197906494"},
{"group":4,"row":0,"col":3,"key":-19,"loc":"897 83.62105197906494"},
{"group":5,"row":0,"col":0,"key":-20,"loc":"1020 83.62105197906494"},
{"group":7,"row":0,"col":0,"key":-21,"loc":"297.5 355.1526299476623"},
{"group":7,"row":0,"col":1,"key":-22,"loc":"403.5 355.1526299476623"},
{"group":8,"row":0,"col":0,"key":-23,"loc":"526.5 355.1526299476623"},
{"group":8,"row":0,"col":1,"key":-24,"loc":"632.5 355.1526299476623"},
{"group":8,"row":0,"col":2,"key":-25,"loc":"738.5 355.1526299476623"},
{"group":9,"row":0,"col":0,"key":-26,"loc":"16 173.2315779685974"}
],
  "linkDataArray": [{"from":1,"to":6}]}
  </textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script src="https://unpkg.com/gojs/extensions/TableLayout.js"></script>
  <script id="code">
const CLIENT = true;

const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      initialAutoScale: go.Diagram.Uniform,
      layout: $(go.TreeLayout, { angle: 90 }),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      alignment: go.Spot.Left,
      margin: 4
    },
    CLIENT
      ? new go.Binding("location", "loc", go.Point.parse)
      : new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding('column', 'col'),
    new go.Binding('columnSpan', 'colSpan'),
    $(go.Shape,
      { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock,
      { margin: 8 },
      new go.Binding("text", "key", k => `Node key: ${k}`))
  );

myDiagram.groupTemplate =
  $(go.Group, "Vertical",
    { 
      alignment: go.Spot.TopLeft,  // affects TableLayout of containing group
      stretch: go.GraphObject.Horizontal,
      margin: 4,  // affects TableLayout of containing group
      layout: CLIENT ? $(go.Layout) : $(TableLayout),  // affects arrangement of member nodes
      minSize: new go.Size(100, 50),  // affects this panel (i.e. whole group)
      defaultStretch: go.GraphObject.Horizontal  // affects elements of this panel
    },
    CLIENT
      ? new go.Binding("location", "loc", go.Point.parse)
      : new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding("column", "col"),
    new go.Binding("columnSpan", "colSpan"),
    new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
    // the header
    $(go.Panel, "Table",
      { padding: new go.Margin(8, 4), background: "green" },
      new go.Binding("background", "color"),
      $(go.Panel, "Horizontal",
        $("SubGraphExpanderButton"),
        $(go.TextBlock,
          { font: "bold 12pt sans-serif", margin: new go.Margin(0, 4) },
          new go.Binding("text", "", data => `Table: ${data.key}`)
        )
      )
    ),
    // the body holding the member nodes
    $(go.Panel, "Auto",
      $(go.Shape, { fill: null }),
      $(go.Placeholder, { padding: 8, alignment: go.Spot.Left })
    )
  );

if (CLIENT) {  // client side
  // Load the model shown above in the textarea where each node data object
  // does have location information and the templates have OneWay Bindings.
  var str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
} else {  // server side
  // Note that the node data do NOT have any location information,
  // so this requires Group.layout being a TableLayout and the templates
  // having TwoWay Bindings on "location" so as to remember the layout results.
  myDiagram.model = new go.GraphLinksModel(
  [
    { key: 1, isGroup: true },
    { key: 2, isGroup: true, group: 1, row: 0, col: 0 },
    { key: 3, isGroup: true, group: 1, row: 0, col: 1  },
    { key: 4, isGroup: true, group: 1, row: 0, col: 2  },
    { key: 5, isGroup: true, group: 1, row: 0, col: 3  },
    { key: 6, isGroup: true, row: 0, col: 4  },
    { key: 7, isGroup: true, group: 6, row: 0, col: 0  },
    { key: 8, isGroup: true, group: 6, row: 0, col: 1  },
    { key: 9, isGroup: true, group: 1, row: 1, col: 0  },
    { key: 10, isGroup: true, group: 1, row: 1, col: 1, colSpan: 3  },
    { group: 2, row: 0, col: 0 },
    { group: 2, row: 0, col: 1 },
    { group: 3, row: 0, col: 0 },
    { group: 3, row: 0, col: 1 },
    { group: 3, row: 0, col: 2 },
    { group: 4, row: 0, col: 0 },
    { group: 4, row: 0, col: 1 },
    { group: 4, row: 0, col: 2 },
    { group: 4, row: 0, col: 3 },
    { group: 5, row: 0, col: 0 },
    { group: 7, row: 0, col: 0 },
    { group: 7, row: 0, col: 1 },
    { group: 8, row: 0, col: 0 },
    { group: 8, row: 0, col: 1 },
    { group: 8, row: 0, col: 2 },
    { group: 9, row: 0, col: 0 },
  ],
  [
    { from: 1, to: 6 }
  ]);
}

document.getElementById("myTestButton").addEventListener("click", e => {
  if (myDiagram.layout.angle === 0) {
    myDiagram.commit(d => d.layout.angle = 90);
  } else {
    myDiagram.commit(d => d.layout.angle = 0);
  }
});
  </script>
</body>
</html>

Thanks for the sample. I don’t know what you want to show here, however. My question above still stands…

OK, here’s a simpler example, following your case of a group containing a single node whose only node is positioned on the server. There is a Group.layout but there is only the default Diagram.layout.

The results are saved in the textarea, which is read on the client without any Group.layout but with a Diagram.layout.

As far as I can understand the situation, everything seems to be working as I believe you are expecting.

<!DOCTYPE html>
<html>
<head>
  <title>Loading Graph from Server Data</title>
  <!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <span id="mySide"></span><br>
  <button id="myClientButton">Change Diagram.layout</button>
  <button id="myServerButton">Save server-side layout</button>
  <!-- this model includes "loc" and "size" and "expanded" property values,
       and thus does not require Group.layout being a TableLayout nor any TwoWay Bindings -->
  <textarea id="mySavedModel" style="width:100%;height:250px">
{ "class": "GraphLinksModel",
  "nodeDataArray": [
{"key":1,"isGroup":true,"loc":"0 33.81052598953247"},
{"key":-1,"group":1,"row":0,"col":0,"loc":"8 41.81052598953247"},
{"key":2,"isGroup":true,"loc":"128 33.81052598953247"},
{"key":-2,"group":2,"row":0,"col":0,"loc":"136 41.81052598953247"}
],
  "linkDataArray": [{"from":1,"to":2}]}
  </textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script src="https://unpkg.com/gojs/extensions/TableLayout.js"></script>
  <script id="code">
const CLIENT = true;

const $ = go.GraphObject.make;

const myDiagram =
  new go.Diagram("myDiagramDiv",
    {
      initialAutoScale: go.Diagram.Uniform,
      layout: CLIENT ? $(go.TreeLayout, { angle: 90 }) : $(go.Layout),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      alignment: go.Spot.Left,
      margin: 4
    },
    CLIENT
      ? new go.Binding("location", "loc", go.Point.parse)
      : new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding('column', 'col'),
    new go.Binding('columnSpan', 'colSpan'),
    $(go.Shape,
      { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock,
      { margin: 8 },
      new go.Binding("text", "key", k => `Node key: ${k}`))
  );

myDiagram.groupTemplate =
  $(go.Group, "Vertical",
    { 
      alignment: go.Spot.TopLeft,  // affects TableLayout of containing group
      stretch: go.GraphObject.Horizontal,
      margin: 4,  // affects TableLayout of containing group
      layout: CLIENT ? $(go.Layout) : $(TableLayout),  // affects arrangement of member nodes
      minSize: new go.Size(100, 50),  // affects this panel (i.e. whole group)
      defaultStretch: go.GraphObject.Horizontal  // affects elements of this panel
    },
    CLIENT
      ? new go.Binding("location", "loc", go.Point.parse)
      : new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding("desiredSize", "size", go.Size.parse).makeTwoWay(go.Size.stringify),
    new go.Binding("row", "row"),  // these all affect TableLayout of containing Group
    new go.Binding("column", "col"),
    new go.Binding("columnSpan", "colSpan"),
    new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
    // the header
    $(go.Panel, "Table",
      { padding: new go.Margin(8, 4), background: "green" },
      new go.Binding("background", "color"),
      $(go.Panel, "Horizontal",
        $("SubGraphExpanderButton"),
        $(go.TextBlock,
          { font: "bold 12pt sans-serif", margin: new go.Margin(0, 4) },
          new go.Binding("text", "", data => `Table: ${data.key}`)
        )
      )
    ),
    // the body holding the member nodes
    $(go.Panel, "Auto",
      $(go.Shape, { fill: null }),
      $(go.Placeholder, { padding: 8, alignment: go.Spot.Left })
    )
  );

if (CLIENT) {  // client side
  // Load the model shown above in the textarea where each node data object
  // does have location information and the templates have OneWay Bindings.
  var str = document.getElementById("mySavedModel").value;
  myDiagram.model = go.Model.fromJson(str);
} else {  // server side
  // Note that the node data do NOT have any location information,
  // so this requires Group.layout being a TableLayout and the templates
  // having TwoWay Bindings on "location" so as to remember the layout results.
  myDiagram.model = new go.GraphLinksModel(
  [
    { key: 1, isGroup: true },
    { key: -1, group: 1, row: 0, col: 0 },
    { key: 2, isGroup: true },
    { key: -2, group: 2, row: 0, col: 0 },
  ],
  [
    { from: 1, to: 2 }
  ]);
}

document.getElementById("mySide").textContent = CLIENT ? "on CLIENT" : "on SERVER";

document.getElementById("myClientButton").disabled = !CLIENT;
document.getElementById("myClientButton").addEventListener("click", e => {
  if (myDiagram.layout.angle === 0) {
    myDiagram.commit(d => d.layout.angle = 90);
  } else {
    myDiagram.commit(d => d.layout.angle = 0);
  }
});

document.getElementById("myServerButton").disabled = CLIENT;
document.getElementById("myServerButton").addEventListener("click", e => {
  document.getElementById("mySavedModel").value = myDiagram.model.toJson();
});
  </script>
</body>
</html>

Thanks for such a nice explanation with a perfectly-crafted sample. I see that the diagram works well without breaking, as is the case in my layout. If a member node has location (20,20) and its parents group gets pushed to location (100,100) on the client, then the node still occupies the correct position inside its parent. I am just trying to get an explanation for why this happens, if the location of nodes are in document co-ordinates. From what I understand, node locations being in document co-ordinates should break the entire structure of the diagram, with the parent and the member node being in different positions on the client end. I just want an explanation for what goes on internally in this scenario. If you could help me understand this, I would really appreciate it. Thanks

OK, here’s an explanation from a different perspective. There are two ways of setting the location of a node – by setting Node.location or by calling Node.move. These work the same for simple Nodes.

(I’m ignoring the possible distinction between “location” and “position”. That’s not relevant to this discussion.)

Setting location just sets the location for that node. That includes groups! But for groups there are also member nodes (and links – again that doesn’t matter here) but setting the location of a group will not modify the location of any member nodes. This policy is needed because the location of a group might be independent of the location(s) of its member node(s). As it turns out, if the group has a Group.placeholder, the location of the group will not be independent of the members.

But if you call move on a Group, it will move both the group and all of its member parts together. All of the built-in layouts move nodes by calling move just so that those layouts can treat groups as if they were atomic nodes.