Groups don't align with the grid/other nodes

Hi.

The centre point of my groups don’t seem to line up with the grid and that makes then not align to the nodes in the graph. Here’s a screenshot:

As you can see the links come in and out of the Source and Termination Nodes aligned with the grid. But the links in and out of the group are not aligned.

The nodes have those little black semi-circles as the ports and the links are set to connect to those using the toSpot: go.Spot.Right setting on the TableRow for each of the ports. A Node might have several ports so I’ve got a table with a single column and as many rows as needed to display a semi-circle for each port.

On the group I’ve set toSpot: go.Spot.Left on the entire group, not on a sub part (because I don’t have a similar black semi-circle to connect to).

What is the locationSpot of your Group and locationObjectName? I assume the resulting point in the graph must be mismatched for these different Groups, where the middle one is different, but I can’t be certain.

Hey,

The locationSpot for both the node template and group template is locationSpot: go.Spot.Center.

On the node it’s set on the Node object, whereas the toSpot/fromSpot are set on the table rows inside the node, I’m not sure if that matters?

Neither has a locationObjectName.

It’s not clear to me why they’d be snapping differently, if they have the same locationSpot and are the same size. Can I see the templates and some example data?

Hi Simon, sorry for the delay getting back to you. I’ve got something that might be useful, a demo of the problem happening.

I stripped back my group template to the bare bones and then slowly added things back in until the probably came back.

The issue seems to be with using a table with 2 rows, the first row being a title with a custom shape. If you take the first row out, then you can get the links to align, but with both, you can’t.

Does this help?

Thanks!

<!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’m a bit confused, both of these sets of nodes look OK, though the second set is not aligned with the grid only because the starting locations are not: the Y = 500 value is not divisible by 30, the grid cell size. If you used 480 they would look right. As soon as you drag them, they all seem correct.

Or am I looking at the wrong thing, here?

Whoops, that second set is for a different ticket that I have open. You can ignore them (but the bug exists for them too).

The problem is not with the nodes aligning, they always do. It’s the groups that dont:

The incoming link doesn’t align with the grid no matter how you drag it around.

But if you remove the header at the top, then it does.

Oh, I see what the issue is. I’m sorry for the confusion.

The problem is that you can specify a locationSpot (place in the Node that the location, which is what snaps, snaps to), but you can also specify a locationObjectName. By default the location object is the whole node… Unless it is a Group with a Placeholder, then the location object is always that placeholder.

That presents a problem here for grid snapping. Either you’ll have to compute the true center of the Group, and set an offset on the locationSpot so that it’s center-except-offset-by-that-amount. Or you will want to make sure the Placeholder is invisible in this collapsed state:

        $(go.Placeholder, { padding: 20 }, new go.Binding("visible", "isSubGraphExpanded", (isExpanded) => isExpanded).ofObject())

That alone might be sufficient, though I’m not sure what issues you’ll have here when expanded, since that placeholder could be a very different size.

I’ve added the binding to hide the placeholder when not expanded and it works!

I’m hoping that there won’t be an issue when exapnded because I don’t need it to align then, the links connect directly to the inner Nodes and those are already grid aligned.

Thanks!

Ah ok, I was wondering how you were going to handle the expanded case, but that’s a convenient solution.

I don’t think that works. See this screenshot:

The Placeholder is centered on a grid point. But because the nodes were offset by one cell vertically the member nodes will all be offset from the grid by half a cell height.

And when working with the code in the other topic, I didn’t need this binding to make the collapsed group’s link connection points be on the grid lines.

Ah! So setting the portId on the square part of the group will align the group collapsed or uncollapsed?

I didn’t get as far as trying that suggestion out, I’ll have to give it a go.

I think the solid white background of the group means that the unaligned nodes are not quite so egregious as the unaligned group. But I’ll check with the client :)

Thanks!

The element that is the port, whether the whole Group or just a Panel within it, does not affect how dragging the group will position it. The positioning is controlled by the Part.locationSpot on the Part.locationObject, and the object is controlled by Part.locationObjectName.

I was saying that the link connection point will appear in the (vertical) center of the bottom panel (row 1) of the group because that bottom panel is now the port that the link connects with. Since the spot on the port is vertically centered just as the location point is vertically centered on the location object, connecting link paths will be on the grid lines.