Update node pos after autoLayout

I do my tests with the gojs-react-basic sample

I added this to my diagram init (because at first some nodes have no location)

'InitialLayoutCompleted': function (e) {
  // if not all Nodes have real locations, force a layout to happen
  if (!e.diagram.nodes.all(function (n) {
    return n.location.isReal();
  })) {
    e.diagram.layoutDiagram(true);
  }
  updateNodePosOnLayoutCompleted(e.diagram.nodes);
}

And I try to make this to work

public updateNodePosOnLayoutCompleted(nodes: go.Iterator<go.Node>) {
  this.setState(
    produce((draft: AppState) => {
      nodes.each(n => {
        const idx = this.mapNodeKeyIdx.get(n.key) as number;
        const data = draft.nodeDataArray[idx] as go.ObjectData;
        data["loc"] = `${n.location.x} ${n.location.y}`;
        data["size"] = `${n.height} ${n.width}`;
        draft.nodeDataArray[idx] = data;
      })
      draft.skipsDiagramUpdate = false;
    })
  );
}

loc and sizes are well filled with good data but the nodeobject is not

The data in the Diagram.Model is assumed to be mutable. Just use a TwoWay Binding on the Node.location property (and if desired, a TwoWay Binding on the Node.desiredSize property), and you won’t need that updateNodePosOnLayoutCompleted function at all.

This is one of my templates
They already have the makeTwoWay binding but they are not updating the original value, did I miss something ?

import go from 'gojs';
import { portDecoration } from './nodePorts';
import { textFont, titleFont } from './templateParams';
import { getContextMenuTemplate } from './ContextMenuTemplate';

const $ = go.GraphObject.make;

function sizeParse(size: string){
  if(size == null) return null;
  return go.Size.parse(size);
}

export const getNodeTemplate = () => {

  return $(go.Node, 'Table',
    {
      isShadowed: false,
      shadowColor: 'black',
      shadowOffset: new go.Point(10, 10),
      locationObjectName: 'BODY',
      selectionObjectName: 'BODY',
      locationSpot: go.Spot.TopLeft,
      resizable: true,
      minSize: new go.Size(60, 40),
      contextMenu: getContextMenuTemplate,
      
      selectionAdorned: false,
    },
    new go.Binding('isShadowed', 'shadowed'),
    new go.Binding('isLayoutPositioned', 'layoutPositioned'),
    new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
    new go.Binding('height', 'height').makeTwoWay(),
    new go.Binding('width', 'width').makeTwoWay(),
    new go.Binding('desiredSize', 'size', sizeParse).makeTwoWay(go.Size.stringify),
    new go.Binding('visible', 'visible').makeTwoWay(),
    $(go.Panel, 'Auto',
      {
        row: 1, column: 1, name: 'BODY',
        stretch: go.GraphObject.Fill
      },
      $(go.Shape,
        {
          figure: 'Rectangle',
          stroke: null,
          fill: 'white',
          stretch: go.GraphObject.Fill
        },
        new go.Binding('figure', 'shape'),
        new go.Binding('fill', 'color'),
        new go.Binding('stroke', 'borderColor'),
        new go.Binding('strokeWidth', 'widthBorder'),
        new go.Binding('strokeDashArray', 'widthBorderArray')
      ),
      $(go.Panel, 'Table',
        { margin: 2, stretch: go.GraphObject.Fill, alignment: go.Spot.Center, row: 1, column: 0 },
        $(go.Panel, 'Vertical',
          { stretch: go.GraphObject.Horizontal },
          new go.Binding('margin', 'schemaName',
            function (schemaName) {
              return schemaName !== 'PSE' ?
                new go.Margin(2, 22, 2, 22) : new go.Margin(2, 2, 2, 2);
            }),
          $(go.TextBlock,
            { margin: 4, font: titleFont },
            new go.Binding('font', 'schemaName',
              function (schemaName) {
                return schemaName !== 'PSE' ?
                  titleFont : titleFont.replace('bold', '');
              }),
            new go.Binding('stroke', 'fontColor'),
            new go.Binding('text', 'reference')),
          $(go.TextBlock,
            {
              font: textFont,
              margin: 4, wrap: go.TextBlock.WrapFit, overflow: go.TextBlock.OverflowEllipsis,
              maxLines: 5
            },
            new go.Binding('stroke', 'fontColor'),
            new go.Binding('text', 'name'),
            new go.Binding('visible', 'schemaName',
              function (schemaName) {
                return schemaName !== 'PSE';
              })
          ),
          $(go.Picture,
            new go.Binding('source', 'icon')
          )
        )
      ),
      $(go.Picture,
        { margin: 1, width: 24, height: 24, alignment: go.Spot.TopLeft },
        new go.Binding('source', 'flowDirectionIcon'),
        new go.Binding('name', 'key'),
        {
          toolTip:
            $('ToolTip',
              $(go.TextBlock, { margin: 3 },
                new go.Binding('text', 'flowDirectionIconTooltip')
              ))
        },
        {
          click: (e: any, node: any) => console.log(e, node)
        }
      )
    ),
    portDecoration("top"),
    portDecoration("right"),
    portDecoration("left"),
    portDecoration("bottom"),
  );
}

