How to make the nodes added to the group horizontally centered when the group uses the go.GridLayout layout

Hey guys!
Now there is a requirement in my project that every time I add a node to a group that has a go.GridLayout layout, the nodes must be centered in the group. How do I implement this?

Thanks

If you are using a Placeholder in your Group template, then you just need to arrange that Placeholder to be centered the way that you want. This is exactly the same process as centering any other element in a Panel.

Thanks, I set the alignment in the Placeholder: go.Spot.Center can make the nodes added to the group centered horizontally, but each time the node is added to the group, the group will shift to the lower right corner. What can I do? When I add a node to a group, the group does not shift and stays fixed

Could you provide your Group template and small screenshots showing before and after?

When the node is placed in two nested groups, the group does not shift

When groups are added to a single group, the group is shifted to the right

myDiagram.groupTemplateMap.add("OfGroups",
                $(go.Group, "Auto",
                    {
                        name: "Groups",
                        background: "transparent",
                        computesBoundsAfterDrag: true,

                        mouseDrop: function (e, obj) {
                            finishDrop(e, obj);
                        },

                        layout: $(go.GridLayout,
                            {
                                alignment: go.GridLayout.Position,
                                wrappingColumn: 5,
                                cellSize: new go.Size(1, 1),
                                spacing: new go.Size(4, 4)
                            }
                        ),
                    },
                    nodeStyle(),
                    $(go.Shape, "Rectangle",
                        {
                            fill: null,
                            stroke: "#ff913a",
                            strokeWidth: 2,
                            cursor: "pointer",
                            fromLinkable: true,
                            toLinkable: true,
                            fromLinkableSelfNode: true,
                            toLinkableSelfNode: true,
                            fromLinkableDuplicates: true,
                            toLinkableDuplicates: true
                        }),

                        $(go.Panel, "Vertical",
                        {
                            name: "PanelVertical",
                            minSize: new go.Size(50, 50),
                        },
                        new go.Binding("minSize", "minSize").makeTwoWay(),
                        $(go.Panel, "Table",
                            {
                                stretch: go.GraphObject.Horizontal,
                                background: "#FFAA99"
                            },
                            $(go.TextBlock,
                                {
                                    margin: 5,
                                    isMultiline: false,
                                    font: "bold 18px sans-serif",
                                    opacity: 0.75,
                                    stroke: "#404040"
                                },
                                new go.Binding("text", "text").makeTwoWay(),
                                new go.Binding("font", "font").makeTwoWay()
                            )
                        ),
                        $(go.Placeholder,
                            {
                                padding: 5,
                                alignment: go.Spot.Center
                            })
                    )
                ));

            function finishDrop(e, grp) {

                if (grp !== null) {

                    var gloc = grp.location.copy();

                    var ok = grp.addMembers(e.diagram.selection, true);

                    if (ok) {
                        var newbnds = e.diagram.computePartsBounds(e.diagram.selection);
                        e.diagram.moveParts(e.diagram.selection, gloc.subtract(newbnds.position), false);

                    } else {
                        e.diagram.currentTool.doCancel();
                    }

                } else {
                    var ok = e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true);
                    if (!ok) e.diagram.currentTool.doCancel();
                }
            }
            
            function nodeStyle() {
                return [
                    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
                    {
                        locationSpot: go.Spot.Center,

                        mouseEnter: function (e, obj) {
                            showPorts(obj.part, true);
                        },

                        mouseLeave: function (e, obj) {
                            showPorts(obj.part, false);
                        }
                    }
                ];
            }

The location of a Group that has a Placeholder is determined entirely by the union of the bounds of the member Nodes that are in the group.

A GridLayout that is a Group.layout positions its nodes starting at the top-left corner of the placeholder’s original position plus Placeholder.padding. Thus when layouts happen the position of the placeholder, and hence the position of the group, does not change. This is what most apps want.

However in your case you want to keep a varying width placeholder horizontally centered in a group. If you want the position of the group not to change, that would require changing the position at which the layout places its nodes. What’s worse is that if you have different width member nodes, it cannot know the final width until after the layout has been completed, and thus it cannot know beforehand the placeholder’s relative horizontal position within the group.

My guess right now is that you implement a custom GridLayout that overrides GridLayout.commitLayers (it’s not documented but is like commitLayers in other layouts; ignore its arguments) and moves all of the member nodes so that the horizontal center point is maintained.

