Group moves when expanded/collapsed

Hi. I have some odd behaviour with my groups moving significant distances when expanded.

What I’m seeing at the moment is that I can create a group from a selection of nodes, it starts off collapsed. The position of the collapsed group lines up with the left hand side of the nodes that were selected, so far so good. If you then select the group and expand it, the expanded group jumps (often to the left and down?). If you collapse the group it stays where it is, but if you then expand it a second time the expanded group jumps again. If you move the group after having expanded it then it will no longer jump around when collapsing. The very first expand always moves the group.

So I suspect that there’s an issue with positioning that gets resolved when the user moves the group and then “fixes” the position. I don’t fully understand though :)

I’ve read through other similar reports but I can’t find anything that resolves the problem.

I’ve turned off the auto layout features on the entire diagram by creating it like this:

let diagram = $(
        go.Diagram,
        elemId,
        {
            "undoManager.isEnabled": true,
            "draggingTool.isGridSnapEnabled": true,
            layout: $(
                go.LayeredDigraphLayout,
                { isInitial: false, isOngoing: false }
            )
        }
    );

This seems to mean that when a group is created it has no position (that makes sense), so I have a event listener like this:

diagram.addDiagramListener("SelectionGrouped", (evt) => {
        let firstNode = evt.subject.memberParts.first();
        evt.subject.move(firstNode.position);
    });

This seems to work, because the collapsed group is initially in the right position. But the expanded version of the group has a different position?

Any ideas? Thanks!

Does your group template include a Placeholder?
And what is the Group.layout?
Actually, what are all of the properties you set on the Group?

Hey! Thanks for the speedy response!

It does include a placeholder yes. I think the group doesn’t have a layout? (At least I haven’t set one directly).

Here are the properties I’m setting on my group template:

let groupTemplate = $(
    go.Group,
    "Table",
    {
        minSize: new go.Size(180, 0),
        isSubGraphExpanded: false,
        ungroupable: true,
        selectionAdorned: false,
        locationSpot: go.Spot.Center,
        toSpot: go.Spot.Left,
        fromSpot: go.Spot.Right,
    },
...
)

Each group starts off collapsed, yet you have disabled automatic layouts for the diagram. What is assigning the location for the group?

I think it’s the code that runs on the SelectionGrouped event? It positions the group in the same location as the first part contained within the group?

CommandHandler.groupSelection does not assign the new group’s location.

Group.layout defaults to an instance of Layout, so that will assign locations to all of its members that do not already have real locations when the group is expanded. Perhaps you are grouping nodes that are/were all members of a group before the grouping command?

CommandHandler.groupSelection does not assign the new group’s location.

Yes, that’s why I’ve got the SelectionGrouped event handler that sets the group’s location? Or am I misunderstanding what that’s doing?

Group.layout defaults to an instance of Layout, so that will assign locations to all of its members that do not already have real locations when the group is expanded.

And is that a location within the expanded group? Or a location within the document?

Perhaps you are grouping nodes that are/were all members of a group before the grouping command?

I don’t think I am? How could I check that? The SelectionGrouped event seems to fire after the grouping has happened? Is there a PreSelectionGrouped event?

I don’t know how your group template is defined, but it is quite probable that the position of the group will never be the same as the position of any of its member nodes. It is commonplace for there to be some offset between those two Points, due to Placeholder.padding or to objects outside of the placeholder within the group. So I don’t think that “SelectionGrouped” DiagramEvent listener is doing what you want.

The behavior of the base Layout is fairly stupid – it looks at each node that it is responsible for laying out and sees if its current location has a real value for the x and y properties. If the location is real, it ignores the node. If it is not real, then it moves the node into rows, which might overlap already located nodes.

Since your group has a placeholder, by definition all of the member nodes will be “within” the expanded group.

It seems that you are uncomfortable providing small screenshots showing the problem, before and after. If you would prefer communicating via email, send us email to GoJS at our domain, nwoods.com.

If all of selected nodes are members of a group, the new group created by the groupSelection command will also be made a member of that old existing group. But if any or all of the selected nodes are top-level nodes, the new group will be a top-level group too.

Hey. I can share some screenshots, no worries :)

If I start with a graph that looks like this:

I then group the middle two nodes:

