Diagram Event nodeDataArray behavior

Hi!
I’m working with GoJS and diagrams that can have 5k+ nodes.
While attempting to optimize everything I noticed that the “nodeDataArray” event is fired for every node.

This surprised me, I had assumed that supplying an array of nodes to the diagram model would allow the diagram to bypass this event. (for each node at least)

Here is a sample of the code I added to the diagram initialization code.

let count = 1;
let count2 = 1;

function configureBaseDiagram(diagram: go.Diagram): void {
  const firstStart = new Date();
  let addStart = new Date();

  console.log("ConfigureBaseDiagram() started");

  diagram.addDiagramListener("InitialLayoutCompleted", () => {
    console.log(`InitialLayoutCompleted called ${count} times!`);
    count += 1;
    const EndTime = new Date();
    console.log(
      `Diagram loaded after ${
        Math.abs(firstStart.getTime() - EndTime.getTime()) / 1000
      } seconds`
    );
  });

  diagram.addDiagramListener("LayoutCompleted", () => {
    console.log(`Additional LayoutCompleted called ${count2} times!`);
    count2 += 1;
    const EndTime = new Date();
    console.log(
      `Diagram re-loaded after ${
        Math.abs(addStart.getTime() - EndTime.getTime()) / 1000
      } seconds`
    );
  });

  diagram.addDiagramListener("Modified", () => {
    addStart = new Date();
    console.log(`Modified called`);
  });

  diagram.addModelChangedListener((evt) => {
    if (evt.propertyName === "nodeDataArray") {
      console.log("nodeDataArray changed");
    }
    // ignore unimportant Transaction events
    if (!evt.isTransactionFinished) return;
    var txn = evt.object; // a Transaction
    if (txn === null) return;
    console.log("Nodes: " + diagram?.model?.nodeDataArray?.length);
  });

  diagram.model = configureDiagramModel();

  diagram.div = null;
  // Relinking depends on modelData
  diagram.linkTemplate = RelationshipLink();

  // Hover delay
  diagram.toolManager.hoverDelay = 200;
  // Padding in the diagram layout
  diagram.padding = new go.Margin(120, 20, 20, 20);

  diagram.click = (e) => {
    e.diagram.commit((d) => {
      d.clearHighlighteds();
    });
  };

  attachZoomHandlers(diagram);
}

And here is a screenshot of the browser console: (notice the 5000 count next to “nodeDataArray changed”
diagramConfig

So I have two questions really:
Is this normal?
Can I do something to improve this?

First, I suggest initializing everything about the Diagram other than the Diagram.model property, and then setting Diagram.model last. That way when you want to show a different diagram (i.e. load a different model), all you have to do is replace the value of Diagram.model.

For the issue that you report, I would examine the ChangedEvents that have the ChangedEvent.propertyName equal to “nodeDataArray”. Look at the ChangedEvent.change property. Look at the stack at the time to see why the event is being raised.

Thanks for the suggestions Walter, I will look into the second part of your reply right away.
Edit: It seems that the ChangedEvent.change property is always EnumValue.Insert

As for the first part (setting Diagram.model last) we used the gojs-react-basic example as the starting point for the integration of GoJS, is there a way to init the with everything except the model, then update it later.

With gojs-react, you’ll need to initialize the model in your initDiagram function, but you can provide the data later. If you want to provide data later and treat it as initial data, you’ll want to call clear first.

That is interesting, how do we know when it is time to call clear() and replace the model?
The description of initDiagram specifies not setting the model data there, so could we for instance:

  1. Render a ReactDiagram component with all the setup minus the data (so EnumValue.Insert is not called thousands of times)
  2. Know when the component is ready to accept model data (InitialLayoutCompleted maybe?)
  3. Call clear() then replace the model
  4. Know when the diagram is finished updated

Sorry, I misunderstood before. When using gojs-react, there is no way to avoid all those inserts, as gojs-react works by merging data into the existing data arrays via mergeNodeDataArray and mergeLinkDataArray. This is the most efficient way of updating the model with new state from a React app under normal circumstances.

However, if you’re worried about performance and change models frequently during interaction, it may be better not to use gojs-react. gojs-react requires immutable state updates and doesn’t replace models, but updates them in place, which is faster for typical changes like adding a few nodes or updating some data properties.

Ok, thanks for the info, this seemed to be where my tests were leading me already.
I have attached console.logs to most of the Diagram events and model events that seemed relevant in an effort to see if we were implementing the ReactDiagram in a flawed way.

I don’t think so though because when creating a fresh project using the gojs-react-basic sample as a starter and assigning the same logging to the same events, and supplying the app.tsx with the same data (5k nodes) we see the same time to init and update.

Ok, I will explore not using ReactDiagram and using the gojs library directly.
In the meantime, though we have a rather large implementation already.

How could we achieve something like a loading state for the component?

Meaning after updating the nodeDataArray or linkDataArray the loading is true, then after the diagram has completely finished its update loading is false.

I didn’t see a diagram event corresponding to a clear moment when the diagram is done processing.
Modified, LayoutCompleted and even InitialLayoutCompleted caught my eye, but they seem to be called multiple times, so are not reliable for a clear “done” signal.

Thanks again for the support, it is much appreciated.

The assignment of Diagram.model to a different model will result in an “InitialLayoutCompleted” DiagramEvent that you could listen to in order to find out when the model is completely “loaded”. That precedes any loading animations.

When you assign Diagram.model again, you’ll get another “InitialLayoutCompleted” event.

Yes, that seems to work well.
Side question, when updating the model (merge not replace) I am seeing an error event for each node being triggered.

Example merge:

 diagram.model.commit((m) => {
      m.mergeNodeDataArray(nodeDataArray);
      m.mergeLinkDataArray(linkDataArray);
    }, "update");

Example logging code:

diagram.addModelChangedListener((evt) => {
    if (evt.propertyName === "errors") {
      console.log(`error change ${evt.change}`);
    }
  });

What I find interesting is that the error event fires for every node, not just what changed.
I assume it was due to the hidden property node.__gohashid not being the same, but when I tested mapping the property to make sure they matched, the errors still fired.

So your data has a property named “errors”, and each node data object has a new value for that property? If so, that makes sense.

Ah, nice catch, I didn’t see that, I assumed “errors” was a GoJS property.
I just checked and in all cases, the node.errors properties were empty arrays, so I guess GoJS checks if a property has changed by reference and not by value.
I updated the mapping function that syncs changes to assign the .errors property using the old reference, (if the value of node.errors hasn’t changed) and all the errors disappeared and the time for merging changes went down drastically.