Saved model changes when loading it back

Hi, I’m using Vue with GoJS

I’m trying to save the model using Model.toJson() to Session Storage and load them on refresh with Model.fromJson(). However I tested the this out and the diagram drawn differently and become messed up. I already use .makeTwoWay on both node and link.

Please find the code references below

Save Function

        function save(){
          console.log(diagram.model.toJson());
          sessionStorage.setItem("model",diagram.model.toJson());
          diagram.isModified = false;
        }

Link Template:

const linkTemplateSimple = $(go.Link,{
                adjusting: go.Link.Stretch,
                reshapable: true,
                resegmentable: true,
                routing: go.Link.Orthogonal,
                corner: 10,
                fromEndSegmentLength:20,
                toEndSegmentLength:12,
                fromSpot: go.Spot.None,
                toSpot: go.Spot.None,
              },
              // Line preset
              $(go.Shape, {
                contextMenu: linkContextMenuTemplate
              },
                  new go.Binding("stroke", "isHighlighted", function(h)
                  {
                    return h ? mappings[currentTheme].highlight : mappings[currentTheme].fill;
                  }).ofObject(),
                  new go.Binding("strokeWidth", "isHighlighted", function(h)
                  {
                    return h ? 2 : 1;
                  }).ofObject()
              ),
              // Arrow preset
              $(go.Shape, {
                toArrow: "Standard",
                },
                  new go.Binding("stroke", "isHighlighted", function(h)
                  {
                    return h ? mappings[currentTheme].highlight : mappings[currentTheme].fill;
                  }).ofObject(),
                  new go.Binding("fill", "isHighlighted", function(h)
                  {
                    return h ? mappings[currentTheme].highlight : mappings[currentTheme].fill;
                  }).ofObject(),
              ),
              // Line label preset
              $(go.Panel, "Auto",
                  $(go.TextBlock, new go.Binding("text","type"), {
                    stroke: mappings[this.themeConfig].text,
                    font: "small-caps 14px Sans-Serif",
                    background: mappings[this.themeConfig].text_background,
                    segmentOffset: new go.Point(0, 0),
                    textAlign: "center",
                    contextMenu: linkContextMenuTemplate,
                    segmentIndex: 1,
                  },),
                ),
              new go.Binding("points").makeTwoWay(),
        );

Node Template

const nodeTemplateSimple =
            $(go.Node, "Auto",
             {
               fromSpot: go.Spot.None,
               toSpot: go.Spot.None,
               avoidable: true,
               movable: false,
               layerName: "Foreground",
               contextMenu: nodeContextMenuTemplate,
               click: highlight,
             },
                $(go.Panel, "Auto",
                    // Container Panel
                    $(go.Shape, {stroke: mappings[this.themeConfig].fill},
                        // Binding corresponding to dataNode data
                        // stroke and strokeWidth changes when highlighted
                        new go.Binding("figure", "shape"),
                        new go.Binding("fill", "color"),
                        new go.Binding("stroke", "isHighlighted", function(h)
                        {
                          return h ? mappings[currentTheme].highlight : mappings[currentTheme].fill;
                        }).ofObject(),
                        new go.Binding("strokeWidth", "isHighlighted", function(h)
                        {
                          return h ? 4 : 1;
                        }).ofObject()),
                    // Info Panel
                    // Render name, icon, and canonical_name data from dataNode
                    $(go.Panel, "Vertical", { padding: 20 },
                        $(go.TextBlock, new go.Binding("text", "name"), { font: '12pt sans-serif' }),
                        $(go.Picture, new go.Binding("source","icon"),{desiredSize: new go.Size(50,50)}),
                        $(go.TextBlock, new go.Binding("text", "canonical_name"), { font: '12pt sans-serif' }),
                    ),
                ),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
            );

Group Template

/**
         * Group simple template - Used for application level grouping
         */