By the way, on an unrelated subject: your finishDrop function that moves the selection has very unusual behavior. Are you sure that you want that behavior?

Thank you very much for your answer,But I don’t quite understand what you mean

Could you please be more specific about what you don’t understand and what we could do to help you?

When I set the alignment: go.Spot.Center for placeholders in the group, I add nodes to the group, the nodes can be centered in the group, but when I add a node to the group, the group will shift to the right, what do I do? Can be achieved, the node can be centered in the group when adding a node to the group, and the group is not offset

<!DOCTYPE html>
<html>
<head>
    <title>GoJS expt</title>
    <meta charset="UTF-8">
    <style>
        .save {
            margin: 10px 0 10px 0;
        }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gojs/1.8.9/go-debug.js"></script>
    <script id="code">
        function init() {
            var $ = go.GraphObject.make;
            myDiagram =
                $(go.Diagram, "myDiagramDiv",
                    {
                        allowDrop: true,
                        initialContentAlignment: go.Spot.Center,

                        mouseDrop: function (e) {
                            finishDrop(e, null);
                        }
                    });

            function finishDrop(e, grp) {
                if (grp !== null) {
                    var gloc = grp.location.copy();
                    var ok = grp.addMembers(e.diagram.selection, true);

                    if (ok) {
                        var newbnds = e.diagram.computePartsBounds(e.diagram.selection).addMargin(grp.placeholder.padding);
                        e.diagram.moveParts(e.diagram.selection, gloc.subtract(newbnds.position), false);
                    } else {
                        e.diagram.currentTool.doCancel();
                    }
                } else {
                    var ok = e.diagram.commandHandler.addTopLevelParts(e.diagram.selection, true);
                    if (!ok) e.diagram.currentTool.doCancel();
                }
            }

            myDiagram.groupTemplateMap.add("OfGroups",
                $(go.Group, "Auto",
                    {
                        background: "transparent",
                        computesBoundsAfterDrag: true,
                        computesBoundsIncludingLocation: true,
                        handlesDragDropForMembers: true,
                        mouseDrop: finishDrop,
                        layout:
                            $(go.GridLayout,
                                {
                                    wrappingColumn: 3,
                                    alignment: go.GridLayout.Position,
                                    cellSize: new go.Size(1, 1),
                                    spacing: new go.Size(4, 4),
                                    sorting: go.GridLayout.Forward
                                })
                    },
                    new go.Binding("background", "isHighlighted", function (h) {
                        return h ? "rgba(255,0,0,0.2)" : "transparent";
                    }),
                    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
                    $(go.Shape, "Rectangle",
                        {
                            fill: null,
                            stroke: "#FFDD33",
                            strokeWidth: 2
                        }),
                    $(go.Panel, "Vertical",
                        {
                            minSize: new go.Size(200, 60)
                        },
                        $(go.Panel, "Table",
                            {
                                stretch: go.GraphObject.Horizontal,
                                minSize: new go.Size(200, 20),
                                background: "#FFDD33"
                            },
                            $(go.TextBlock,
                                {
                                    alignment: go.Spot.Center,
                                    margin: 5
                                },
                                new go.Binding("text", "text").makeTwoWay())
                        ),
                        $(go.Placeholder,
                            {
                                padding: 5,
                                alignment: go.Spot.Center
                            })
                    )
                ));

            myDiagram.nodeTemplate =
                $(go.Node, "Auto",
                    {
                        mouseDrop: function (e, nod) {
                            finishDrop(e, nod.containingGroup);
                        }
                    },
                    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
                    $(go.Shape, "Rectangle",
                        {},
                        new go.Binding("fill", "color")),
                    $(go.TextBlock,
                        {
                            margin: 5
                        },
                        new go.Binding("text", "text").makeTwoWay())
                );

            myPalette =
                $(go.Palette, "myPaletteDiv",
                    {
                        nodeTemplateMap: myDiagram.nodeTemplateMap,
                        groupTemplateMap: myDiagram.groupTemplateMap,
                        layout: $(go.GridLayout,
                            {
                                wrappingColumn: 1,
                                alignment: go.GridLayout.Position
                            })
                    });
            myPalette.model = new go.GraphLinksModel([
                {text: "lightgreen", color: "#ACE600"},
                {text: "yellow", color: "#FFDD33"},
                {text: "lightblue", color: "#33D3E5"}
            ]);
            load();
        }

        function load() {
            myDiagram.model = go.Model.fromJson(document.querySelector("#mySavedModel").value);
        }
        
        function save() {
            document.querySelector("#mySavedModel").value = myDiagram.model.toJson();
        }
    </script>
