Issue with group node layout when more than one group node is present in the diagram

Hi,

In my go js diagram, there is a need for a group node which can be expanded/ collapsed when user clicks on Hide/ Show link on the group node. And I am using TreeLayout for the group node.
Also, when a group node is added, it automatically, adds two nodes to that group (Start -> End), which works perfectly fine, when a group node is added for the first time to the diagram. See below snapshot:

The real problem is when user adds another instance of the group node to the diagram. In this scenario, recently added group node’s layout is distorted and it appears as below:

If user deletes the previously added group node, then the new group’s layout is automatically adjusted to proper TreeLayout.

Not sure whether I am missing any thing here.

That’s surprising. How did you add the second group? Did you make all of the changes within a single transaction?

So it’s the same piece of code that adds a group node of this type to diagram every time. But everytime it generates a unique key property (generated on server-side) for every new node added to the diagram.
I don’t see any issues w.r.t data also as this is an excerpt from the model that gets added, whenever a group node is added to the diagram:
Below nodes are added to nodeDataArray:

{
    "key" : "404c08e153a46c100b0cddeeff7b1286",
    "node_id" : "56f50a0c5360e4100b0cddeeff7b12b8",
    "name" : "Simple For Loop",
    "isGroup" : true
  }, {
    "key" : "start_node_404c08e153a46c100b0cddeeff7b1286",
    "node_id" : "a5097e39532310100b0cddeeff7b1250",
    "name" : "Start",
    "group" : "404c08e153a46c100b0cddeeff7b1286"
  }, {
    "key" : "end_node_404c08e153a46c100b0cddeeff7b1286",
    "node_id" : "6d393279532310100b0cddeeff7b1202",
    "name" : "End",
    "group" : "404c08e153a46c100b0cddeeff7b1286"
  }

And to linkDataArray,

{
    "from" : "start_node_404c08e153a46c100b0cddeeff7b1286",
    "to" : "end_node_404c08e153a46c100b0cddeeff7b1286",
    "edge_id" : "539bb9bb532310100b0cddeeff7b1207",
    "from_port" : "49de92bc535c20100b0cddeeff7b12f9",
    "to_port" : "c0bed67c535c20100b0cddeeff7b12fc"
  }

That looks OK.

Do you have multiple ports per node? If so, you aren’t recording the port identifiers in the node data objects. If not, why include “from_port” and “to_port” in the link data object?

What are “node_id” and “edge_id”? Do you want to consider using those as the keys for the data in your model? If so, set Model.nodeKeyProperty to “node_id” and GraphLinksModel.linkKeyProperty to “edge_id”. And then you won’t need to set “key”, and your “from” and “to” will need to be your identifiers.

But these issues should not affect the layout, unless maybe the nodes and links in the diagram aren’t what you think they are.

Yes, each node will have one input port and one output port. I shared the JSON that gets stored in DB. We don’t store portIds of nodes as they would be derived on the fly and go js Bindings for portId would be added with appropriate port Ids.

node_id and edge_id are used to derive the (node/ group/ link) template of given node or edge. I have set these to Model.nodeCategoryProperty and Model.linkCategoryProperty on model to override default category property name.

node_id and edge_id are not unique across various nodes/ edges as diagram can have more than one node which share same template. For instance, if we have two “Simple For Loop” group nodes, for both of them node_id would be the same. so we still need unique identifier, “key” on each node.

I just tried this function for adding groups containing two nodes connected by a link. I didn’t bother with “node_id” and “edge_id” and “from_port” and “to_port”, since the details of the templates shouldn’t matter here.

  var myCounter = 1001;
  function test() {
    myDiagram.model.commit(function(m) {
      var k = myCounter++;
      m.addNodeData({
        "key" : k,
        "name" : "Simple For Loop",
        "isGroup" : true
      });
      m.addNodeData({
        "key" : "start_node_" + k,
        "name" : "Start",
        "group" : k
      });
      m.addNodeData({
        "key" : "end_node_" + k,
        "name" : "End",
        "group" : k
      });
      m.addLinkData({
        "from" : "start_node_" + k,
        "to" : "end_node_" + k
      });

      m.addLinkData({ from: 1, to: k });
      if (k > 1001) {
        m.addLinkData({ from: k, to: k-1 });
        var prevgroup = myDiagram.findNodeForKey(k-1);
        if (prevgroup !== null) {
          var inlink = prevgroup.findLinksInto().first();
          if (inlink !== null) m.removeLinkData(inlink.data);
        }
      }
    });
  }