const groupTemplateSimple =  $(go.Group, "Auto", {
                contextMenu: groupContextMenuTemplate,
                avoidable: true,
                movable: false,
                layout: $(go.LayeredDigraphLayout, 
                      {
                          aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
                          linkSpacing: 50,
                          layerSpacing: 150,
                          layeringOption: go.LayeredDigraphLayout.LayerOptimalLinkLength,
                          direction:180,
                          setsPortSpots: false,
                          columnSpacing: 50,
                      }
                   ),
                  subGraphExpandedChanged: function(group){
                    if(group.isSubGraphExpanded){
                      group.layerName = 'Background';
                    }else{
                      group.layerName = 'Foreground';
                    }
                  },
                  click: highlight,
                },
                new go.Binding("isSubGraphExpanded", "expanded"),
                $(go.Shape, "Rectangle",
                        {   stroke: mappings[this.themeConfig].fill, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] },new go.Binding("fill", "color")),
                $(go.Panel, "Vertical",
                    { defaultAlignment: go.Spot.TopLeft, margin: 50},
                    $(go.Panel, "Horizontal",
                        { defaultAlignment: go.Spot.Center},
                        $("SubGraphExpanderButton"),
                        $(go.TextBlock, { font: "small-caps 25px Sans-Serif", margin: 4, stroke: mappings[this.themeConfig].groupText }, new go.Binding("text", "key")),
                    ),
                    $(go.Placeholder, { padding: 50 }),
                ),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

        /**
         * Group structural template - Used for pillar / business context level grouping 
         */
        const groupTemplateStructural =  $(go.Group, "Auto", {
                avoidable: true,
                movable: false,
                layout: $(go.LayeredDigraphLayout,
                    {
                         aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
                         linkSpacing: 100,
                         layerSpacing: 100,
                         direction:180,
                         layeringOption: go.LayeredDigraphLayout.LayerLongestPathSink,
                         setsPortSpots: false,
                         columnSpacing: 50,
                    },),
                  // If not expanded, put node on the foreground, else put on the background
                  // Prevent links to overlap the nodes
                  subGraphExpandedChanged: function(group){
                    if(group.isSubGraphExpanded){
                      group.layerName = 'Background';
                    }else{
                      group.layerName = 'Foreground';
                    }
                  },
                  click: highlight,
                },
                new go.Binding("isSubGraphExpanded", "expanded"),
                $(go.Shape,
                    new go.Binding("figure", "shape"),
                        { stroke: mappings[this.themeConfig].fill, fill: null, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] }),

                    $(go.Panel, "Vertical",
                        { defaultAlignment: go.Spot.Left, padding: 10},
                        $(go.Panel, "Horizontal",
                            { defaultAlignment: go.Spot.TopLeft },
                            $("SubGraphExpanderButton"),
                            $(go.TextBlock, {stroke: mappings[this.themeConfig].fill, font: '14pt sans-serif', margin: new go.Margin(0,0,0,10) }, new go.Binding("text", "name"),
                            ),

                        ),
                        $(go.Placeholder, { padding: 100 })
                    ),
                new go.Binding("stroke", "isHighlighted", function(h)
                {
                  return h ? mappings[currentTheme].fill : mappings[currentTheme].fill;
                }).ofObject(),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

        /**
         * Group main template - Used for service bigger business grouping / bigger partition
         */
        const groupTemplateMain =  $(go.Group, "Auto", {
              avoidable: true,
              movable: false,
              layout: $(go.TreeLayout, {
                sorting: go.TreeLayout.SortingAscending,
                comparer: function(a, b){
                  let aData = a.data;
                  let bData = b.data;
                  if(aData.key < bData.key) return -1;
                  if(aData.key > bData.key) return 1;
                  return 0;
                },
              }),
              // If not expanded, put node on the foreground, else put on the background
              // Prevent links to overlap the nodes
              subGraphExpandedChanged: function(group){
                if(group.isSubGraphExpanded){
                  group.layerName = 'Background';
                }else{
                  group.layerName = 'Foreground';
                }
              }
            },
            $(go.Shape,
                new go.Binding("figure", "shape"),
                { stroke: mappings[this.themeConfig].fill, fill: null, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] }),

            $(go.Panel, "Auto",
                { defaultAlignment: go.Spot.Left, padding: 10},
                $(go.Panel, "Horizontal",
                    { defaultAlignment: go.Spot.TopLeft },
                    $(go.TextBlock, {stroke: mappings[this.themeConfig].fill, font: 'bold 30pt sans-serif', margin: new go.Margin(0,0,0,10) }, new go.Binding("text", "name"),),
                ),
                $(go.Placeholder, { padding: 80 })
            ),
            new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