Delete these two Bindings because they are redundant with the desiredSize/“size” Binding:

Also, one cannot set desiredSize to null. Return new go.Size(NaN, NaN) instead.

Those Bindings will update the GoJS model. Your code has to update your app state. Check how the onModelChange function is implemented.

Hey,

I finally managed to make it work

public updateNodesOnLayoutCompleted(nodes: go.Iterator<go.Node>) {
    this.setState(
      produce((draft: AppState) => {
        nodes.each(n => {
          const idx = this.mapNodeKeyIdx.get(n.key) as number;
          const data = draft.nodeDataArray[idx] as go.ObjectData;
          data["loc"] = `${n.location.x} ${n.location.y}`;
          data["size"] = `${n.measuredBounds.width} ${n.measuredBounds.height} `;
          draft.nodeDataArray[idx] = data;
        })
        draft.skipsDiagramUpdate = true;

      })
    );
  }

and moved the call of the funtion in the LayoutCompleted event in the schema initialisation

That’s OK when a layout happens, but how do you update your state when no layout is performed? That’s why I asked about what your onModelChange is doing. Get it right and you won’t need this updateNodesOnLayoutCompleted function called after each layout at all.

I didn’t understand it that way my bad,
I use the unmodified function handleModelChange given by the gojs-react-basic sample ( NorthwoodsSoftware/gojs-react-basic: An example project demonstrating usage of GoJS and React together (github.com) )

And that sample’s handleModelChange function is suitable for that sample’s props/state – its “schema”. But I don’t know what “schema” you have for your app’s state.

I am using near the same data structure if it’s what you are speaking about


      nodeDataArray: [
        { key: 0, text: 'Alpha', color: 'lightblue', loc: '-1 1', size: '50 60' },
        { key: 1, text: 'Beta', color: 'orange', loc: '', size: '50 60' },
        { key: 2, text: 'Gamma', color: 'lightgreen', loc: '', size: '30 60' },
        { key: 3, text: 'Delta', color: 'pink', loc: '150 150', size: '30 60' }
      ],
      linkDataArray: [
        { key: -1, from: 0, to: 1 },
        { key: -2, from: 0, to: 2 },
        { key: -3, from: 1, to: 1 },
        { key: -4, from: 2, to: 3 },
        { key: -5, from: 3, to: 0 }
      ],

If you don’t speak about that, can you be more precise (or am I missing something ?)

Well, if your diagram doesn’t let the user modify the “text” property (i.e. no TextBlock has TextBlock.editable set or bound to true, and no code setting it directly) and if your app doesn’t have any code changing the “color” property, then it’s mostly OK.

The one case I can think of is when the user drags a node (or a few of them). Normally no layout happens, so your “updateNodesOnLayoutCompleted” function won’t get called, so your state won’t get updated node location information.

To come back to the original error
My issue was that when there is no data in the loc or in the size, the nodes are well placed by the auto layout but they are not updated by the handleModelChange
In this case if I move an object with the Inspector it will move it right (like if I want to move his x pos, x is good but all others are set to 0) And in this case all elements go back to 0 0 and min height/width values
The second option is to move every element by hand, in this case handleModelChange update the nodeDataArray state and if I edit the value in the inspector it works

That’s probably because normally one doesn’t want the source data to be modified when loading a diagram model. But the scenario you describe sounds like an exception – you do want to update your app state upon just showing the model/diagram. In such a case I suggest that you explicitly do an update of the app state, but not within the initial transaction. In other words, not in an “InitialLayoutCompleted” (or “LayoutCompleted”) listener. Have your “InitialLayoutCompleted” listener queue up the update for later.

I am taking the project and have no back logs of what is done and why, that’s why I am using the gojs-react-basic

I found out that (in the initDiagram function)

if (autoLayout && diagram !== undefined && diagram !== null) {
   // this is working
    diagram.layout = applyLayout();
   //this is not
    diagram.nodes.each((node: any) => {
      if (node.data.isGroup) {
        node.layout = applyLayout();
      }
    });
  }

And the old implementation was

diagram.addDiagramListener('InitialLayoutCompleted', (_) => {
    setTimeout(() => {

      if (true || autoLayout && diagram !== undefined && diagram !== null) {
        resetLayout(diagram, () => applyLayout());
        diagram.layoutDiagram(true);
        resetLayout(diagram, () => new go.Layout());
      }

    }, 100);
  });


function resetLayout(diagram: go.Diagram, layoutBuilder: () => go.Layout) {
  diagram.layout = layoutBuilder();
  diagram.nodes.each((node: any) => {
    if (node.data.isGroup) {
      node.layout = layoutBuilder();
    }
  });
}

(bit of a nightmare)
But it seems that they managed to make the schema.nodes.each… part working is by using the timeout
Do you have a better solution ?

(Note that now I have the position updating without updateNodesOnLayoutCompleted but not the height and width)

That does seem contorted. Normally one checks the model data first (before merging the data with the GoJS model) to see if there are any node data that don’t have location information. If that’s the case, then queue up a call to Diagram.layoutDiagram with a true argument, to be called after everything has been initialized. The normal app state update mechanisms should happen normally, as if the user had clicked an HTML button to call Diagram.layoutDiagram.