Issue with the connections when toggling the visibility of specific node types

Hello im implementing a graph in my angular project (typescript) that has 3 types of nodes ( virtualMachines, databases and external services). In my case i want with a button to toggle the visibility of the external services type nodes.
The way i did that is the following:

private showExternals = true;

  toggleExternals() {
    this.showExternals = !this.showExternals;
    this.diagram.commit((diag) => {
      diag.nodes.each((n: any) => {
        if (n.ob.AssetType === 'externals') {
          n.visible = this.showExternals;
        }
      });
    }, 'updated filter');
  }

This currently works but if i move one of the nodes(virtual machine or database) the moment that my external services are not visible here is my end results:

Step 1: nodes before toggling visibility

Step 2: after i hide externals

Step 3: moving one node to a new position

Step 4: making external node visible again final result

If i dont move the nodes at all when i toggle there is no issues.
It looks like when i enable visibility the nodes and connections that are comming back dont respect the position of the changed nodes loc.
I Guess that the way i toggle nodes visibility is causing this. any suggestion? thanks

Yes, that is because (by default) changing the visible property of any Node or Link will invalidate the Layout and cause it to be performed again towards the end of the transaction.

If you don’t want that to happen, there are several possibilities, depending on what you want to do. Easiest would be to just set Layout.isOngoing to false.

But maybe you want a layout to happen when you add or remove Nodes or Links. In that case it might be best not to set Layout.isOngoing to false but to set Part.layoutConditions to a bit mask including the conditions under which you want the layout to be invalidated. Maybe you’ll want to set Part.layoutConditions on your Node template(s) and Link template(s) such that it does not include the go.Part.LayoutShown and go.Part.LayoutHidden flags.

Yet another possibility is instead of setting Node.visible, to set Node.opacity to zero or one. You would probably also want to do the same for all Links connected to Nodes whose opacity you change.

For more information: GoJS Layouts -- Northwoods Software

thanks but can you give me an example how to set layout isOngoing to false?

here is what i tried and doesnt seems to work:

  this.diagram = $(go.Diagram, this.graphId, {
      // initialAutoScale: go.Diagram.Uniform,
      mouseDrop: (e: go.InputEvent) => {
        this.finishDrop(e, null);
      },
      'commandHandler.archetypeGroupData': {
        isGroup: false,
        text: 'Group',
        horiz: false,
      },
      'grid.visible': false,
      'undoManager.isEnabled': true,
      'draggingTool.isGridSnapEnabled': false,
    });

    this.diagram.layout.isOngoing = false;



Do you set the Diagram.layout property at all? If not, then setting Layout.isOngoing to false won’t help.

I think I completely misunderstood the situation. Did all of the steps that you show above happen in the same session without reloading any model data?

yes correct there was no big change between steps. only the small location change of one node

Could you please try something like:

  toggleExternals() {
    this.showExternals = !this.showExternals;
    this.diagram.commit((diag) => {
      diag.nodes.each((n: any) => {
        if (n.data.AssetType === 'externals') {
          n.visible = this.showExternals;
        }
      });
      diag.links.each(l => l.invalidateRoute());
    }, 'updated filter');
  }

Note also that I have replaced your use of the undocumented “ob” property with my guess as to what you really want to do. All two letter property/method names are minified names that will change in future builds. (Except for the InputEvent.up property.)

did that and looks like the connections mesh up after making externals visible again

step1:


step2 (hide externals and moving one node):

step3:

You have Link.routing set to go.Link.AvoidsNodes for all of your Links, don’t you?

yes here is the links code:

 this.diagram.linkTemplate = $(
      go.Link,
      {
        routing: go.Link.AvoidsNodes,
        reshapable: true,
        resegmentable: true,
        relinkableFrom: this.allowEdit,
        relinkableTo: this.allowEdit,
      },
      new go.Binding('points').makeTwoWay(),
      $(go.Shape),
      $(go.Shape, { toArrow: 'Standard' })
    );

The link template looks fine.

I’m unable to reproduce the problem on my own. Here’s my code:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button id="myHideButton">Hide some</button><button id="myShowButton">Show all</button>

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      layout: $(go.GridLayout,
        { wrappingColumn: 6, spacing: new go.Size(50, 50), isOngoing: false }),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    { width: 50, height: 50 },
    $(go.Shape,
      { fill: "lightgray", strokeWidth: 0 }),
    $(go.TextBlock,
      { margin: 8, stroke: "gray" },
      new go.Binding("text", "key"))
  );

myDiagram.linkTemplate =
  $(go.Link,
    { routing: go.Link.AvoidsNodes, corner: 10 },
    $(go.Shape),
    $(go.Shape, { toArrow: "OpenTriangle" })
  );