Loading function

        if(sessionStorage.getItem("model") === null){
          // Default data when saved model does not exist
          diagram.model.nodeDataArray = dataNodes;
          diagram.model.linkDataArray = dataLinks;
        }else{
          diagram.layout.isInitial = false;
          diagram.model = go.Model.fromJson(sessionStorage.getItem("model"));
          diagram.updateAllRelationshipsFromData();
          diagram.updateAllTargetBindings();
        }

Saved model:
image

After loading:

Saved model snippet:

{ "class": "GraphLinksModel",
  "modelData": {"position":"1088.203306039712 -348.02078361192434"},
  "nodeDataArray": [
...
{"key":3,"group":"main","name":"LOGISTICS","isGroup":true,"expanded":true,"category":"structural","loc":"1006.03515625 496.2070892333984"},
{"key":"driver-app","description":"Driver App","group":3,"isGroup":true,"expanded":false,"category":"simple","color":"#d5f5e3","loc":"3465.97607421875 681.7798431396484"}
...
],
  "linkDataArray": [
...
{"category":"simple","key":"a-to-b","from":"a","to":"b","type":"TRIGGER_EVENT","documentations":<redacted>,"points":[4455.521484375,655.0652923583984,4425.521484375,655.0652923583984,4235.021484375,655.0652923583984,4235.021484375,764.2434661865234,3704.0009765625,764.2434661865234,3704.0009765625,914.2434661865234,3671.82421875,914.2434661865234,3645.82421875,914.2434661865234]}
...
]}

My assumption for now, is the save function does not capture the correct points (not sure why), any kind of help will be much appreciated.

Regards,
K.

How is the result “messed up”? From the screenshots it appears that the scale has changed, zoomed out. But if you zoom back in, are the nodes and links still where you would expect them to be?

You don’t show how the Diagram is initialized. Perhaps you have set Diagram.initialAutoScale to cause the initial diagram to be zoomed out. Or perhaps you are doing something else to have that effect.

A separate matter, but two comments:

  1. calling Model.toJson or Storage.getItem repeatedly is wasteful
  2. if you replace the Diagram.model you don’t need to do anything else to get the diagram to show up – so don’t call Diagram.updateAll… methods.

Hi walter, thank you for replying.

It is not a zoomed out result. As you can see, the link is extended further to the left and making U turns, where supposedly it comes directly from other node which placed on the right side of “driver-app” (it is not visible on the picture, but there are some nodes at the right side) to “driver-app” node. I’m also using initialAutoScale: go.Diagram.Uniform to make sure the zoom does not change. Also the node are not expected to be draggable nor adjustable, only the links are.

Here is my initialization setting.

      const diagram = $(go.Diagram, this.$el, {
        "undoManager.isEnabled": false,
        "panningTool.isEnabled": false,
        "draggingTool.isEnabled": false,
        initialAutoScale: go.Diagram.Uniform,
      });

As a side note, I didn’t use div id, but I uses this.$el which is Vue property that embed the viewport to the div inside the template that the component has.

Thank you for the comments, they are well noted.

Regards,
K.

Ah, OK, I couldn’t see the details in the image.

Try commenting out the setting of Link.adjusting in your link template.

Tried it, it’s still happening

After the load, are the Node.position for each Node the same as it was before the save, and do they match what was saved in the JSON string?

There’s no use of AvoidsNodes routing, is there? I did not see it in the link template that you quoted. If there isn’t, as I assume there should not be for your app, then that will rule out one possible cause.

Yes I can confirm the json being saved and loaded are the same. However they render completely different.

No, I use Orthogonal routing, to avoid links overlapping between each other. The main goal is to be able to “tidy up” the positioning of the links by manually readjusting/reshaping them after routing is done and keep the state after the links are being repositioned, as orthogonal routing still have overlapping links behind the node making it less obvious in determining where it connects to and from.

No, I was asking whether the value of Node.position had changed for any node.

If you can provide a way for me to reproduce the problem, I can look at it tomorrow.

