Link.position has NaN values despite valid points presence

I have an issue with links position not being real. It prevents the GridLayout from repositioning them after expanding groups inside. What could I do wrong that could cause link’s position to not be set? My link template has only a points binding (new go.Binding(“points”, parse, stringify)) where parse and stringify are custom functions. It works fine (position is real) until I reload the entire app so maybe I’m missing something during initialization? I’m looking for suggestions that could help me figure out why the position field has NaN values.

I can’t explain that, because in a sample that has the situation you describe, Draggable Link, Allow Users to Drag Links and Reconnect Them on Drop | GoJS Diagramming Library , the Link in the Palette has a real Link.position, and the link template only has a Binding on the Link.points property, and the Palette.layout is an instance of GridLayout. And you say that everything works until reloading.

Is that unreal-position Link initially hidden because it is inside a subgraph that is in a collapsed Group? If it hasn’t ever been shown or laid out, I could imagine that various aspects of the Links (or any such Part) would not yet have been computed, because there’s no point in doing so until a transaction has occurred to show it. Such as the Group becoming expanded and the layout that normally happens on its subgraph. So maybe what you are seeing is an intentional optimization that we could be doing.

I found the first step to reproduce it (I tried to replicate the way it is done in the project). Look at the SubGraphExpanderButton.click implementation. The click callback was added to enable the button in the read only mode. There’s a loop that calls updateTargetBindings on each member part. It looks like it prevents the correct move of the link.To replicate it expand each lane one by one starting from the left side. If you expand the right side lane first, it will work fine though - the link will be moved by the layout.

I tried to remove the updateTargetBindings call but then it leads to other issues for example invisible adornments, invisible parts, invisible links etc.

Expected behavior:

Actual behavior:

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/release/go-debug.js"></script>
    <div id="sample" style="display: flex">
      <div
        id="diagramDiv"
        style="border: solid 1px black; width: 600px; height: 600px"
      ></div>
    </div>

    <script id="code">
      const diagram = new go.Diagram("diagramDiv", {
        "animationManager.isEnabled": false,
      });

      const nodeTemplate = new go.Node("Auto")
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .add(
          new go.Shape("RoundedRectangle", {
            strokeWidth: 0,
            fill: "white",
          }).bind("fill", "color"),
          new go.TextBlock({
            margin: 8,
            font: "bold 14px sans-serif",
            stroke: "#333",
          }).bind("text")
        );

      const poolTemplate = new go.Group("Auto", {
        layout: new go.GridLayout({
          alignment: go.GridAlignment.Position,
          comparer: (a, b) => a.position.x - b.position.x,
          spacing: new go.Size(0, 0),
          cellSize: new go.Size(0, 0),
          wrappingColumn: Infinity,
          wrappingWidth: Infinity,
          isRealtime: true,
        }),
      })
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .add(
          new go.Shape("RoundedRectangle", {
            strokeWidth: 1,
            fill: "white",
            width: 400,
            height: 400,
          })
        );

      const laneTemplate = new go.Group("Vertical", {
        isSubGraphExpanded: true,
      })
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .bindTwoWay("isSubGraphExpanded")
        .add(
          go.GraphObject.make("SubGraphExpanderButton", {
            click: (event, thisObj) => {
              thisObj.part.diagram.startTransaction("Toggle");
              if (thisObj.part.isSubGraphExpanded) {
                thisObj.part.collapseSubGraph();
              } else {
                thisObj.part.expandSubGraph();
                for (const part of thisObj.part.memberParts) {
                  part.updateTargetBindings();
                }
              }
              thisObj.part.diagram.commitTransaction("Toggle");
            },
          }),
          new go.Shape("RoundedRectangle", {
            strokeWidth: 1,
            fill: "white",
            width: 150,
            height: 350,
          }).bindObject(
            "desiredSize",
            "isSubGraphExpanded",
            (isSubGraphExpanded) =>
              isSubGraphExpanded ? new go.Size(150, 350) : new go.Size(30, 350)
          )
        );

      const linkTemplate = new go.Link({
        reshapable: true,
        resegmentable: true,
        routing: go.Routing.Orthogonal,
      })
        .bindTwoWay(
          "points",
          "points",
          (points) => points.map(go.Point.parse),
          (points) => {
            const result = points.map(go.Point.stringify).toArray();
            console.trace(result);
            return result;
          }
        )
        .add(new go.Shape({ strokeWidth: 1.5, stroke: "red" }))
        .add(
          new go.Shape({
            strokeWidth: 0,
            fill: "red",
            scale: 0.7,
            fromArrow: "circle",
          })
        )
        .add(
          new go.Shape({
            strokeWidth: 0,
            fill: "red",
            scale: 0.7,
            toArrow: "circle",
          })
        );

      diagram.nodeTemplate = nodeTemplate;
      diagram.groupTemplateMap.add("Pool", poolTemplate);
      diagram.groupTemplateMap.add("Lane", laneTemplate);
      diagram.linkTemplate = linkTemplate;

      const nodes = [
        {
          key: "P",
          isGroup: true,
          category: "Pool",
          location: "31.24609375 -0.3515625",
        },
        {
          key: "L1",
          group: "P",
          isGroup: true,
          category: "Lane",
          location: "31.24609375 -0.3515625",
          isSubGraphExpanded: false,
        },
        {
          key: "L2",
          group: "P",
          isGroup: true,
          category: "Lane",
          location: "62.24609375 -0.3515625",
          isSubGraphExpanded: false,
        },
        {
          key: "N1",
          group: "L1",
          text: "Alpha",
          color: "lightblue",
          location: "74.30859375 157.82421875",
        },
        {
          key: "N2",
          group: "L2",
          text: "Beta",
          color: "orange",
          location: "150.8359375 88.375",
        },
        {
          key: "N3",
          group: "L2",
          text: "Gamma",
          color: "lightgreen",
          location: "72.37538656080795 203.03515625",
        },
      ];

      const links = [
        {
          from: "N2",
          to: "N3",
          points: [
            "176.76972453040395 125.05858968580793",
            "176.76972453040395 135.05858968580793",
            "176.76972453040395 164.04687296790397",
            "108.81600952871193 164.04687296790397",
            "108.81600952871193 193.03515625",
            "108.81600952871193 203.03515625",
          ],
        },
      ];

      diagram.model = new go.GraphLinksModel([], []);

      setTimeout(() => {
        diagram.model.addNodeDataCollection(nodes);
        diagram.model.addLinkDataCollection(links);
      }, 1000);
    </script>
  </body>