It seems to work well, so I cannot explain why your code to insert a group doesn’t work well. Can you produce a minimal reproducible example?

Here is a sample code that adds group along with start/ end nodes to it.

function addNode(diagram, newNodeData, newNodeConnectors, linkData, shapeTemplatePropertyMap) {
	var _addLoopTemplate = function() {
		diagram.model.commit(function (model) {
		debugger;
            // Now that we added loop node to the diagram, add Start and end nodes to the loop node.
            var startNodeKey = 'start_node_' + newNodeData.key;
            var endNodeKey = 'end_node_' + newNodeData.key;
			var startNodeTemplateId = 'b1d83e39532310100b0cddeeff7b1227';
			var endNodeTemplateId = 'f4988e3053d420100b0cddeeff7b1251';
            var startNode = Object.assign({
                    key: startNodeKey,
                    node_id: 'a5097e39532310100b0cddeeff7b1250',
                    name: 'Start',
                    templateId: startNodeTemplateId,
                    group: newNodeData.key

                }, shapeTemplatePropertyMap[startNodeTemplateId] // Shape template visual properties.
            );
            var endNode = Object.assign({
                    key: endNodeKey,
                    node_id: '6d393279532310100b0cddeeff7b1202',
                    name: 'End',
                    templateId: startNodeTemplateId,
                    group: newNodeData.key

                }, shapeTemplatePropertyMap[endNodeTemplateId] // Shape template visual properties.
            );

            // Create link data that connects start and end node.
            var linkFromStartToEnd = Object.assign({
                    from: startNodeKey,
                    to: endNodeKey,
                    templateId: llinkData.edge_id,
					fromPort: '49de92bc535c20100b0cddeeff7b12f9',
					toPort: 'c0bed67c535c20100b0cddeeff7b12fc',
                }, shapeTemplatePropertyMap[linkData.templateId] // Shape template visual properties.
            );
		
	model.addNodeData(startNode);
            model.addNodeData(endNode);
            model.addLinkData(linkFromStartToEnd);
		}, "Adding loop start and end nodes to the loop node");
	};

   // Add loop node first.
    diagram.model.commit(function(model) {
        if (linkData) {
            var nextNodeToPort = linkData.toPort;

            model.addNodeData(newNodeData); // Add new node to the diagram.

            // Extract input/ output connectors of loop node.
            var toConnector, fromConnector;

            for (var i = 0; i < newNodeConnectors.length; i++) {
                if (newNodeConnectors[i].direction === "input")
                    toConnector = newNodeConnectors[i];
                else if (newNodeConnectors[i].direction === "output") {
                    fromConnector = newNodeConnectors[i];
                }
            }

            // Create link data that connects new node to the next node.
            var newLinkData = Object.assign({
                    from: newNodeData.key,
                    to: linkData.to,
                    templateId: linkData.edge_id
                }, shapeTemplatePropertyMap[linkData.templateId] // Shape template visual properties.
            );

            // If from connector is available for the new node, add fromPort property to the new link.
            if (fromConnector)
                newLinkData.fromPort = fromConnector.sysId;
            // If to port is available for the next node, add toPort property to the new link.
            if (nextNodeToPort)
                newLinkData.toPort = nextNodeToPort;
            //Add new link from then port to the diagram
            model.addLinkData(newLinkData);

            // Relink existing link to new node.
            model.setDataProperty(linkData, 'to', newNodeData.key);
            // If to connector is available for the new node, add toPort property to the existing link.
            model.setDataProperty(linkData, 'toPort', toConnector.sysId);
        } else
            model.addNodeData(newNodeData); // Add new node to the diagram.
    }, "Adding Loop node to the diagram");
	//setTimeout(function() {
	_addLoopTemplate();
//}, 0);
	debugger;
}