I do not have that information, I can only tell by the look, and yes they have different position. I only have information of their location as I add makeTwoWay for them, and they did not change upon loading.

To reproduce basically you need to have nested groups (for my case it’s three) using specific layout that I put on the main post. And trigger the save function (I used simple button) then reload your page while make sure to call the load function which I put on the main post as well.

Template mapping:

        // Register template with keyword 'simple'
        nodeTemplateMap.add("simple", nodeTemplateSimple);
        diagram.nodeTemplateMap = nodeTemplateMap;

        // Register template with keyword 'simple'
        linkTemplateMap.add("simple",linkTemplateSimple);
        diagram.linkTemplateMap = linkTemplateMap;

        const groupTemplateMap = new go.Map();
        groupTemplateMap.add("simple", groupTemplateSimple)
        groupTemplateMap.add("structural", groupTemplateStructural)
        groupTemplateMap.add("main", groupTemplateMain)
        diagram.groupTemplateMap = groupTemplateMap

Here are some snippets of nodeData and linkData you can use

nodeDataArray = [
{"key":"main","name": "main", "isGroup": true,"expanded": true, "category": "main"}
{"key":1,"group":"main","name":"GROUP A","isGroup":true,"expanded":true,"category":"structural"},
{"key":2,"group":"main","name":"GROUP B","isGroup":true,"expanded":true,"category":"structural"},
{"key":"app-a","description":"App A","group":1,"isGroup":true,"expanded":false,"category":"simple"}
{"key":"app-b","description":"App B","group":1,"isGroup":true,"expanded":false,"category":"simple"}
{"key":"app-c","description":"App C","group":1,"isGroup":true,"expanded":false,"category":"simple"}
{"key":"app-d","description":"App D","group":2,"isGroup":true,"expanded":false,"category":"simple"}
{"group": "app-a","name": "app-a-component1","canonical_name": "App A component 1","key": "app-a-component1","description": "component of App A","color": "#fadbd8","category": "simple","shape": "RoundRectangle","documentations": [{"title": "Docs","ref": "https://google.com"}],"icon": "image.png"}
{"group": "app-a","name": "app-a-component2","canonical_name": "App A component 2","key": "app-a-component2","description": "component of App A","color": "#fadbd8","category": "simple","shape": "RoundRectangle","documentations": [{"title": "Docs","ref": "https://google.com"}],"icon": "image.png"}
]

