Link invalidates upon binding

I recently upgraded our GoJS version from 1.7.15 to the latest and I have encountered a weird bug.

I have this binding for my Link’s points:

new go.Binding('points', 'linkPoints',
    (pointsArray) => {
      const points = pointsArray.map(go.Point.parse);
      const pointsList = new go.List(go.Point);
      return pointsList.addAll(points);
    },
  ).makeTwoWay(points => points.toArray().map(go.Point.stringify))

The issue is that the diagram invalidates the links’s positions but it shouldn’t be. I’ve tracked that this started since version 1.7.28.

In version 1.7.28 it appears that we fixed a bug (or improved a misfeature) where Links that are members of a Group were automatically having their routes invalidated when the Group was moved.

That change would presumably decrease the frequency at which Link.points would be set. So I cannot explain the behavior that you see happen in 1.7.28.

A comment unrelated to your problem: doing the back-conversion every time a link route changes is fairly expensive in time and space. You might find it worth it. But the traditional new go.Binding("points").makeTwoWay() avoids doing any conversions, at the expense of leaving a List of Points in the model data. That is why Model.toJson and Model.fromJson handle that property specially.

In very performance-critical apps I avoid converting using stringify and leave the data property values as instances of Point or Size. But that is rare.

In my case, my nodes are not in any member of a group. So it doesn’t make sense why the data is not binding upon initialization.

I have a callback that would move all nodes with after LayoutCompleted in order to align them to the grid, I tested disabling it and the issue still exists.

About your side note, Thanks for suggesting these, I saw these method in a later part of the project. I do agree that there is a slow down upon loading huge diagrams. I’ll be working on a solution to migrate the current data model later on.

What do you mean by “data is not binding upon initialization”? If you load a model that has link routes, those routes should be kept. But if you modify node size or position or some link properties, the route will be invalidated and will eventually be recomputed.

I mean, this happens upon initialization. I did nothing but load the nodeDataArray to the model but the Link binding isn’t reading the data. It’s as if the conversion function isn’t there. Apologies, I haven’t explained the problem properly.

OK, so when you say “initialization”, I assume you mean when you set Diagram.model to a GraphLinksModel that has both a Model.nodeDataArray and a GraphLinksModel.linkDataArray.

Or are you using a TreeModel?

Do all your nodes have location information and a Node.location Binding? Or are you depending on your Diagram.layout to position the nodes?

In the latter case, the layout will cause the nodes to move, thereby invalidating the connected link routes. So the links get the routes that were in the model data, via the Link.points Binding, but then those routes immediately get discarded because the nodes were moved by the layout.

Normally if all the nodes always have valid locations, one doesn’t need to assign Diagram.layout. This is typically the case for apps where the user builds up the diagram, perhaps by dragging nodes from a Palette.

If you really do want a Layout to automatically arrange nodes, typically when nodes or links are added or removed or when nodes change size, but you don’t want a layout to happen upon initialization, you can set Layout.isInitial to false.

I’m using a TreeModel and I’m loading a saved layout. The Node.location bindings are loaded properly because my Layout.isInitial is set to false. It’s just that my link’s position isn’t read so they’re being invalidated.

My flow is first I initialize my Diagram with a model with it’s model data. Then I set nodeDataArray by diagram.model.nodeDataArray = nodeDataArray.

Obviously my flow seems to be wrong. I should be saving and loading with Model.toJson and Model.fromJson. But what’s weird is that it’s working properly once I revert back to 1.7.27. I feel like there’s something wrong with the fix for the bug in 1.7.28 with routes inside Groups… I haven’t implemented a single group in my diagram.

I remember that that change in 1.7.28 really was a fix to the implementation of Group.move. You had tested 1.7.27 and the behavior hadn’t changed in that version?

OK, so you have a TreeModel. I think you should create a new TreeModel and assign its properties, including Model.nodeDataArray, before assigning Diagram.model.

If after that there’s still a problem, we’ll investigate it.

I tried loading the model after setting all diagram and layout settings and the issue is still there.

The parse/converter function is properly returning a List but the links are still invalidated.

Here’s what I just tried:

<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2018 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
  function init() {
    var $ = go.GraphObject.make;

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
          {
            initialContentAlignment: go.Spot.Center,  // for v1.*
            layout: $(go.TreeLayout, { isInitial: false }),
            "ModelChanged": function(e) {
              if (e.isTransactionFinished) document.getElementById("mySavedModel").textContent = e.model.toJson();
            },
            "undoManager.isEnabled": true
          });

    myDiagram.nodeTemplate =
      $(go.Node, "Auto",
        { locationSpot: go.Spot.Center },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(go.Shape,
          { fill: "white", portId: "", fromLinkable: true, toLinkable: true, cursor: "pointer" },
          new go.Binding("fill", "color")),
        $(go.TextBlock, { margin: 8 },
          new go.Binding("text"))
      );

    myDiagram.linkTemplate =
      $(go.Link,
        { reshapable: true, resegmentable: true },
        new go.Binding("points").makeTwoWay(),
        $(go.Shape),
        $(go.Shape, { toArrow: "OpenTriangle" })
      );

    load();
  }

  function load() {
    var str = document.getElementById("mySavedModel").value;
    myDiagram.model = go.Model.fromJson(str);
  }
</script>
</head>
<body onload="init()">
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <textarea id="mySavedModel" style="width: 100%; height: 300px">
{ "class": "go.TreeModel",
  "nodeDataArray": [ 
{"key":1, "text":"Alpha", "color":"pink", "loc":"25 41.3"},
{"key":2, "text":"Beta", "color":"orange", "loc":"122 15.65", "parent":1, "points":[50,41.3,60,41.3,90,15.65,100,15.65]},
{"key":3, "text":"Gamma", "color":"lightgreen", "loc":"131.5 66.94999999999999", "parent":1, "points":[50,41.3,60,41.3,90,66.95,100,66.95]},
{"key":4, "text":"Delta", "color":"pink", "loc":"227.49999999999986 125.94999999999996", "parent":3, "points":[163,66.94999999999999,173,66.94999999999999,231.3333282470703,23.333328247070312,134.3333282470703,164.3333282470703,194,125.94999999999999,204,125.94999999999999]}
 ]}
  </textarea>
</body>
</html>

This loads as:

Note the { isInitial: false } on the TreeLayout.