const NUM = 30;
const nda = [];
for (let i = 0; i < NUM; i++) nda.push({ key: i });
const lda = [];
for (let i = 0; i < 30; i++) lda.push({ from: Math.floor(Math.random() * NUM), to: Math.floor(Math.random() * NUM) });

myDiagram.model = new go.GraphLinksModel(nda, lda);

document.getElementById("myHideButton").addEventListener("click", e => {
    myDiagram.commit(d => {
      d.nodes.each(n => n.visible = n.isSelected || Math.random() > 0.1);
    });
  });

document.getElementById("myShowButton").addEventListener("click", e => {
    myDiagram.commit(d => {
      d.nodes.each(n => n.visible = true);
    });
  });
  </script>
</body>
</html>

Note that I don’t need to call Link.invalidateRoute.

very strange cause im using the same example and still have this issue
Im posting here the core parts of my code if i did something wrong

  initDiagram() {
   this.diagram = $(go.Diagram, this.graphId, {
      layout: $(go.GridLayout,
        { wrappingColumn: 6, spacing: new go.Size(50, 50), isOngoing: false }),

      mouseDrop: (e: go.InputEvent) => {
        this.finishDrop(e, null);
      },
      'grid.visible': false,
      'undoManager.isEnabled': true,
      'draggingTool.isGridSnapEnabled': false,
    });
    
 
     this.diagram.nodeTemplate =  this.getTemplateWithShape();;
   
     this.diagram.linkTemplate = $(
      go.Link,
      {
        routing: go.Link.AvoidsNodes,
        reshapable: true,
        resegmentable: true,
        relinkableFrom: this.allowEdit,
        relinkableTo: this.allowEdit,
      },
      new go.Binding('points').makeTwoWay(),
      $(go.Shape),
      $(go.Shape, { toArrow: 'Standard' })
      );  
  }

 getTemplateWithShape() {
    return $(
      go.Node,
      'Auto',
      {

        padding: 20,
        selectionAdorned: false,
        cursor: 'pointer',
        selectionObjectName: 'SHAPE',
        toolTip: $(
          'ToolTip',
          $(
            go.TextBlock,
            { margin: 4, width: 140 },
            new go.Binding('text', '', (data) => data.text)
          )
        ),
        mouseDrop: (e, node: any) => {
          this.finishDrop(e, node.containingGroup);
        },
        doubleClick: (e, node: any) => {
          const selectedNode = e.diagram.selection.first();
          if (!selectedNode?.data.inPalette) {
            this.graphService.setNodeData({
              ...node.data,
              diagram: e.diagram,
              selectedNode,
              open: true,
            });
          }
        }
      },
      new go.Binding('location', 'loc').makeTwoWay(),
      $(
        go.Panel,
        'Spot',
        $(
          go.Shape,
          'RoundedRectangle',
          {
            strokeWidth: 1,
            width: 80,
            height: 80,
            stroke: '#cfcfcf',
            portId: '',
            cursor: 'pointer', 
            alignment: go.Spot.Center,
            alignmentFocus: go.Spot.Bottom,
            fromLinkable: this.allowEdit,
            fromLinkableSelfNode: this.allowEdit,
            fromLinkableDuplicates: this.allowEdit,
            toLinkable: this.allowEdit,
            toLinkableSelfNode: false,
            toLinkableDuplicates: false,
          },
          new go.Binding('fill', 'color').makeTwoWay()
        ),
        $(
          go.Shape,
          {
            margin: 0,
            fill: 'black',
            strokeWidth: 0,
          },
          new go.Binding('geometry', 'AssetType', this.geoFunc)
        ),
        $(
          go.TextBlock,
          {
            margin: 13,
            editable: this.allowEdit,
            font: 'bold 13px sans-serif',
            opacity: 0.75,
            stroke: '#404040',
            alignment: go.Spot.BottomCenter,
            alignmentFocus: go.Spot.TopCenter,
            isMultiline: false,
          },
          new go.Binding('text', 'text').makeTwoWay()
        )
      )
    );
  }