The group isn’t perfectly positioned, but I’m happy with it, it’s not too bad. The problem is that if I now expand the group, this happens:

It’s moved loads!

What could be causing this? What determines the location of the collapsed group and the expanded group? Are they different locations?

I’m not sure how to reproduce the problem. Here’s what I tried. Could you try it and see how it’s different from your app (ignore irrelevant details like the contents of the nodes, groups, and links)?

<!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>

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

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      layout: $(go.LayeredDigraphLayout),
      "commandHandler.archetypeGroupData": { isGroup: true },
      "SelectionGrouped": e => e.subject.location = e.subject.memberParts.first().location,
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    { width: 100, height: 120, locationSpot: go.Spot.Center },
    $(go.Shape, { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.TextBlock,
      new go.Binding("text"))
  );

myDiagram.groupTemplate =
  $(go.Group, "Auto",
    { locationSpot: go.Spot.Center, ungroupable: true, isSubGraphExpanded: false },
    { doubleClick: (e, grp) => {
        if (grp.isSubGraphExpanded) {
          e.diagram.commandHandler.collapseSubGraph(grp);
        } else {
          e.diagram.commandHandler.expandSubGraph(grp);
        }
      }
    },
    $(go.Shape, { fill: "white" },
      new go.Binding("fill", "color")),
    $(go.Placeholder, { padding: 10, minSize: new go.Size(100, 80), alignment: go.Spot.Center }),
    $(go.TextBlock, { alignment: go.Spot.Top },
      new go.Binding("text", "key"))
  )

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, text: "Alpha", color: "lightblue" },
  { key: 2, text: "Beta", color: "orange" },
  { key: 3, text: "Gamma", color: "lightgreen" },
  { key: 4, text: "Delta", color: "pink" }
],
[
  { from: 1, to: 2 },
  { from: 2, to: 3 },
  { from: 3, to: 4 },
]);
  </script>
</body>
</html>

Oh, I didn’t set Layout.isInitial or isOngoing to false on the Diagram.layout. I just tried setting isOngoing to false and I believe it still doesn’t demonstrate the problem you are talking about. Though I’m not sure about that.

Hi. Sorry about the huge delay responding to you.

I used your demo and tried to see what the difference was that was causing my problem. As far as I can see it’s the SelectionGrouped handler.

I had it doing this:

let firstNode = evt.subject.memberParts.first();
evt.subject.move(firstNode.location);

But you used:

evt.subject.location = evt.subject.memberParts.first().location;

Changing over to your version has solved the problem (I think). The groups don’t seem to move around when expanded now.

Thanks!

3 posts were split to a new topic: Links not connecting with collapsed Group

Hey, I’m not sure if it’s useful to post here too, sorry if this is a bit spammy.

Here’s a demo of the node moving problem.

If you select some of the bottom four nodes, group them, and double click the group to expand it you’ll see the group move downwards. This doesn’t happen for the top four as much.

If you change the SelectionGrouped event handler so that it sets the group location instead of calling the move function then expanding the group doesn’t move the nodes.

<!DOCTYPE html>
<html>
<head>
    <title>Minimal GoJS Sample</title>
    <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
