Groups Expand and Collapse are taking time more than 5 seconds

We are trying to expand and collapse all the groups on the diagram with a toggle button and we are using the following syntax:

myDiagram.commit((diag) =>
        diag.nodes.each((g) => {
          if (g instanceof go.Group){
            g.isSubGraphExpanded = toggle;
            g.wasSubGraphExpanded = toggle
          } 
        })
      );

This is working fine in gojs version 2.3.11 . Recently we upgraded to 3.0.9 and are facing this issue. Could you please suggest any changes required to resolve this issue?

Are you saying that your code is still working correctly, but that it is slower in v3.0.9? If so, is the slowness the same for both expanding and collapsing?

We need more information in order for us to understand where the problem might be. Could you show us a screenshot of an expanded diagram?

How long for the same large diagram did it take to expand in v2.3.11 and now in v3.0.9? And what times are you getting for collapsing?

Are the changes animated? Is there a performance problem if you disable all animations?

Hi,
Thanks for replying .
Answering to your questions:

  1. Are you saying that your code is still working correctly, but that it is slower in v3.0.9? If so, is the slowness the same for both expanding and collapsing?

Yes the code is working correctly but it is way slower with v3.0.9. And yes it takes same time for expanding and collapsing

  1. We need more information in order for us to understand where the problem might be. Could you show us a screenshot of an expanded diagram?

  2. How long for the same large diagram did it take to expand in v2.3.11 and now in v3.0.9? And what times are you getting for collapsing?

Yes, we are using the same diagram in both the versions. It used to be less than a second in v2.3.11 and now it is approximate 10seconds or more in v3.0.9.

  1. Are the changes animated? Is there a performance problem if you disable all animations?

Yes, we only have a transition animation and tried removing the animation and it is taking more time than earlier.

What are your Diagram.layout and Group.layout?

What is your link template definition?

(By the way, an easy way to redact a diagram is to temporarily set the TextBlock.background to be the same color as the TextBlock.stroke, which defaults to “black”.)

Diagram.layout

diagram.layout = new go.Layout();

Group.layout

var groupingLayout = $(go.LayeredDigraphLayout, {
      columnSpacing: 40,
      layerSpacing: 80,
      layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
      aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
      packOption: go.LayeredDigraphLayout.PackAll,
      isOngoing: false,
      isInitial: false,
      isRealtime: false,
      direction: 90,
      setsPortSpots: false
    });

Link Template