toggleExternals() {
    this.showExternals = !this.showExternals;
    this.diagram.commit((diag) => {
      diag.nodes.each((n: any) => {
        if (n.data.AssetType === 'externals') {
          n.visible = this.showExternals;
        }
      });
      diag.links.each(l => {
        l.invalidateRoute()
      });
    
    }, 'updated filter');



 HTML
   <button mat-button  (click)="toggleExternals()">Toggle Externals</button>

   <div style="width: 100%; display: flex; justify-content: space-between; overflow: hidden;">
      <div *ngIf="allowEdit" id="myPaletteDiv" style="width: 230px; margin-right: 2px; background-color: whitesmoke; border: solid 1px black"></div>
      <div *ngIf="!allowEdit"  id="asis" class="graph"></div>
      <div *ngIf="allowEdit"  id="tobe" class="graph"></div>
    </div>
  

i think somewhere here willl be the issue. the rest code doesnt affect diagram config

OK, I’ve used your templates, substituting true for this.allowEdit, commenting out the use of this.graphService, and not depending on data.AssetType. Still, everything works as expected.

But I think the padding on the Node makes each Node bigger than it looks, thereby decreasing the room available for links to route without crossing over nodes. Does removing that padding help?

Also, the margin on the TextBlock adds empty space on the bottom of the node, again possibly interfering with routing.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button id="myHideButton">Hide some</button><button id="myShowButton">Show all</button>

  <script src="go.js"></script>
  <script id="code">
const $ = go.GraphObject.make;

const myDiagram =
  $(go.Diagram, "myDiagramDiv",
    {
      layout: $(go.GridLayout,
        { wrappingColumn: 6, spacing: new go.Size(50, 50), isOngoing: false }),
      "undoManager.isEnabled": true
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    {
      // padding: 20,
      selectionAdorned: false,
      cursor: 'pointer',
      selectionObjectName: 'SHAPE',
      toolTip: $('ToolTip',
        $(
          go.TextBlock,
          { margin: 4, width: 140 },
          new go.Binding('text', '', (data) => data.text)
        )
      ),
      mouseDrop: (e, node) => {
        this.finishDrop(e, node.containingGroup);
      },
      doubleClick: (e, node) => {
        const selectedNode = e.diagram.selection.first();
        if (!selectedNode?.data.inPalette) {
          alert(node.key)
          // this.graphService.setNodeData({
          //   ...node.data,
          //   diagram: e.diagram,
          //   selectedNode,
          //   open: true,
          // });
        }
      }
    },
    new go.Binding('location', 'loc').makeTwoWay(),
    $(go.Panel, 'Spot',
      $(go.Shape, 'RoundedRectangle',
        {
          strokeWidth: 1,
          width: 80,
          height: 80,
          stroke: '#cfcfcf',
          portId: '',
          cursor: 'pointer', 
          alignment: go.Spot.Center,
          alignmentFocus: go.Spot.Bottom,
          fromLinkable: true /* this.allowEdit */,
          fromLinkableSelfNode: true /* this.allowEdit */,
          fromLinkableDuplicates: true /* this.allowEdit */,
          toLinkable: true /* this.allowEdit */,
          toLinkableSelfNode: false,
          toLinkableDuplicates: false,
          fill: "lightgray"
        },
        new go.Binding('fill', 'color').makeTwoWay()
      ),
      $(go.Shape,
        {
          margin: 0,
          fill: 'black',
          strokeWidth: 0,
          geometryString: "F1 M0 0 L20 0 20 20 0 20z"
        },
        /* new go.Binding('geometry', 'AssetType', this.geoFunc) */
      ),
      $(go.TextBlock,
        {
          // margin: 13,
          editable: true /* this.allowEdit */,
          font: 'bold 13px sans-serif',
          opacity: 0.75,
          stroke: '#404040',
          alignment: go.Spot.BottomCenter,
          alignmentFocus: go.Spot.Bottom, //TopCenter,
          isMultiline: false,
        },
        new go.Binding('text', 'text').makeTwoWay()
      )
    )
  );

myDiagram.linkTemplate =
  $(go.Link,
    { routing: go.Link.AvoidsNodes, corner: 10 },
    new go.Binding("points").makeTwoWay(),
    $(go.Shape),
    $(go.Shape, { toArrow: "OpenTriangle" })
  );

const NUM = 30;
const nda = [];
for (let i = 0; i < NUM; i++) nda.push({ key: i, text: i.toString() });
const lda = [];
for (let i = 0; i < 30; i++) lda.push({ from: Math.floor(Math.random() * NUM), to: Math.floor(Math.random() * NUM) });

myDiagram.model = new go.GraphLinksModel(nda, lda);

document.getElementById("myHideButton").addEventListener("click", e => {
    myDiagram.commit(d => {
      d.nodes.each(n => n.visible = n.isSelected || Math.random() > 0.1);
    });
  });

document.getElementById("myShowButton").addEventListener("click", e => {
    myDiagram.commit(d => {
      d.nodes.each(n => n.visible = true);
      d.links.each(l => l.invalidateRoute());
    });
  });
  </script>
</body>
</html>