linkDataArray = [
{"category": "simple","key": "b-to-a","from": "app-b","to": "app-a","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "c-to-a","from": "app-c","to": "app-a","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "d-to-a","from": "app-d","to": "app-a","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "ac1-to-d","from": "app-a-component1","to": "app-d","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "ac2-to-b","from": "app-a-component2","to": "app-b","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
]

The group has more node inside but it’s set to be viewable on group level only in the meantime.

important update:

I did some experiment, and I can confirm that the problem are the links that connect group to group in this case “driver-app” is a group linking to another group. “driver-app” also has nodes inside (it’s not visible because it’s not expanded) that connect to another group, this also affected.

The experiment I did was removing all the group-to-group and node-to-group links, leaving node-to-node only links. This makes everything works as intended, however I still need node-to-group and group-to-group to work as well.

Regards,
K.

Thanks for the additional code, but it was still a lot of work to produce a working sample. And after I added a TwoWay Binding to the Group.isSubGraphExpanded property, I am unable to reproduce any problems with links after saving and loading the model in JSON format.

Here’s my complete code:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          "undoManager.isEnabled": true,
          "panningTool.isEnabled": false,
          "draggingTool.isEnabled": false,
          initialAutoScale: go.Diagram.Uniform
        });

    const nodeTemplateSimple =
      $(go.Node, "Auto",
        {
          fromSpot: go.Spot.None,
          toSpot: go.Spot.None,
          avoidable: true,
          movable: false,
          layerName: "Foreground",
        },
          $(go.Panel, "Auto",
              // Container Panel
              $(go.Shape, { fill: "lightgray" },
                  // Binding corresponding to dataNode data
                  // stroke and strokeWidth changes when highlighted
                  new go.Binding("figure", "shape"),
                  new go.Binding("fill", "color"),
                ),
              // Info Panel
              // Render name, icon, and canonical_name data from dataNode
              $(go.Panel, "Vertical", { padding: 20 },
                  $(go.TextBlock, new go.Binding("text", "name"), { font: '12pt sans-serif' }),
                  $(go.Picture, new go.Binding("source","icon"),{desiredSize: new go.Size(50,50)}),
                  $(go.TextBlock, new go.Binding("text", "canonical_name"), { font: '12pt sans-serif' }),
              ),
          ),
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
      );

    const linkTemplateSimple =
      $(go.Link,{
          adjusting: go.Link.Stretch,
          reshapable: true,
          resegmentable: true,
          routing: go.Link.Orthogonal,
          corner: 10,
          fromEndSegmentLength:20,
          toEndSegmentLength:12,
          fromSpot: go.Spot.None,
          toSpot: go.Spot.None,
        },
        // Line preset
        $(go.Shape),
        // Arrow preset
        $(go.Shape, {
            toArrow: "Standard",
          },
        ),
        // Line label preset
        $(go.Panel, "Auto",
            $(go.TextBlock, new go.Binding("text","type"), {
              font: "small-caps 14px Sans-Serif",
              segmentOffset: new go.Point(0, 0),
              textAlign: "center",
              segmentIndex: 1,
            },),
          ),
        new go.Binding("points").makeTwoWay(),
      );

      const groupTemplateSimple =  $(go.Group, "Auto", {
                avoidable: true,
                movable: false,
                layout: $(go.LayeredDigraphLayout, 
                      {
                          aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
                          linkSpacing: 50,
                          layerSpacing: 150,
                          layeringOption: go.LayeredDigraphLayout.LayerOptimalLinkLength,
                          direction:180,
                          setsPortSpots: false,
                          columnSpacing: 50,
                      }
                   ),
                  subGraphExpandedChanged: function(group){
                    if(group.isSubGraphExpanded){
                      group.layerName = 'Background';
                    }else{
                      group.layerName = 'Foreground';
                    }
                  },
                },
                new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
                $(go.Shape, "Rectangle",
                        { fill: "lightgray", strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] },new go.Binding("fill", "color")),
                $(go.Panel, "Vertical",
                    { defaultAlignment: go.Spot.TopLeft, margin: 50},
                    $(go.Panel, "Horizontal",
                        { defaultAlignment: go.Spot.Center},
                        $("SubGraphExpanderButton"),
                        $(go.TextBlock, { font: "small-caps 25px Sans-Serif", margin: 4 }, new go.Binding("text", "key")),
                    ),
                    $(go.Placeholder, { padding: 50 }),
                ),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

        /**
         * Group structural template - Used for pillar / business context level grouping 
         */
        const groupTemplateStructural =  $(go.Group, "Auto", {
                avoidable: true,
                movable: false,
                layout: $(go.LayeredDigraphLayout,
                    {
                         aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
                         linkSpacing: 100,
                         layerSpacing: 100,
                         layeringOption: go.LayeredDigraphLayout.LayerLongestPathSink,
                         setsPortSpots: false,
                         columnSpacing: 50,
                    },),
                  // If not expanded, put node on the foreground, else put on the background
                  // Prevent links to overlap the nodes
                  subGraphExpandedChanged: function(group){
                    if(group.isSubGraphExpanded){
                      group.layerName = 'Background';
                    }else{
                      group.layerName = 'Foreground';
                    }
                  },
                },
                new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
                $(go.Shape,
                    new go.Binding("figure", "shape"),
                        { fill: null, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] }),

                    $(go.Panel, "Vertical",
                        { defaultAlignment: go.Spot.Left, padding: 10},
                        $(go.Panel, "Horizontal",
                            { defaultAlignment: go.Spot.TopLeft },
                            $("SubGraphExpanderButton"),
                            $(go.TextBlock, { font: '14pt sans-serif', margin: new go.Margin(0,0,0,10) }, new go.Binding("text", "name"),
                            ),

                        ),
                        $(go.Placeholder, { padding: 100 })
                    ),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

        /**
         * Group main template - Used for service bigger business grouping / bigger partition
         */
        const groupTemplateMain =  $(go.Group, "Auto", {
              avoidable: true,
              movable: false,
              layout: $(go.TreeLayout, {
                sorting: go.TreeLayout.SortingAscending,
                comparer: function(a, b){
                  let aData = a.data;
                  let bData = b.data;
                  if(aData.key < bData.key) return -1;
                  if(aData.key > bData.key) return 1;
                  return 0;
                },
              }),
              // If not expanded, put node on the foreground, else put on the background
              // Prevent links to overlap the nodes
              subGraphExpandedChanged: function(group){
                if(group.isSubGraphExpanded){
                  group.layerName = 'Background';
                }else{
                  group.layerName = 'Foreground';
                }
              }
            },
            $(go.Shape,
                new go.Binding("figure", "shape"),
                { fill: null, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] }),

            $(go.Panel, "Auto",
                { defaultAlignment: go.Spot.Left, padding: 10},
                $(go.Panel, "Horizontal",
                    { defaultAlignment: go.Spot.TopLeft },
                    $(go.TextBlock, { font: 'bold 30pt sans-serif', margin: new go.Margin(0,0,0,10) }, new go.Binding("text", "name"),),
                ),
                $(go.Placeholder, { padding: 80 })
            ),
            new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

    // Register template with keyword 'simple'
    myDiagram.nodeTemplateMap.add("simple", nodeTemplateSimple);

    // Register template with keyword 'simple'
    myDiagram.linkTemplateMap.add("simple",linkTemplateSimple);

    const groupTemplateMap = new go.Map();
    groupTemplateMap.add("simple", groupTemplateSimple)
    groupTemplateMap.add("structural", groupTemplateStructural)
    groupTemplateMap.add("main", groupTemplateMain)
    myDiagram.groupTemplateMap = groupTemplateMap

    myDiagram.model =
      $(go.GraphLinksModel, {
        nodeDataArray: [
{"key":"main","name": "main", "isGroup": true,"expanded": true, "category": "main"},
{"key":1,"group":"main","name":"GROUP A","isGroup":true,"expanded":true,"category":"structural"},
{"key":2,"group":"main","name":"GROUP B","isGroup":true,"expanded":true,"category":"structural"},
{"key":"app-a","description":"App A","group":1,"isGroup":true,"expanded":false,"category":"simple"},
{"key":"app-b","description":"App B","group":1,"isGroup":true,"expanded":false,"category":"simple"},
{"key":"app-c","description":"App C","group":1,"isGroup":true,"expanded":false,"category":"simple"},
{"key":"app-d","description":"App D","group":2,"isGroup":true,"expanded":false,"category":"simple"},
{"group": "app-a","name": "app-a-component1","canonical_name": "App A component 1","key": "app-a-component1","description": "component of App A","color": "#fadbd8","category": "simple","shape": "RoundRectangle","documentations": [{"title": "Docs","ref": "https://google.com"}],"icon": "image.png"},
{"group": "app-a","name": "app-a-component2","canonical_name": "App A component 2","key": "app-a-component2","description": "component of App A","color": "#fadbd8","category": "simple","shape": "RoundRectangle","documentations": [{"title": "Docs","ref": "https://google.com"}],"icon": "image.png"}
        ],
        linkDataArray: [
{"category": "simple","key": "b-to-a","from": "app-b","to": "app-a","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "c-to-a","from": "app-c","to": "app-a","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "d-to-a","from": "app-d","to": "app-a","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "ac1-to-d","from": "app-a-component1","to": "app-d","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]},
{"category": "simple","key": "ac2-to-b","from": "app-a-component2","to": "app-b","type": "API_CALL","documentations": [{"title": "Docs","ref": "google.com"}]}
        ]});
  }


    // save a model to and load a model from Json text, displayed below the Diagram
    function save() {
      var str = myDiagram.model.toJson();
      document.getElementById("mySavedModel").value = str;
    }
    function load() {
      var str = document.getElementById("mySavedModel").value;
      myDiagram.model = go.Model.fromJson(str);
    }
  </script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <div id="buttons">
    <button id="loadModel" onclick="load()">Load</button>
    <button id="saveModel" onclick="save()">Save</button>
  </div>
  <textarea id="mySavedModel" style="width:100%;height:200px">
  </textarea>