</head>
<body>
    <div id="diagram" style="border: solid 1px black; width:100%; height:600px"></div>
    <script src="https://unpkg.com/gojs@2.2.22"></script>
    <script id="code">
        const $ = go.GraphObject.make;

        const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
        go.Shape.defineFigureGenerator("HalfEllipse", function (shape, w, h) {
            return new go.Geometry()
                .add(new go.PathFigure(0, 0, true)
                    .add(new go.PathSegment(go.PathSegment.Bezier, w, .5 * h, KAPPA * w, 0, w, (.5 - KAPPA / 2) * h))
                    .add(new go.PathSegment(go.PathSegment.Bezier, 0, h, w, (.5 + KAPPA / 2) * h, KAPPA * w, h).close()))
                .setSpots(0, 0.156, 0.844, 0.844);
        });
        go.Shape.defineFigureGenerator("RoundedTopRectangle", function (shape, w, h) {
            // this figure takes one parameter, the size of the corner
            let p1 = 5;  // default corner size
            if (shape !== null) {
                let param1 = shape.parameter1;
                if (!isNaN(param1) && param1 >= 0) p1 = param1;  // can't be negative or NaN
            }
            p1 = Math.min(p1, w / 2);
            p1 = Math.min(p1, h / 2);  // limit by whole height or by half height?
            let geo = new go.Geometry();
            // a single figure consisting of straight lines and quarter-circle arcs
            geo.add(new go.PathFigure(0, p1)
                .add(new go.PathSegment(go.PathSegment.Arc, 180, 90, p1, p1, p1, p1))
                .add(new go.PathSegment(go.PathSegment.Line, w - p1, 0))
                .add(new go.PathSegment(go.PathSegment.Arc, 270, 90, w - p1, p1, p1, p1))
                .add(new go.PathSegment(go.PathSegment.Line, w, h))
                .add(new go.PathSegment(go.PathSegment.Line, 0, h).close()));
            // don't intersect with two top corners when used in an "Auto" Panel
            geo.spot1 = new go.Spot(0, 0, 0.3 * p1, 0.3 * p1);
            geo.spot2 = new go.Spot(1, 1, -0.3 * p1, 0);
            return geo;
        });

        const diagram = $(
            go.Diagram,
            "diagram",
            {
                layout: $(go.LayeredDigraphLayout, { isInitial: false, isOngoing: false }),
                "commandHandler.archetypeGroupData": { isGroup: true },
                "SelectionGrouped": e => {
                    // e.subject.location = e.subject.memberParts.first().location;
                    e.subject.move(e.subject.memberParts.first().location, true);
                },
                "undoManager.isEnabled": true,
                "draggingTool.isGridSnapEnabled": true,
            }
        );

        diagram.grid =
            $(go.Panel, "Grid",
                { visible: true, gridCellSize: new go.Size(30, 30) },
                $(go.Shape, "LineH", { stroke: "#E8E8E8" }),
                $(go.Shape, "LineV", { stroke: "#E8E8E8" }),
            );

        const inPortTemplate = $(
            go.Panel,
            "TableRow",
            {},
            $(
                go.Shape,
                "HalfEllipse",
                {
                    angle: 180,
                    cursor: "pointer",
                    fromLinkable: false,
                    fromMaxLinks: 1,
                    toSpot: go.Spot.Right,
                    margin: new go.Margin(5, -1, 5, 0),
                    toLinkable: true,
                    toMaxLinks: 1,
                    fill: "black",
                    desiredSize: new go.Size(9, 18),
                },
                new go.Binding("portId", ""),
            )
        );

        const outPortTemplate = $(
            go.Panel,
            "TableRow",
            {},
            $(
                go.Shape,
                "HalfEllipse",
                {
                    cursor: "pointer",
                    fromLinkable: true,
                    fromMaxLinks: 1,
                    fromSpot: go.Spot.Right,
                    margin: new go.Margin(5, 0, 5, -1),
                    toLinkable: false,
                    toMaxLinks: 1,
                    fill: "black",
                    desiredSize: new go.Size(9, 18),
                },
                new go.Binding("portId", ""),
            )
        );

        diagram.nodeTemplate = $(
            go.Node,
            "Horizontal",
            new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
            { locationSpot: go.Spot.Center },
            $(
                go.Panel,
                "Table",
                {
                    itemTemplate: inPortTemplate,
                },
                new go.Binding("itemArray", "inPortIds")
            ),
            $(
                go.Panel,
                "Auto",
                {
                    desiredSize: new go.Size(80, 80),
                },
                $(
                    go.Shape,
                    { fill: "white" },
                    new go.Binding("fill", "color")
                ),
                $(go.TextBlock, new go.Binding("text")),
            ),
            $(
                go.Panel,
                "Table",
                {
                    itemTemplate: outPortTemplate,
                },
                new go.Binding("itemArray", "outPortIds")
            ),
        );

        diagram.groupTemplate = $(
            go.Group,
            "Table",
            {
                locationSpot: go.Spot.Center,
                ungroupable: true,
                isSubGraphExpanded: false,
                toSpot: go.Spot.Left,
                fromSpot: go.Spot.Right,
            },
            new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
            { doubleClick: (e, grp) => {
                    if (grp.isSubGraphExpanded) {
                        e.diagram.commandHandler.collapseSubGraph();
                    } else {
                        e.diagram.commandHandler.expandSubGraph();
                    }
                }
            },
            // If you remove this heading row then you can get the group to align
            $(
                go.Panel,
                "Auto",
                {
                    stretch: go.GraphObject.Horizontal,
                    row: 0,
                    column: 0,
                },
                $(
                    go.Shape,
                    "RoundedTopRectangle",
                    {
                        strokeWidth: 2,
                        stroke: "black",
                        margin: new go.Margin(0, 0, -1.5, 0),
                        fill: "white",
                    },
                ),
                $(
                    go.Panel,
                    "Vertical",
                    {
                        padding: 12,
                        stretch: go.GraphObject.Fill
                    },
                    $(
                        go.TextBlock,
                        "Default Text",
                        {
                            isMultiline: false,
                            alignment: go.Spot.Center,
                            wrap: go.TextBlock.None,
                            overflow: go.TextBlock.OverflowEllipsis,
                            maxSize: new go.Size(175, Infinity),
                            text: "Group Title"
                        },
                    ),
                ),
            ),
            $(
                go.Panel,
                "Auto",
                {
                    stretch: go.GraphObject.Horizontal,
                    row: 1,
                    column: 0,
                },
                $(
                    go.Shape,
                    "Square",
                    {
                        strokeWidth: 2,
                        stroke: "black",
                        fill: "white"
                    },
                ),
                $(
                    go.Panel,
                    "Vertical",
                    {
                        stretch: go.GraphObject.Horizontal,
                        padding: new go.Margin(8, 0, 5, 0),
                    },
                    new go.Binding("visible", "isSubGraphExpanded", (isExpanded) => !isExpanded).ofObject(),
                    $(
                        go.Panel,
                        "Vertical",
                        {
                            name: "icon",
                            margin: new go.Margin(0, 10, 0, 10),
                        },
                        $(go.TextBlock, {text: "This is a group"}),
                        $(go.TextBlock, {text: "On two lines"}),
                    ),
                ),
                $(go.Placeholder, { padding: 20 })
            )
        );

        diagram.linkTemplate = $(
            go.Link,
            {
                selectionAdorned: false,
                routing: go.Link.Normal,
                relinkableTo: true,
                fromEndSegmentLength: 10,
                toEndSegmentLength: 10,
            },
            $(
                go.Shape,
                {
                    strokeWidth: 3,
                },
            )
        );

        diagram.model = new go.GraphLinksModel(
            [
                { key: 1, text: "Alpha", color: "lightblue", location: "-300 0", inPortIds: [], outPortIds: ["out"]},
                { key: 2, text: "Beta", color: "orange", location: "-150 0", inPortIds: ["in"], outPortIds: ["out"]},
                { key: 3, text: "Gamma", color: "lightgreen", location: "0 0", inPortIds: ["in"], outPortIds: ["out"]},
                { key: 4, text: "Delta", color: "pink", location: "150 0", inPortIds: ["in"], outPortIds: [] },

                // If you remove these nodes then expanding a group doesn't cause such a large movement
                { key: 5, text: "Alpha", color: "lightblue", location: "-300 500", inPortIds: [], outPortIds: ["out"]},
                { key: 6, text: "Beta", color: "orange", location: "-150 500", inPortIds: ["in"], outPortIds: ["out"]},
                { key: 7, text: "Gamma", color: "lightgreen", location: "0 500", inPortIds: ["in"], outPortIds: ["out"]},
                { key: 8, text: "Delta", color: "pink", location: "150 500", inPortIds: ["in"], outPortIds: [] }
            ],
            [
                { from: 1, to: 2, fromPort: "out", toPort: "in" },
                { from: 2, to: 3, fromPort: "out", toPort: "in" },
                { from: 3, to: 4, fromPort: "out", toPort: "in" },

                { from: 5, to: 6, fromPort: "out", toPort: "in" },
                { from: 6, to: 7, fromPort: "out", toPort: "in" },
                { from: 7, to: 8, fromPort: "out", toPort: "in" },
            ]
        );
        diagram.model.linkFromPortIdProperty = "fromPort";
        diagram.model.linkToPortIdProperty = "toPort";
    </script>
</body>
</html>

I’ve answered at: Links not connecting with collapsed Group - #5 by walter