Links not connecting with collapsed Group

Although I have noticed now that the links don’t get drawn properly? They seem like they stay where they are until I drag the group or the connected nodes around a bit.

The incoming link seems fine? But I think that’s because I’ve positioned the group to be where the first group member is and that mostly seems to be the left-most node and the mostly the one that has the incoming link.

Here’s a screenshot:

Can you provide a minimal stand-alone sample so that we can reproduce the problem? I really don’t know anything about any of the node/group/link templates that you are using, nor any Group.layout and Diagram.layout.

Yes, that makes sense. I’ll try to get that done this week.

Hi. I’ve got a demo of the problem for you.

If you group some nodes you’ll see that the final outgoing link doesn’t get redrawn. I think the incoming link doesn’t either, it’s just harder to see.

If you change the SelectionGrouped event handler so that it sets the group location instead of calling the move function then the links get re-drawn, but then expanding the nodes causes the group to move (see other post).

Hopefully this is a helpful demo?

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>

Thank you very much for providing a simple stand-alone example.

If you set

portId: “”

on the group’s row 1 panel, I think the link point for the group will naturally be the middle of that square area, rather than for the whole node including the header. You probably also want to move those fromSpot and toSpot settings that were on the Group (which was the default port) to that panel with the portId setting.

Part of the problem with locations is that a collapsed group has invisible members, so those members don’t help locate or size the group. That’s why in the absence of a Diagram.layout (well, it’s disabled due to isOngoing false) the group won’t have any real location. Which is why you implemented a “SelectionGrouped” DiagramEvent listener. That’s good. But you don’t want to move the new group because that would also shift the member nodes.

Here’s what I would do:

      "SelectionGrouped": e => {
        const grp = e.subject;
        const b = e.diagram.computePartsBounds(grp.memberParts);
        grp.location = b.center;
        grp.findExternalLinksConnected().each(l => l.invalidateRoute());
      },

I’m not sure why that last line seems to be needed. Maybe that’s a bug.

That is perfect! Thanks!

FYI, here’s how I would simplify the code, mostly removing unneeded property settings. And how I would format it.

<!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:800px"></div>
  <script src="https://unpkg.com/gojs"></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 }),
      // align all of the nodes to the grid, in case their saved locations are off
      "InitialLayoutCompleted": e => {
        e.diagram.moveParts(e.diagram.nodes, new go.Point(0, 0));
      },
      "commandHandler.archetypeGroupData": { isGroup: true },
      // a newly created group, being collapsed and there being no layout, needs a real location
      "SelectionGrouped": e => {
        const b = e.diagram.computePartsBounds(e.subject.memberParts);
        e.subject.location = b.center;
        e.subject.findExternalLinksConnected().each(l => l.invalidateRoute());
      },
      "undoManager.isEnabled": true,
      "draggingTool.isGridSnapEnabled": true,
    });

diagram.grid =
  $(go.Panel, "Grid",
    { 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",
        fromMaxLinks: 1,
        toSpot: go.Spot.Right,
        margin: new go.Margin(5, -1, 5, 0),
        toLinkable: true,
        toMaxLinks: 1,
        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),
        toMaxLinks: 1,
        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,
      doubleClick: (e, grp) => {
        if (grp.isSubGraphExpanded) {
          e.diagram.commandHandler.collapseSubGraph();
        } else {
          e.diagram.commandHandler.expandSubGraph();
        }
      },
      defaultStretch: go.GraphObject.Horizontal
    },
    new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify),
    $(go.Panel, "Auto",
      { row: 0 },
      $(go.Shape, "RoundedTopRectangle",
        {
          strokeWidth: 2,
          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",
      {
        row: 1,
        portId: "",
        toSpot: go.Spot.Left,
        fromSpot: go.Spot.Right
      },
      $(go.Shape, "Square",
        {
          strokeWidth: 2,
          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",
          { 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, relinkableTo: true },
    $(go.Shape, { strokeWidth: 3 })
  );

diagram.model = new go.GraphLinksModel(
  {
    linkFromPortIdProperty: "fromPort",
    linkToPortIdProperty: "toPort",
    nodeDataArray: [
      { 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: [] }
    ],
    linkDataArray: [
      { 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" },
    ]
  });
  </script>
</body>

</html>

Note that I added an “InitialLayoutCompleted” DiagramEvent listener to align all of the nodes to the grid.

Thanks! That’s really useful, it’s hard to tell which settings are doing something and which can be removed :)

Well, you could check the documentation. Or better yet, don’t set it unless you need it.

:) True :)

The issue is that I haven’t memorized the docs, so 6 months later when I change something it’s hard to tell if I’ve made something obsolete :)

I take your point though :)