</html>

Yes, I don’t understand why you are doing:

                for (const part of thisObj.part.memberParts) {
                  part.updateTargetBindings();
                }

You are explicitly updating all bound properties before the group has been fully expanded, which causes them to get values at the wrong time.

What problem are you really trying to solve with that code? This sample does not demonstrate any problem if one removes that code.

For example adornments may not be visible after group expansion if there’s no updateTargetBindings call:

<!DOCTYPE html>
<html lang="en">
  <body>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/release/go-debug.js"></script>
    <div id="sample" style="display: flex">
      <div
        id="diagramDiv"
        style="border: solid 1px black; width: 600px; height: 600px"
      ></div>
    </div>

    <script id="code">
      const diagram = new go.Diagram("diagramDiv", {
        "animationManager.isEnabled": false,
      });

      const nodeTemplate = new go.Node("Auto")
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .bind("", "", (_, part) => {
          if (part.findAdornment("Adornment")) {
            return;
          }
          const adornment = makeAdornment();
          adornment.adornedObject = part;
          part.addAdornment("Adornment", adornment);
        })
        .add(
          new go.Shape("RoundedRectangle", {
            strokeWidth: 0,
            fill: "white",
          }).bind("fill", "color"),
          new go.TextBlock({
            margin: 8,
            font: "bold 14px sans-serif",
            stroke: "#333",
          }).bind("text")
        );

      const makeAdornment = () =>
        new go.Adornment("Spot")
          .bind("visible", "", (_, targetObj) => {
            const ad = targetObj.part;
            if (!(ad instanceof go.Adornment)) return true;
            const part = ad.adornedObject;
            if (!(part instanceof go.Part)) return true;
            return part.isVisible();
          })
          .add(
            new go.Panel("Vertical").add(
              new go.Placeholder(),
              new go.TextBlock({ text: "Adornment" })
            )
          );

      const poolTemplate = new go.Group("Auto", {
        layout: new go.GridLayout({
          alignment: go.GridAlignment.Position,
          comparer: (a, b) => a.position.x - b.position.x,
          spacing: new go.Size(0, 0),
          cellSize: new go.Size(0, 0),
          wrappingColumn: Infinity,
          wrappingWidth: Infinity,
          isRealtime: true,
        }),
      })
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .add(
          new go.Shape("RoundedRectangle", {
            strokeWidth: 1,
            fill: "white",
            width: 400,
            height: 400,
          })
        );

      const laneTemplate = new go.Group("Vertical", {
        isSubGraphExpanded: true,
      })
        .bindTwoWay("location", "location", go.Point.parse, go.Point.stringify)
        .bindTwoWay("isSubGraphExpanded")
        .add(
          go.GraphObject.make("SubGraphExpanderButton", {
            click: (event, thisObj) => {
              thisObj.part.diagram.startTransaction("Toggle");
              if (thisObj.part.isSubGraphExpanded) {
                thisObj.part.collapseSubGraph();
              } else {
                thisObj.part.expandSubGraph();
                // for (const part of thisObj.part.memberParts) {
                //   part.updateTargetBindings();
                // }
              }
              thisObj.part.diagram.commitTransaction("Toggle");
            },
          }),
          new go.Shape("RoundedRectangle", {
            strokeWidth: 1,
            fill: "white",
            width: 150,
            height: 350,
          }).bindObject(
            "desiredSize",
            "isSubGraphExpanded",
            (isSubGraphExpanded) =>
              isSubGraphExpanded ? new go.Size(150, 350) : new go.Size(30, 350)
          )
        );

      const linkTemplate = new go.Link({
        reshapable: true,
        resegmentable: true,
        routing: go.Routing.Orthogonal,
      })
        .bindTwoWay(
          "points",
          "points",
          (points) => points.map(go.Point.parse),
          (points) => {
            const result = points.map(go.Point.stringify).toArray();
            console.trace(result);
            return result;
          }
        )
        .add(new go.Shape({ strokeWidth: 1.5, stroke: "red" }))
        .add(
          new go.Shape({
            strokeWidth: 0,
            fill: "red",
            scale: 0.7,
            fromArrow: "circle",
          })
        )
        .add(
          new go.Shape({
            strokeWidth: 0,
            fill: "red",
            scale: 0.7,
            toArrow: "circle",
          })
        );

      diagram.nodeTemplate = nodeTemplate;
      diagram.groupTemplateMap.add("Pool", poolTemplate);
      diagram.groupTemplateMap.add("Lane", laneTemplate);
      diagram.linkTemplate = linkTemplate;

      const nodes = [
        {
          key: "P",
          isGroup: true,
          category: "Pool",
          location: "31.24609375 -0.3515625",
        },
        {
          key: "L1",
          group: "P",
          isGroup: true,
          category: "Lane",
          location: "31.24609375 -0.3515625",
          isSubGraphExpanded: false,
        },
        {
          key: "L2",
          group: "P",
          isGroup: true,
          category: "Lane",
          location: "62.24609375 -0.3515625",
          isSubGraphExpanded: false,
        },
        {
          key: "N1",
          group: "L1",
          text: "Alpha",
          color: "lightblue",
          location: "74.30859375 157.82421875",
        },
        {
          key: "N2",
          group: "L2",
          text: "Beta",
          color: "orange",
          location: "150.8359375 88.375",
        },
        {
          key: "N3",
          group: "L2",
          text: "Gamma",
          color: "lightgreen",
          location: "72.37538656080795 203.03515625",
        },
      ];

      const links = [
        {
          from: "N2",
          to: "N3",
          points: [
            "176.76972453040395 125.05858968580793",
            "176.76972453040395 135.05858968580793",
            "176.76972453040395 164.04687296790397",
            "108.81600952871193 164.04687296790397",
            "108.81600952871193 193.03515625",
            "108.81600952871193 203.03515625",
          ],
        },
      ];

      diagram.model = new go.GraphLinksModel([], []);

      setTimeout(() => {
        diagram.model.addNodeDataCollection(nodes);
        diagram.model.addLinkDataCollection(links);
      });
    </script>
  </body>
</html>


Your more complete code has a problem with a Binding that you are using to add an Adornment to the Node. Bindings must not have any side effects (other than setting the target property, of course).

Do you really need a permanent Adornment on each Node? If so, why not include its contents in the Node template itself?

Including adornment contents affects the actualBounds of a node. Using adornment doesn’t. I want to avoid node size shifts when adornment content changes.

In order to avoid invalidating the layout when a node changes size, try setting Part.layoutConditions to go.LayoutConditions.Standard & ~go.LayoutConditions.NodeSized.

In order for a layout to pretend a node is a different size than its Part.actualBounds, override Layout.getLayoutBounds.

In order for AvoidsNodes routing to pretend a node is a different size than it actually is, set Node.avoidableMargin or override Node.getAvoidableRect.