(
      go.Link,
      { selectable: true, selectionAdorned: false },
      { relinkableFrom: true, relinkableTo: true, reshapable: false },
      {
        routing: go.Link.Orthogonal,
        curve: go.Link.None,
        cursor: me.linkCursorStyle(),
        toolTip: $(
          go.Adornment,
          'Auto',
          {
            isShadowed: true,
            shadowBlur: 10,
            shadowOffset: new go.Point(0, 0)
          },
          $(go.Shape, 'RoundedRectangle', { stroke: '#fff', fill: '#fff' }),
          $(
            go.Panel,
            'Vertical',
            { padding: 4 },
            $(
              go.Panel,
              'Horizontal',
              $(go.TextBlock, {
                margin: new go.Margin(0, 5, 0, 0),
                stroke: '#212122',
                font: '500 14px Outfit, sans-serif',
                text: 'Stream ID:'
              }),
              $(
                go.TextBlock,
                { stroke: $primaryColor, font: '500 14px Outfit, sans-serif' },
                new go.Binding('text', 'streamName')
              )
            ),
            $(
              go.Panel,
              'Table',
              { margin: new go.Margin(6, 0, 0, 0), alignment: go.Spot.Left },
              new go.Binding('visible', 'streamFieldsHas'),
              new go.Binding('itemArray', 'streamFields'),
              {
                itemTemplate: $(
                  go.Panel,
                  'Auto',
                  new go.Binding('column'),
                  new go.Binding('row'),
                  { margin: 2, stretch: go.GraphObject.Horizontal },
                  $(go.Shape, 'RoundedRectangle', { fill: '#E6F4FB' }),
                  $(go.TextBlock, new go.Binding('text'), {
                    margin: 2,
                    font: '13px Outfit, sans-serif'
                  })
                )
              }
            ),
            $(
              go.TextBlock,
              new go.Binding('text', 'streamFieldsExtra'),
              new go.Binding('visible', 'streamFieldsHasExtra'),
              { margin: 2, font: '13px Outfit, sans-serif' }
            )
          )
        )
      },
      {
        fromPortChanged: me.linkEndsChanged.bind(me),
        toPortChanged: me.linkEndsChanged.bind(me),
        selectionChanged: me.selectionChanged.bind(me),
        mouseEnter: me.mouseEnterLink.bind(me),
        mouseLeave: me.mouseLeaveLink.bind(me),
        mouseDragEnter: me.mouseEnterLink.bind(me),
        mouseDragLeave: me.mouseLeaveLink.bind(me),
        mouseDrop: me.autoConnect.bind(me)
      },
      new go.Binding('opacity', '', me.checkStepFilter.bind(me)),
      new go.Binding('toShortLength', 'linkStrokeWidth'),
      $(
        go.Shape,
        new go.Binding('stroke', '', me.linkStroke).ofObject(),
        new go.Binding('strokeWidth', 'linkStrokeWidth'),
        { isPanelMain: true, name: 'LINK_PATH' }
      ),
      $(go.Shape,
        new go.Binding('visible', 'hasCondition'),
        new go.Binding('stroke', '', me.linkStroke).ofObject(),
        new go.Binding('toolTip', 'stream', me.predicateTooltip.bind(me)),
        { strokeWidth: 1 },
        { fromArrow: 'TripleFeathers', name: 'PREDICATE', scale: 1.5 }),

      $(
        go.Shape,
        new go.Binding('stroke', '', me.linkStroke).ofObject(),
        new go.Binding('fill', '', me.linkArrowFill.bind(me)).ofObject(),
        new go.Binding('strokeWidth', 'linkStrokeWidth'),
        new go.Binding('segmentOffset', 'linkStrokeWidth', function (w) {
          return new go.Point(-1 * ((w - 3) / 2), 0);
        }),
        { toArrow: 'Triangle', name: 'LINK_ARROW' }
      ),
      $(
        go.Shape,
        {
          segmentIndex: -1,
          segmentFraction: 0,
          segmentOffset: new go.Point(-10, 0)
        },
        new go.Binding('visible', 'moreTo'),
        new go.Binding('stroke', '', me.linkStroke).ofObject(),
        new go.Binding('fill', '', me.linkArrowFill.bind(me)).ofObject(),
        new go.Binding('strokeWidth', 'linkStrokeWidth'),
        { toArrow: 'Triangle', name: 'LINK_ARROW_AUX' }
      ),
      $(
        go.Panel,
        'Auto',
        new go.Binding('segmentOffset', 'linkStrokeWidth', function (w) {
          return new go.Point(-1 * ((w - 3) / 2), 0);
        }),
        $(
          go.Shape,
          'RoundedRectangle',
          { name: 'GROUPING-SHAPE', strokeWidth: 1, isPanelMain: true },
          { parameter1: 9 },
          new go.Binding('stroke', '', me.groupStroke).ofObject(),
          new go.Binding('strokeDashArray', '', me.groupStrokeDash).ofObject(),
          new go.Binding('fill', '', me.groupFill).ofObject()
        ),
        $(
          go.Panel,
          'Horizontal',
          new go.Binding('margin', '', me.groupMargin).ofObject(),
          $(
            go.TextBlock,
            {
              margin: new go.Margin(0, 1, 0, 0),
              stretch: go.Stretch.Fill,
              name: 'GROUPING-TEXT',
              font: '11px Outfit, sans-serif',
              stroke: '#fff'
            },
            new go.Binding('text', 'groupingBy', function (groupingBy) {
              return groupingBy || '';
            }),
            new go.Binding('visible', 'groupingBy', function (groupingBy) {
              return !!groupingBy;
            })
          ),
          this.state.isReadOnly
            ? {}
            : $(
              go.Panel,
              'Spot',
              {
                name: 'GROUPING_CIRCLE_PANEL',
                height: 11,
                width: 11,
                stretch: go.Stretch.Fill
              },
              new go.Binding('visible', 'isSelected').ofObject(),
              { click: this.groupingClick.bind(this) },
              $(go.Shape, 'Circle', {
                stroke: null,
                fill: '#fff',
                isPanelMain: true,
                stretch: go.Stretch.Fill
              }),
              $(
                go.Shape,
                {
                  angle: 90,
                  strokeWidth: 1,
                  fill: null,
                  width: 3,
                  height: 6,
                  alignment: go.Spot.Center
                },
                {
                  geometryString: GeoStrings.OPEN_TRI_GEO_STR
                },
                new go.Binding('stroke', '', me.linkStroke).ofObject()
              )
            )
        )
      )
    );

