initialAutoScale with redux and react diagram

I’m rewriting some old code using the react diagram and redux, and I’m seeing weird behavior with initialAutoScale Uniform. I have my nodes/links being parsed and then stored in redux. The modelData is being set when I init the diagram, as it’s properties never have to change in my use case.

        <ReactDiagram
          initDiagram={initDiagram}
          divClassName="diagram-component"
          nodeDataArray={nodes}
          linkDataArray={links}
          onModelChange={() => {
            console.log("model changed");
            zoomToFit();
          }}
          skipsDiagramUpdate={false}
        />

  function initDiagram(): go.Diagram {
    return GO(go.Diagram, {
      ...,
      initialAutoScale: go.Diagram.Uniform,
      ...,
      model: GO(go.GraphLinksModel, {
        nodeKeyProperty: 'id',
        nodeGroupKeyProperty: 'parent',
        linkKeyProperty: 'id',
        linkFromPortIdProperty: 'fromPort',
        linkToPortIdProperty: 'toPort',
      }),
    });

The issue is that I’m thinking the way redux works breaks the initialAutoScale property. Because the model is set when the diagram first renders, and then it’s not until my rest apis come back and my parse finishes that the node/link data arrays are being constructed and sent. However, by then the model is already set and won’t change, so the scale never fires.

If I have autoScale set to Uniform, everything is sized as I expect. If I weren’t using the react diagram I could do something like diagram.model = new go.GraphLinksModel(nodes, links), but because the react diagram abstracts away that stuff behind it’s properties I can’t get to it there.

Is there a nice way to fix this? I’m thinking I might just have to redo my stuff to do something more like what your wrapper does, but it would make my other code more complex. I currently have some stuff that updates various states on the nodes, and those are all done using data moving from redux -> gojs, rather than gojs getting updated first, and then it updating redux, which I think is what I’d need to do if I used the wrapper.

So the diagram is basically read-only? You don’t need to handle any changes to any data from either the diagram side or the rest of the app?

If so, you could simplify by assigning the Diagram.model at a time when you have both the node data Array and the link data Array available. Don’t use ReactDiagram at all – just render the Div with the new Diagram and its new GraphLinksModel. Since the props never change, you can avoid ever re-rendering.

Sorry no, the diagram is getting updates. I have everything set up so that the code that queries the api parses the stuff apart and then updates them both at the same time, rather than the data flowing through the react diagram, and then the diagram’s model onUpdate keeping the redux in sync. Basically there’s some metadata that comes back, and a few other calls that mutate that data (updates to a status property, etc). I parse the metadata apart, then generate nodes and links from that, then store the nodes, links and the original metadata in redux.

I suspect what I need to do then is not set the model when I create the diagram and it’s templates, but rather when I generate the nodes and links, and set them all together then,

Or maybe I shouldn’t set nodes/links at all, and just set the model, and only store it in redux, then where ever I need to mutate the nodes or links, I just get them off the model from redux? I’m not sure if one way would be better than the other.

To clarify for me – the diagram is basically read-only but does need to display updates in the props. The user might be able to scroll and zoom and click, but won’t make any changes that need to be reflected back in the props data.

OK, so if that is true, then you don’t need to worry about the diagram modifying any data, so whenever there are any changes in the props data, you just need to call Model.mergeNodeDataArray and GraphLinksModel.mergeLinkDataArray within a transaction.

okay, so something like this in my view with my diagram, where “nodes” is my useSelector() for my nodeDataArray property?

  useEffect(() => {
    if (!diagramref.current) return;
    const myDiagram = diagramref.current.getDiagram();
    if(myDiagram instanceof go.Diagram) {
      myDiagram.startTransaction("updating ndoes");
      myDiagram.model.mergeNodeDataArray(nodes);
      myDiagram.commitTransaction("updating nodes");
    }
  }, [nodes]);

If you are using a TreeModel and therefore don’t have a linkDataArray, yes, that’s right.

Hmm, I’m still seeing the same behavior. When I load into my diagram, I see my nodes being set, but the initialAutoScale isn’t being observed. The diagram loads with everything way way zoomed out. if I set autoScale to uniform, it loads in as I’d expect, so maybe something isn’t causing the node or model update listeners to fire?

Do I need to do something like reset the model when the node/link compute finishes in order for it to recompute the bounds or whatever is making it not process? I’m not sure where my issue lies.

I suspect it has something to do with my load order.

I have two methods of entry, with and without a query parameter. If there is no parameter, the load happens when a user clicks a table row. this goes and fires a query to get some metadata, and then it swaps tabs to my diagram and the flow loads. This method works fine, everything is sized correctly.

If you have a query parameter, that immediately triggers the load and tab change. This method does not work.

The initial… properties of the Diagram are used only when the Diagram.model is replaced, or when there is a call to Diagram.delayInitialization.

If you look at the implementation of gojs-react, you’ll see that there is a call to Diagram.delayInitialization in componentDidMount.

But maybe you are making changes to the model data at other times? If so, then those “initial…” properties do not apply.

I’m not using components, so I don’t have a didMount.

The thing I’m not sure how to do is that I don’t have a model being set separate from the init of ReactDiagram. Everything should be getting set together then:

export const DiagramView = () => {
  const metadata = useSelector(selectDiagramMetadata);
  const nodes = parseNodes(metadata);
  const links = parseLinks(metadata);

...

  return (
    <div className="DiagramView" style={{}}>
      <div className="DiagramView-header">
        <Heading className="nova-bold" level={4}>
          {flowName || ''}
        </Heading>
      </div>
      <LogViewer />
      {nodes.length > 1 ? (
        <ReactDiagram
          ref={diagramref}
          initDiagram={initDiagram}
          divClassName="diagram-component"
          nodeDataArray={nodes}
          linkDataArray={links}
          onModelChange={() => {
            console.log('model change');
          }}
          skipsDiagramUpdate={false}
        />

The model is set directly on the diagram during initDiagram:

  function initDiagram(): go.Diagram {
    // @ts-ignore
    return GO(go.Diagram, {
      'undoManager.isEnabled': true,
      'draggingTool.isEnabled': false,
      'toolManager.mouseWheelBehavior': go.ToolManager.WheelZoom,
      'toolManager.hoverDelay': 300,
      'animationManager.isEnabled': false,
      nodeTemplate: getTaskTemplate(),
      groupTemplate: getSubFlowTemplate(),
      linkTemplateMap: getLinkTemplateMap(),
      contextMenu: getDiagramContextMenu(),
      doubleClick: expandCollapseAllToggle,
      initialScale: 1,
      initialAutoScale: go.Diagram.Uniform,
      initialContentAlignment: go.Spot.Center,
      initialDocumentSpot: go.Spot.Center,
      initialViewportSpot: go.Spot.Center,
      contentAlignment: go.Spot.Center,
      layout: getLayout(),
      model: getModel(),
      padding: 100,
      ChangedSelection: updateHighlights,
      DocumentBoundsChanged: zoomToFit,
      ChangingSelection: clearDataObjectSelection,
    });
  }

Could there be a problem with the timing of the model vs nodes/links?

I should note that I’m not setting the nodes/links on the model itself, just the various key properties:

  function getModel() {
    return GO(go.GraphLinksModel, {
      nodeKeyProperty: 'id',
      nodeGroupKeyProperty: 'parent',
      linkKeyProperty: 'id',
      linkFromPortIdProperty: 'fromPort',
      linkToPortIdProperty: 'toPort',
    });
  }

Setting both Diagram.initialScale and Diagram.initialAutoScale seem to conflict with each other. I think the initialAutoScale would take precedence.

Setting Diagram.initialContentAlignment seems superfluous because you have set Diagram.contentAlignment.

Setting Diagram.initialDocumentSpot and initialViewportSpot are superfluous because you have set Diagram.contentAlignment (or Diagram.initialContentAlignment).

Yeah I’m sure they’re leftovers from when I was trying to get resizing and collapse/expand of subGraphs working the way I wanted. I went ahead and cleaned them up as you suggested, though it does not appear to have impacted the initial sizing.

There is ever only one creation of an instance of Diagram, yes?
And you set its templates and layout and model at that time, and not at any other time?
The initDiagram function is called first.
Then it calls Diagram.delayInitialization in order to get the data from the nodeDataArray and linkDataArray props.
That should cause an initial layout and then an initial auto scale based on the newly laid out nodes and links and the size of the viewport.

That declaration of a “DocumentBoundsChanged” DiagramEvent listener, calling zoomToFit will have an effect similar to setting Diagram.autoScale to Uniform. Assuming your function does a “zoom to fit”. So that is also superfluous.

Yes, there is only ever one diagram. And yes, the templates, layout and model never change. The only things that can change is links (I add temporary ones in another layer based on some selections), expand/collapse state of subGraphs, and visual properties of some nodes (icons, text fields, etc). None of these persist between diagrams.

The function isn’t superfluous, because it doesn’t lock your zoom like autoScale does. A user can still scroll, pan and zoom around in the diagram, but then if they do something that triggers a relayout (like expanding or collapsing a subGraph), then I re-zoomToFit. Basically I want initialAutoScale, and then to return to that uniform auto scale every time a layout needs to reoccur.

Ah, OK, that’s a good use for having the diagram listener and not setting Diagram.autoScale.

Does the initial layout always happen correctly? If so, try defining “InitialLayoutCompleted” and “LayoutComplete” DiagramEvent listeners to see when they are called. I’m guessing that “InitialLayoutCompleted” isn’t happening at the right time for you.

As far as I can tell it’s happening at the correct time.

It’s happening after the metadata and nodes have been constructed, and after the diagram dom element has been set (so the diagram should know what nodes and links it has for sizing purposes).

Here are my console.log traces:
App.tsx:81 pfd: url parameter detected, firing query for metadata
App.tsx:83 pfd: api response received, setting metadata and opening diagram
DiagramView.tsx:23 pfd: metadata acquired from redux during init, parsing nodes
DiagramParser.ts:3 pfd: parsing nodes
DiagramParser.ts:121 pfd: parsing links…
DiagramView.tsx:35 pfd: initDiagram called
DiagramView.tsx:85 pfd: gojs diagram model created
DiagramView.tsx:1339 pfd: ZoomToFit called
DiagramView.tsx:57 pfd: Initial Layout Completeted
DiagramView.tsx:60 pfd: Layout Completed
DiagramView.tsx:1339 pfd: ZoomToFit called
DiagramView.tsx:57 pfd: Initial Layout Completeted
DiagramView.tsx:60 pfd: Layout Completed
DiagramView.tsx:1395 pfd: Model Changed

Also for the initial layout question, it depends what you mean. The diagram nodes/layout/links themselves look entirely correct, it just looks like someone scrolled way out when it loads. If you scroll in to zoom it’s fine, and if you perform one of the actions that triggers a zoomtofit, like expanding or collapsing something, the resulting zoomtofit fits everything correctly. It’s just on the initial load in that everything looks small (you can see the super tiny nodes in the center):

Hmm, I’m even more convinced this is a weird sizing/rendering timing issue. I’ve discovered if I have the chrome dev tools window fullscreen over the top of chrome when the rendering should be occurring, the issue no longer occurs. If it is still windowed (so you can see chrome in the background), or if you minimize chrome window itself, the problem still occurs. This appears to be specific to chrome dev tools?

Try it in Firefox and Safari and Edge?

I don’t have access to safari.

It happens very occasionally on edge and firefox. It’s also apparently somewhat inconsistent on Chrome. Sometimes when I use back/forward in the browser, the load in works as expected on chrome.

I’d estimate it works 1/20 times on chrome, and nearly every time on edge and firefox, I have seen it fail on both, but it’s very infrequent.

Edit: to clarify, I just mean refreshing the page or using back/forward. No weird window shenanigans.