</body>
</html>

Thanks for the suggestions walter,

I have found the culprit. So after trying commenting, uncommenting layout presets, and isolating them, I have found that the every app level grouping like “driver-app” is using groupTemplateSimple which is nested inside context level grouping, in this case “LOGISTICS” which uses groupTemplateStructural.

Both are using LayeredDigraphLayout that specify direction: 180, after removing the preset for direction on the outer grouping (groupTemplateStructural), the nodes and groups inside are not being re-arranged and behaving as expected. I’m not sure if that was because the order between rendering and applying the layout.

I think the issue only happen if you have reached a certain amount nodes and links ( I have 54 nodes and 51 links in total inside LOGISTICS group)

        /**
         * Group simple template - Used for application level grouping 
         */
const groupTemplateSimple =  $(go.Group, "Auto", {
                contextMenu: groupContextMenuTemplate,
                avoidable: true,
                movable: false,
                layout: $(go.LayeredDigraphLayout, 
                      {
                          aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
                          linkSpacing: 50,
                          layerSpacing: 150,
                          layeringOption: go.LayeredDigraphLayout.LayerOptimalLinkLength,
                          direction:180,
                          setsPortSpots: false,
                          columnSpacing: 50,
                      }
                   ),
                  subGraphExpandedChanged: function(group){
                    if(group.isSubGraphExpanded){
                      group.layerName = 'Background';
                    }else{
                      group.layerName = 'Foreground';
                    }
                  },
                 click: highlight,
                },
                new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
                $(go.Shape, "Rectangle",
                        {   stroke: mappings[this.themeConfig].fill, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] },new go.Binding("fill", "color")),
                $(go.Panel, "Vertical",
                    { defaultAlignment: go.Spot.TopLeft, margin: 50},
                    $(go.Panel, "Horizontal",
                        { defaultAlignment: go.Spot.Center},
                        $("SubGraphExpanderButton"),
                        $(go.TextBlock, { font: "small-caps 25px Sans-Serif", margin: 4, stroke: mappings[this.themeConfig].groupText }, new go.Binding("text", "key")),
                    ),
                    $(go.Placeholder, { padding: 50 }),
                ),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

        /**
         * Group structural template - Used for pillar / business context level grouping
         */
        const groupTemplateStructural =  $(go.Group, "Auto", {
                avoidable: true,
                movable: false,
                layout: $(go.LayeredDigraphLayout,
                    {
                         aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
                         linkSpacing: 100,
                         layerSpacing: 100,
                         direction:180, // remove this line
                         layeringOption: go.LayeredDigraphLayout.LayerLongestPathSink,
                         setsPortSpots: false,
                         columnSpacing: 50,
                    },),
                  // If not expanded, put node on the foreground, else put on the background
                  // Prevent links to overlap the nodes
                  subGraphExpandedChanged: function(group){
                    if(group.isSubGraphExpanded){
                      group.layerName = 'Background';
                    }else{
                      group.layerName = 'Foreground';
                    }
                  },
                 click: highlight,
                },
                new go.Binding("isSubGraphExpanded", "expanded").makeTwoWay(),
                $(go.Shape,
                    new go.Binding("figure", "shape"),
                        { stroke: mappings[this.themeConfig].fill, fill: null, strokeWidth: 1, strokeCap: 'round', strokeDashArray: [5,10] }),

                    $(go.Panel, "Vertical",
                        { defaultAlignment: go.Spot.Left, padding: 10},
                        $(go.Panel, "Horizontal",
                            { defaultAlignment: go.Spot.TopLeft },
                            $("SubGraphExpanderButton"),
                            $(go.TextBlock, {stroke: mappings[this.themeConfig].fill, font: '14pt sans-serif', margin: new go.Margin(0,0,0,10) }, new go.Binding("text", "name"),
                            ),

                        ),
                        $(go.Placeholder, { padding: 100 })
                    ),
                new go.Binding("stroke", "isHighlighted", function(h)
                {
                  return h ? mappings[currentTheme].fill : mappings[currentTheme].fill;
                }).ofObject(),
                new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
        )

I can close this thread now, thanks!

Regards,
K.