For v3.0 the LayeredDigraphLayout is operating differently, because you haven’t set alignOption, but it should almost always be faster when it does run, and since Layout.isOngoing is false, it shouldn’t be running at all during expand or collapse.

Your links do not use AvoidsNodes routing, so that’s another expensive computation that is avoided. There are a lot of Bindings that have the whole data object as the source, which is distinctly slower than when specifying the source property, but I think those will not be evaluated due to an expand or a collapse.

So it’s still a mystery to me why it would be slower in v3.0. Is there any chance we could try to debug your diagram?

Hi ,
I am not able to attach the diagram here as it is a big file , so sending a mail on GoJS at your domain nwoods.com listing the heading of this issue and the diagram-state along with it. Please note that the issue is not with individual grouping but when we are trying to expand or collapse all the groups together using the tool.

I got it – thanks! I’ll try some experiments.

Here’s what I came up with by parsing the SVG and guessing about the group memberships.

I find that the performance of collapsing and expanding groups in v3.0 is good. I’m still wondering how this is different from your situation.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
  <button onclick="myDiagram.layoutDiagram(true)">Layout</button>
  <button onclick="collapse()">Collapse All</button>
  <button onclick="expand()">Expand All</button>
  The model in JSON format:
  <textarea id="mySavedModel" style="width:100%;height:250px">
{ "class": "GraphLinksModel",
  "linkFromPortIdProperty": "fpid",
  "linkToPortIdProperty": "tpid",
  "nodeDataArray": [
{"key":-28},
{"key":"gr-5","isGroup":true},
{"key":"gr-9","isGroup":true},
{"key":"gr-3","isGroup":true},
{"key":"qqqtipxwtir","group":"gr-3"},
{"key":"gr-6","isGroup":true},
{"key":"ncivxmhbezjldeviucyci","group":"gr-6"},
{"key":"gr-7","isGroup":true},
{"key":"gr-4","isGroup":true},
{"key":"zaqirhwvvcirmlhdtba","group":"gr-4"},
{"key":"gr-8","isGroup":true},
{"key":"prslehdaftrcedkyxyfjrnwmybaxmbjbnehh","group":"gr-8"},
{"key":"gr-11","isGroup":true},
{"key":"gr-12","isGroup":true},
{"key":"vkhhwcw","group":"gr-12"},
{"key":"upzkhyiwyouvvenhzmwgxwf","group":"gr-12"},
{"key":"drydwhyh","group":"gr-12"},
{"key":"xhtttnsdkrmzpod","group":"gr-12"},
{"key":"ezhnam","group":"gr-12"},
{"key":"jqxjtuc","group":"gr-12"},
{"key":"fzgvx","group":"gr-12"},
{"key":"hiwhmkscwbengwntw","group":"gr-12"},
{"key":"gr-14","isGroup":true},
{"key":"mjec","group":"gr-14"},
{"key":"ooezdivexhsrpguriuki","group":"gr-14"},
{"key":"sfjnanljslirukwmevb","group":"gr-14"},
{"key":"gr-14-1051","isGroup":true},
{"key":"gr-31","isGroup":true},
{"group":"gr-31","key":-29},
{"group":"gr-31","key":-30}
],
  "linkDataArray": [
{"from":"norrrgdpqjbvnlrwwtukuaa","fpid":"B","to":"ncivxmhbezjldeviucyci","tpid":"T"},
{"from":"norrrgdpqjbvnlrwwtukuaa","fpid":"B","to":"vkhhwcw","tpid":"T"},
{"from":"norrrgdpqjbvnlrwwtukuaa","fpid":"E","to":"mgdoxemspvtpmwyoldhyhpnwsfz","tpid":"T"},
{"from":"jpkzlxxgodk","fpid":"B","to":"ezhnam","tpid":"T"},
{"from":"joyqwwzdnrzihdbmpcgzoq","fpid":"B","to":"drydwhyh","tpid":"T"},
{"from":"ncivxmhbezjldeviucyci","fpid":"B","to":"qqqtipxwtir","tpid":"T"},
{"from":"ncivxmhbezjldeviucyci","fpid":"B","to":"mjec","tpid":"T"},
{"from":"gfiybttxrdbcieo","fpid":"B-strCanadianRetail","to":"gubfvbudrwumtsstzyics","tpid":"T"},
{"from":"gfiybttxrdbcieo","fpid":"B-strCanadianCommercial","to":"kbhziivaetyrowglauvmyvozmhi","tpid":"T"},
{"from":"gfiybttxrdbcieo","fpid":"B-strUsRetail","to":"mftvqcfgkxygzsrqe","tpid":"T"},
{"from":"gfiybttxrdbcieo","fpid":"B-strUsCommercial","to":"sfrsrvxetarskyvb","tpid":"T"},
{"from":"zaqirhwvvcirmlhdtba","fpid":"B","to":"gfiybttxrdbcieo","tpid":"T"},
{"from":"zaqirhwvvcirmlhdtba","fpid":"E","to":"prslehdaftrcedkyxyfjrnwmybaxmbjbnehh","tpid":"T"},
{"from":"kbhziivaetyrowglauvmyvozmhi","fpid":"B","to":"ncivxmhbezjldeviucyci","tpid":"T"},
{"from":"kbhziivaetyrowglauvmyvozmhi","fpid":"B","to":"vkhhwcw","tpid":"T"},
{"from":"kbhziivaetyrowglauvmyvozmhi","fpid":"E","to":"mgdoxemspvtpmwyoldhyhpnwsfz","tpid":"T"},
{"from":"prslehdaftrcedkyxyfjrnwmybaxmbjbnehh","fpid":"B","to":"joyqwwzdnrzihdbmpcgzoq","tpid":"T"},
{"from":"npdefvoijlldgctszobocrymfkr","fpid":"B","to":"joyqwwzdnrzihdbmpcgzoq","tpid":"T"},
{"from":"mftvqcfgkxygzsrqe","fpid":"B","to":"vkhhwcw","tpid":"T"},
{"from":"mftvqcfgkxygzsrqe","fpid":"B","to":"ncivxmhbezjldeviucyci","tpid":"T"},
{"from":"mftvqcfgkxygzsrqe","fpid":"E","to":"mgdoxemspvtpmwyoldhyhpnwsfz","tpid":"T"},
{"from":"ulyxsahreacdhwllzwfeb","fpid":"B","to":"ncivxmhbezjldeviucyci","tpid":"T"},
{"from":"ulyxsahreacdhwllzwfeb","fpid":"B","to":"vkhhwcw","tpid":"T"},
{"from":"ulyxsahreacdhwllzwfeb","fpid":"E","to":"mgdoxemspvtpmwyoldhyhpnwsfz","tpid":"T"},
{"from":"upzkhyiwyouvvenhzmwgxwf","fpid":"B","to":"zaqirhwvvcirmlhdtba","tpid":"T"},
{"from":"upzkhyiwyouvvenhzmwgxwf","fpid":"E","to":"prslehdaftrcedkyxyfjrnwmybaxmbjbnehh","tpid":"T"},
{"from":"ezhnam","fpid":"B","to":"fzgvx","tpid":"T"},
{"from":"fzgvx","fpid":"B-s-15411","to":"upzkhyiwyouvvenhzmwgxwf","tpid":"T"},
{"from":"fzgvx","fpid":"B-s-15458","to":"hiwhmkscwbengwntw","tpid":"T"},
{"from":"gubfvbudrwumtsstzyics","fpid":"B-s-2012","to":"wglmqhglbmb","tpid":"T"},
{"from":"dqaaohwczlthbhpzkk","fpid":"B","to":"jqxjtuc","tpid":"T"},
{"from":"mjec","fpid":"B-s-19949","to":"xhtttnsdkrmzpod","tpid":"T"},
{"from":"mjec","fpid":"B-s-19949","to":"ooezdivexhsrpguriuki","tpid":"T"},
{"from":"ooezdivexhsrpguriuki","fpid":"B","to":"sfjnanljslirukwmevb","tpid":"T"},
{"from":"sfjnanljslirukwmevb","fpid":"B","to":"jqxjtuc","tpid":"T"},
{"from":"jqxvauyertbvret","fpid":"B","to":"joyqwwzdnrzihdbmpcgzoq","tpid":"T"},
{"from":"ajipqcxmdzlcjrcnvou","fpid":"B-strDM","to":"ulyxsahreacdhwllzwfeb","tpid":"T"},
{"from":"rqcgfmmktvimtahqszfeghjbpaxx","fpid":"B","to":"joyqwwzdnrzihdbmpcgzoq","tpid":"T"},
{"from":"boiczzipkpegrmcocrcxdqzelhyhf","fpid":"B","to":"joyqwwzdnrzihdbmpcgzoq","tpid":"T"},
{"from":"wglmqhglbmb","fpid":"B-strRetailDm","to":"kphjyebilnzdjlnkltjnxamhzual","tpid":"T"},
{"from":"tmsbkgstkhlywga","fpid":"B-getAldDecision","to":"kphjyebilnzdjlnkltjnxamhzual","tpid":"T"},
{"from":"pshcahzogdgeubxiiot","fpid":"B","to":"kphjyebilnzdjlnkltjnxamhzual","tpid":"T"},
{"from":"hiwhmkscwbengwntw","fpid":"B","to":"jqxjtuc","tpid":"T"}
]}    
  </textarea>
  <button onclick="loadJson()">Load JSON</button>

  <!-- <svg id="mySvg" ...>...</svg> -->

  <script src="https://unpkg.com/gojs"></script>
  <script id="code">