Originally I added everything in one single transaction. Later I moved them two transactions, suspecting that might be the reason for this issue - so add group node first and then add start/ end nodes to it.
You can ignore any logic on ports as that works perfectly fine.

You shouldn’t need two separate transactions.

If you interactively examine the diagram that results from adding two or more groups, can you tell if the only problem is that the layout didn’t complete correctly? Does each group actually have two separate member nodes connected by a link? So the problem in your second screenshot is that the End node is overlapping with the Start node? If you move the End node, can you see the Start node and the link between them?

If you disable layout animation by setting AnimationManager.isEnabled to false when you initialize the Diagram, does the problem go away?

Setting AnimationManager.isEnabled to false, didn’t resolve the issue. Yes, problem seems to be with the layout. And as you said, End node is overlapping with the Start node.Because, when I delete one group node (which is rendered properly), the other one’s layout gets rendered properly.

And I will move everything to one single transaction, as that’s not the issue.

What is your group template? What is your node template?

Here is the group template:


$(go.Group, "Spot", {
ungroupable: false,
layout: $(go.TreeLayout, { angle: 90, nodeSpacing: 100, layerSpacing: 80, alternateAngle: 90 }),
},
$(go.Panel, "Table", {
            background: "#ffffff",
            minSize: new go.Size(250, 100)
        },
        $(go.Shape, {
            fill: "#e7a679",
            strokeWidth: 0,
            width: 30,
            stretch: go.GraphObject.Vertical
        }),
        $(go.Panel, "Vertical", {
                column: 2
            },
            $(go.TextBlock, {
                    margin: new go.Margin(10, 0, 0, 10),
                    minSize: new go.Size(200, NaN),
                    alignment: go.Spot.Left,
                    font: "11pt Lato, Helvetica, Arial, sans-serif"
                },
                new go.Binding("text", "name")),
            $(go.TextBlock, new go.Binding("text", "nodeSubCategory"), {
                margin: new go.Margin(0, 0, 10, 10),
                alignment: go.Spot.Left,
                minSize: new go.Size(200, NaN),
                stroke: "grey",
                font: "9pt Lato, Helvetica, Arial, sans-serif"
            }),
            $(go.Panel, "Auto", {
                    margin: 5,
                    defaultAlignment: go.Spot.Center
                },
                $(go.Shape, "RoundedRectangle", {
                    fill: "#f6f4f6",
                    strokeWidth: 0
                }),
                $(go.Placeholder, {
                    padding: 20,
                    minSize: new go.Size(200, NaN)
                })
            ),
            $(go.TextBlock, {
                    name: "showHideLink",
                    cursor: "pointer",
                    alignment: go.Spot.Left,
                    margin: 5,
                    isUnderline: true,
                    stroke: "royalblue",
                    click: function(e, tb) {
                        var group = tb.part;
                        if (group.isSubGraphExpanded) {
                            group.diagram.commandHandler.collapseSubGraph(group);
                        } else {
                            group.diagram.commandHandler.expandSubGraph(group);
                        }
                        setTimeout(function() {
                            group.diagram.select(group);
                        }, 0);
                    }
                },
                new go.Binding("text", "isSubGraphExpanded",
                    function(exp) {
                        return exp ? "Hide" : "Show";
                    }).ofObject()
            )
        )
    )
)

There are various types of node templates (in addition to start/ end nodes), which can be added to group node. And issue happens for any node template.

Also, is there a clean way to collapse group node, on load itself, instead of user clicking on Hide link after it is loaded?

I looked at API documentation through entire class hierarchy, GraphObject->Panel->Part->Node->Group, for some event or hook method, which gives control on render, but couldn’t find any.

Surprisingly, when we nest group nodes, I don’t see any layout distortions. See screenshot below:

So the issue is only when we have more than one instance of group node (not nested) in the diagram.

Set Group.isSubGraphExpanded to false in the template.

Any other suggestions on the original layout distortion issue would be quite helpful :)

I just used your Group template, completely unmodified, in my sample app, using the code that I posted above for adding groups, also unmodified. Everything worked well. So I cannot explain why it does not work for you.