Lazy makeTwoWay binding

Is there a way to make the two way binding only update when a transaction has finished?

I am testing with a vue component that holds the state. If I pass this state directly to the nodeArray, the web application is not able to keep up when a node is moved:


<!DOCTYPE html>
<html>

<head>
  <title>Performance two way binding</title>
  <meta charset="UTF-8">
</head>

<body onload="init()">
  <div id="app">
    <span id="myNumberOfNodes"></span>
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
    <pre v-text="diagramData.nodeDataArray" style="height: 300px; overflow: scroll"></pre>
  </div>

  <script src="go.js"></script>
  <script src="https://unpkg.com/vue"></script>
  <script id="code">
    function init() {
      myApp = new Vue({
        el: '#app',
        data: {
          diagramData: {
            nodeDataArray: [],
            linkDataArray: []
          }
        },
        mounted: function () {
          var $ = go.GraphObject.make;  // for conciseness in defining templates

          myDiagram =
            $(go.Diagram, "myDiagramDiv",  // create a Diagram for the DIV HTML element
              { // enable undo & redo
                "undoManager.isEnabled": true
              });

          // define a simple Node template
          myDiagram.nodeTemplate =
            $(go.Node, "Auto",  // the Shape will go around the TextBlock
              $(go.Shape, "RoundedRectangle", { strokeWidth: 2, fill: "white" }),
              $(go.TextBlock, { margin: 8 }, new go.Binding("text", "key")),
              new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify)
            );

          // but use the default Link template, by not setting Diagram.linkTemplate

          // create the model data that will be represented by Nodes and Links
          let numberOfNodes = 50000
          let width = parseInt(Math.sqrt(numberOfNodes))
          let spacing = 100
          for (let i = 0; i < numberOfNodes; ++i) {
            this.diagramData.nodeDataArray.push({ key: `Alpha${i}`, "location": `${(i % width) * spacing} ${parseInt(i / width) * spacing}` })
            this.diagramData.linkDataArray.push({ from: `Alpha${i}`, to: `Alpha${i + 1}` })
          }

          myDiagram.model = new go.GraphLinksModel(this.diagramData.nodeDataArray, this.diagramData.linkDataArray);
        }
      })
    }
  </script>
</body>

</html>

However, when I subscribe to the changed event and only update the vue state when a transaction is finished, it seems to work fine:


<!DOCTYPE html>
<html>

<head>
  <title>Performance on transaction finished update</title>
  <meta charset="UTF-8">
</head>

<body onload="init()">
  <div id="app">
    <span id="myNumberOfNodes"></span>
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
    <pre v-text="savedDiagramData.nodeDataArray" style="height: 300px; overflow: scroll"></pre>
  </div>

  <script src="go.js"></script>
  <script src="https://unpkg.com/vue"></script>
  <script id="code">
    function init() {
      myApp = new Vue({
        el: '#app',
        data: {
          savedDiagramData: {
            nodeDataArray: [],
            linkDataArray: []
          }
        },
        mounted: function () {
          var $ = go.GraphObject.make;  // for conciseness in defining templates
          var app = this

          myDiagram =
            $(go.Diagram, "myDiagramDiv",  // create a Diagram for the DIV HTML element
              { // enab le undo & redo
                "undoManager.isEnabled": true,
                "ModelChanged": function (e) {
                  if (e.isTransactionFinished) {  // show the model data in the page's TextArea
                    // console.log('hi')
                    // app.savedDiagramData = JSON.parse(e.model.toJson())
                    increment = JSON.parse(myDiagram.model.toIncrementalJson(e))
                    if (increment.modifiedNodeData) {
                      increment.modifiedNodeData.forEach((d) => {
                        Object.assign(app.savedDiagramData.nodeDataArray[d.key], d)
                      })
                    }
                  }
                }
              });

          // define a simple Node template
          myDiagram.nodeTemplate =
            $(go.Node, "Auto",  // the Shape will go around the TextBlock
              $(go.Shape, "RoundedRectangle", { strokeWidth: 2, fill: "white" }),
              $(go.TextBlock, { margin: 8, editable: true }, new go.Binding("text", "key")),
              new go.Binding("location", "location", go.Point.parse).makeTwoWay(go.Point.stringify)
            );

          // but use the default Link template, by not setting Diagram.linkTemplate

          // create the model data that will be represented by Nodes and Links
          let numberOfNodes = 50000
          let width = parseInt(Math.sqrt(numberOfNodes))
          let spacing = 100
          for (let i = 0; i < numberOfNodes; ++i) {
            this.savedDiagramData.nodeDataArray.push({ key: i, "location": `${(i % width) * spacing} ${parseInt(i / width) * spacing}` })
            this.savedDiagramData.linkDataArray.push({ from: i, to: i+1 })
          }

          let initialSavedDiagramData = JSON.parse(JSON.stringify(this.savedDiagramData))
          myDiagram.model = new go.GraphLinksModel(initialSavedDiagramData.nodeDataArray, initialSavedDiagramData.linkDataArray);
        }
      })
    }
  </script>
</body>

</html>

However, on the second option, I am not able to easily propagate the source state to the target state using updateTargetBindings since the target and the source are not referring to the same data.

Is it possible to make the target to source update (twoWayBinding) somewhat lazy so that it only updates the state when a transaction has finished?

Thanks!

No, there’s no way to control that. We’ve thought about that in the past, but there hasn’t been much demand for controlling how often bindings get evaluated in either direction.

So where and how is the performance affected? There is some overhead, of course, just in evaluating the binding and in the Point.stringify function you use as the back-conversion function. You could experiment with not using Point.stringify or Point.parse, to see if that helps.

As I recall, Vue does dirty checking by intercepting settings of properties – defining property setters that detect and inform Vue about the change. Presumably the re-rendering of components that are watching that node data is what is being very slow for you. I don’t know enough Vue to say how you could control that on Vue’s side. If you can do it, I think that would be the most efficient way of handling the problem.

Your idea of only updating after a transaction is a reasonable one. However, you are doing so inefficiently by calling toIncrementalJson and then JSON.parse. A more efficient approach would be to call toIncrementalData, although even it makes deep copies of data. The most efficient would be to iterate over all of the ChangedEvents in the Transaction and pick out the changes that you want to record in the shared data.

But I wonder if it wouldn’t be more natural to just assume there’s no shared memory between the GoJS model state and your Vue components. That’s actually what toIncrementalData is for. That way you can use TwoWay Bindings. You can read more about this at https://gojs.net/latest/intro/usingModels.html, especially in the section about immutable data. It’s also what is used in our React component, https://gojs.net/latest/intro/react.html. However I do not know how you want to organize your app.

Thanks for the clarification. I will look at this tomorrow!

I followed the same mechanism as the gojs react-component using:

  • toIncrementalData (for obtaining gojs updates)
  • vue watch with mergeNodeDataArray (for obtaining vuejs updates)

I am now able to keep the models nicely in sync while maintaining performance. Thanks a lot for your support!