// try to create a model from some SVG rendered by Diagram.makeSvg
function loadFromSvg() {
  const svg = document.getElementById("mySvg");

  const parts = svg.querySelectorAll('g[data-class-name]');
  const nda = [];
  const lda = [];
  let currentGroup = null;
  parts.forEach(p => {
    const d = {};
    const cls = p.getAttribute("data-class-name");
    if (p.getAttribute("data-key")) d.key = p.getAttribute("data-key");
    if (cls === "Group") {
      d.isGroup = true;
      currentGroup = d;
    }
    if (cls === "Link") {
      if (p.getAttribute("data-from-key")) d.from = p.getAttribute("data-from-key");
      if (p.getAttribute("data-from-port-id")) d.fpid = p.getAttribute("data-from-port-id");
      if (p.getAttribute("data-to-key")) d.to = p.getAttribute("data-to-key");
      if (p.getAttribute("data-to-port-id")) d.tpid = p.getAttribute("data-to-port-id");
      lda.push(d);
    } else {
      //??? not handling ports: "[data-port-id]"
      if (p.getAttribute("data-group")) {
        d.group = p.getAttribute("data-group");
      } else {  //??? guess about membership
        if (currentGroup && cls !== "Group") d.group = currentGroup.key;
      }
      nda.push(d);
    }
  });

  myDiagram.model = new go.GraphLinksModel({
    linkFromPortIdProperty: "fpid",
    linkToPortIdProperty: "tpid",
    nodeDataArray: nda,
    linkDataArray: lda
  });
}