</head>
<body onload="init()">
<div id="sample">
    <div style="width: 50%; display: flex; justify-content: space-between">
        <div id="myPaletteDiv"
             style="width: 100px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
        <div id="myDiagramDiv" style="flex-grow: 1; height: 500px; border: solid 1px black"></div>
    </div>
    <button class="save" onclick="save()" style="display:none">保存</button>
    <div>
        <textarea id="mySavedModel" cols="30" rows="10" style="display:none">
            { "class": "go.GraphLinksModel",
  "nodeDataArray": [
{"key":1, "text":"Main", "isGroup":true, "category":"OfGroups", "loc":"-82.99999999999997 -35.48784179687501"},
{"key":-1, "text":"A", "color":"#ACE600", "loc":"-174.00000000000003 -99.99999999999996"}
 ],
  "linkDataArray": []}
        </textarea>
    </div>
</div>
</body>
</html>

I don’t think there’s an easy answer if you want to continue using a Placeholder.

If you wanted the nodes to stay in the top-left corner of an area of the group, then in addition to setting the alignment of the Placeholder within its Panel, you only need to set Group.computesBoundsIncludingLocation to true. But that was covered in a different forum topic: How to keep the group fixed

If I want to make the node centered in the group when I add a node to the group, and I can’t achieve it by adding the node group to the group and keeping it still?

Well, another way of achieving that is by turning off the Group.layout.isOngoing property and explicitly computing and setting the desired position of the new node and the old nodes so that the group does not shift.

But I think it is easier and more general to make the layout smarter so that it positions its layout so that the group stays where you want it to be.

By the way, since what you are asking for is so reasonable but not easily achieved, we’re considering adding some functionality in version 2 so that you can declare the behavior that you want without having to customize any layout.

Thank you very much for your answer

Hey Walter, just wanted to ask - was any new functionality added in v2 to make this kind of situation easier?

No, I don’t think there has been.

Here’s some code that I think implements what was originally asked for. It does not use any new functionality in GoJS. Well, at least none that is relevant to the problem.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Shape, { fill: "white" }),
    $(go.TextBlock,
      { margin: 8, editable: true },
      new go.Binding("text").makeTwoWay())
  );

// Assume this is used as a Group.layout
class CenteringGridLayout extends go.GridLayout {
  // As an extra step in doLayout, shift all of the Group.memberParts so that the
  // whole group remains horizontally centered at the same point in document coordinates
  commitLayers(layerRects, offset) {
    if (this.group === null) return;
    // BUG!? when the first member node is added, the group's actualBounds have been updated already,
    // even before doLayout is called.  That causes the group's location to shifted before
    // this layout can try to maintain the location.x value of the group
    const x = this.group.actualBounds.centerX;
    const mbnds = this.diagram.computePartsBounds(this.group.memberParts);
    if (!isNaN(x) && mbnds.isReal()) {
      const dx = x - mbnds.centerX;
      this.diagram.moveParts(this.group.memberParts, new go.Point(dx, 0));
    }
  }
}  // end CenteringGridLayout

myDiagram.groupTemplate =
  $(go.Group, "Vertical",
    {
      locationSpot: go.Spot.Top,  // must have Spot.x === 0.5
      layout: $(CenteringGridLayout, { wrappingColumn: 6 }),
      doubleClick: (e, grp) => {  // add a node to the group
        const ctr = grp.getDocumentPoint(go.Spot.Center);  // start new node at center of group
        e.diagram.model.commit(m => {
          const loc = grp.location.copy();
          m.addNodeData({ text: "new node", group: grp.key, loc: go.Point.stringify(ctr) });
          // BUG: the next two lines (and remembering the original location, above)
          // are necessary to keep the group at the original location,
          // for the first 2 additions to the group
          grp.ensureBounds();
          grp.move(loc, true);
        });
      }
    },
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.TextBlock, new go.Binding("text")),
    $(go.Panel, "Auto",
      $(go.Shape, { fill: "#00000008", minSize: new go.Size(400, NaN) }),
      $(go.Placeholder, { padding: 10, alignment: go.Spot.Top })
    )
  );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 0, text: "Double click to add a member node", isGroup: true, loc: "0 0" },
]);
  </script>
</body>
</html>
1 Like