const myDiagram =
  new go.Diagram("myDiagramDiv", {
      layout: new go.LayeredDigraphLayout({
        isOngoing: false,
        isInitial: false,
        direction: 90,
        setsPortSpots: false
      }),
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

myDiagram.nodeTemplate =
  new go.Node("Auto")
    .add(
      new go.Shape({ fill: "white" }),
      new go.TextBlock({ margin: 8 })
        .bind("text", "key")
    );

myDiagram.groupTemplate =
  new go.Group("Auto", {
      avoidable: false,
      layout: new go.LayeredDigraphLayout({
        //columnSpacing: 40,
        //layerSpacing: 80,
        //layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
        //aggressiveOption: go.LayeredDigraphLayout.AggressiveMore,
        //packOption: go.LayeredDigraphLayout.PackAll,
        isOngoing: false,
        isInitial: false,
        isRealtime: false,
        direction: 90,
        setsPortSpots: false
      })
    })
    .add(
      new go.Shape({ fill: "whitesmoke" }),
      new go.Panel("Vertical", { margin: 5 })
        .add(
          new go.Panel("Horizontal")
            .add(
              go.GraphObject.build("SubGraphExpanderButton"),
              new go.TextBlock()
                .bind("text", "key")
            ),
          new go.Placeholder({ padding: 10 })
        )
    );

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

function loadJson() {
  myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
  myDiagram.layoutDiagram(true);  // force complete layout
}

function collapse() {
  myDiagram.commit(diag => {
    diag.findTopLevelGroups().each(g => g.isSubGraphExpanded = false);
  });
}

function expand() {
  myDiagram.commit(diag => {
    diag.findTopLevelGroups().each(g => g.isSubGraphExpanded = true);
  });
}

loadJson();
  </script>
</body>
</html>