Nodes inside groups not visible in nested treemap

We have created a nested treemap using TreeMap layout.

Hierarchy in the treemap is up to three levels - L1 and L2 are groups and L3 are the nodes.

We also have a toggle button to expand and collapse all the L1s and L2s of the treemap so that we can show and hide all the L2 and L3 entities inside all the L1s/L2s on a click.

Collapsed state -

Expanded state -

We are running into an issue where the nodes inside the groups (L3s inside L1/L2) do not appear when we expand all the entities using the toggle for the first time.

Issue -

You can see that the L3s are not present, and white space appears in the above image. Need your help to resolve this issue so that the diagram loads as expected on the first toggle.

What’s weird is that the missing nodes appear immediately if I select any L1/L2/L3 entity or just zoom in or out.

I just modified the TreeMap sample so that there’s a “maxDepth” input and a button to toggle whether the group at that level of the group hierarchy are forced collapsed or expanded. I think things are working correctly. How is your app different from this code?

<!DOCTYPE html>
<head><body>
<script src="https://unpkg.com/gojs"></script>
<script src="https://unpkg.com/gojs/extensions/TreeMapLayout.js"></script>

<script id="code">
  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;  // for conciseness in defining templates

    myDiagram =
      new go.Diagram("myDiagramDiv",  // must be the ID or reference to div
        {
          initialAutoScale: go.Diagram.Uniform,
          "animationManager.isEnabled": false,
          layout: $(TreeMapLayout, { isTopLevelHorizontal: false }),
          allowMove: false, allowCopy: false, allowDelete: false
        });

    // change selection behavior to cycle up the chain of containing Groups
    myDiagram.toolManager.clickSelectingTool.standardMouseSelect = function () {  // method override requires function, not =>
      var diagram = this.diagram;
      if (diagram === null || !diagram.allowSelect) return;
      var e = diagram.lastInput;
      if (!(e.control || e.meta) && !e.shift) {
        var part = diagram.findPartAt(e.documentPoint, false);
        if (part !== null) {
          var firstselected = null;  // is this or any containing Group selected?
          var node = part;
          while (node !== null) {
            if (node.isSelected) {
              firstselected = node;
              break;
            } else {
              node = node.containingGroup;
            }
          }
          if (firstselected !== null) {  // deselect this and select its containing Group
            firstselected.isSelected = false;
            var group = firstselected.containingGroup;
            if (group !== null) group.isSelected = true;
            return;
          }
        }
      }
      go.ClickSelectingTool.prototype.standardMouseSelect.call(this);
    };

    // Nodes and Groups are the absolute minimum template: no elements at all!
    myDiagram.nodeTemplate =
      $(go.Node,
        { background: "rgba(99,99,99,0.2)" },
        new go.Binding("background", "fill"),
        {
          toolTip: $("ToolTip",
            $(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
          )
        }
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        { layout: null },
        { background: "rgba(99,99,99,0.2)" },
        new go.Binding("background", "fill"),
        {
          toolTip: $("ToolTip",
            $(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
          )
        }
      );

    function tooltipString(part) {
      if (part instanceof go.Adornment) part = part.adornedPart;
      var msg = createPath(part);
      msg += "\nsize: " + part.data.size;
      if (part instanceof go.Group) {
        var group = part;
        msg += "\n# children: " + group.memberParts.count;
        msg += "\nsubtotal size: " + group.data.total;
      }
      return msg;
    }

    function createPath(part) {
      var parent = part.containingGroup;
      return (parent !== null ? createPath(parent) + "/" : "") + part.data.text;
    }

    // generate a tree with the default values
    rebuildGraph();
  }

  function rebuildGraph() {
    var minNodes = document.getElementById("minNodes").value;
    minNodes = parseInt(minNodes, 10);

    var maxNodes = document.getElementById("maxNodes").value;
    maxNodes = parseInt(maxNodes, 10);

    var minChil = document.getElementById("minChil").value;
    minChil = parseInt(minChil, 10);

    var maxChil = document.getElementById("maxChil").value;
    maxChil = parseInt(maxChil, 10);

    // create and assign a new model
    var model = new go.GraphLinksModel();
    model.nodeGroupKeyProperty = "parent";
    model.nodeDataArray = generateNodeData(minNodes, maxNodes, minChil, maxChil);
    myDiagram.model = model;
  }

  // Creates a random number (between MIN and MAX) of randomly colored nodes.
  function generateNodeData(minNodes, maxNodes, minChil, maxChil) {
    var nodeArray = [];
    if (minNodes === undefined || isNaN(minNodes) || minNodes < 1) minNodes = 1;
    if (maxNodes === undefined || isNaN(maxNodes) || maxNodes < minNodes) maxNodes = minNodes;

    // Create a bunch of node data
    var numNodes = Math.floor(Math.random() * (maxNodes - minNodes + 1)) + minNodes;
    for (var i = 0; i < numNodes; i++) {
      var size = Math.random() * Math.random() * 10000;  // non-uniform distribution
      nodeArray.push({
        key: i,  // the unique identifier
        isGroup: false,  // many of these turn into groups, by code below
        parent: undefined,  // is set by code below that assigns children
        text: i.toString(),  // some text to be shown by the node template
        fill: go.Brush.randomColor(),  // a color to be shown by the node template
        size: size,
        total: -1  // use a negative value to indicate that the total for the group has not been computed
      });
    }

    // Takes the random collection of node data and creates a random tree with them.
    // Respects the minimum and maximum number of links from each node.
    // The minimum can be disregarded if we run out of nodes to link to.
    if (nodeArray.length > 1) {
      if (minChil === undefined || isNaN(minChil) || minChil < 0) minChil = 0;
      if (maxChil === undefined || isNaN(maxChil) || maxChil < minChil) maxChil = minChil;

      // keep the Set of node data that do not yet have a parent
      var available = new go.Set();
      available.addAll(nodeArray);
      for (var i = 0; i < nodeArray.length; i++) {
        var parent = nodeArray[i];
        available.remove(parent);

        // assign some number of node data as children of this parent node data
        var children = Math.floor(Math.random() * (maxChil - minChil + 1)) + minChil;
        for (var j = 0; j < children; j++) {
          var child = available.first();
          if (child === null) break;  // oops, ran out already
          available.remove(child);
          // have the child node data refer to the parent node data by its key
          child.parent = parent.key;
          if (!parent.isGroup) {  // make sure PARENT is a group
            parent.isGroup = true;
          }
          var par = parent;
          while (par !== null) {
            par.total += child.total;  // sum up sizes of all children
            if (par.parent !== undefined) {
              par = nodeArray[par.parent];
            } else {
              break;
            }
          }
        }
      }
    }
    return nodeArray;
  }

  var myTreeDepthLimited = false;
  function toggleDepth() {
    myTreeDepthLimited = !myTreeDepthLimited;
    const maxDepth = parseInt(document.getElementById("maxDepth").value);
    myDiagram.commit(diag => {
      myDiagram.nodes.each(grp => {
        if (!(grp instanceof go.Group)) return;
        const lev = grp.findSubGraphLevel();  // zero-based
        if (lev !== maxDepth-1) return;
        grp.isSubGraphExpanded = myTreeDepthLimited ? false : true;
      });
    });
  }
  window.addEventListener('DOMContentLoaded', init);
</script>

<div id="sample">
  <div style="margin-bottom: 5px; padding: 5px; background-color: aliceblue">
    <span style="display: inline-block; vertical-align: top; padding: 5px">
      <b>New Tree</b><br />
      MinNodes: <input type="number" width="2" id="minNodes" value="300" /><br />
      MaxNodes: <input type="number" width="2" id="maxNodes" value="500" /><br />
      MinChildren: <input type="number" width="2" id="minChil" value="2" /><br />
      MaxChildren: <input type="number" width="2" id="maxChil" value="5" /><br />
      MaxDepth: <input type="number" width="2" id="maxDepth" value="3" /><br />
      <button type="button" onclick="rebuildGraph()">Generate Tree</button>
      <button type="button" onclick="toggleDepth()">Toggle Tree Depth</button>
    </span>
  </div>
  <div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 500px"></div>
</div>
</body></head>

We have two templates for the group. One is associated with isSubGraphExpanded: true, and other for isSubGraphExpanded: false. When we expand all nodes, we switch the node category as follows:

this.diagram.model.commit((model: go.Model) => {
  model.nodeDataArray.forEach((node: TranslatedTreeViewData) => {
    node.isGroup && model.setCategoryForNodeData(node, "expanded");
  }),
    "expandAll";
});

Please let me know how I can resolve the issue.

That should be OK. I’ve modified the sample I gave you above with one that uses two different group templates, one with Group.isSubGraphExpanded true and the other false. The toggle button code now just changes the Part.category instead of the Group.isSubGraphExpanded property.

<!DOCTYPE html>
<head><body>
<script src="https://unpkg.com/gojs"></script>
<script src="https://unpkg.com/gojs/extensions/TreeMapLayout.js"></script>

<script id="code">
  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;  // for conciseness in defining templates

    myDiagram =
      new go.Diagram("myDiagramDiv",  // must be the ID or reference to div
        {
          initialAutoScale: go.Diagram.Uniform,
          "animationManager.isEnabled": false,
          layout: $(TreeMapLayout, { isTopLevelHorizontal: false }),
          allowMove: false, allowCopy: false, allowDelete: false
        });

    // change selection behavior to cycle up the chain of containing Groups
    myDiagram.toolManager.clickSelectingTool.standardMouseSelect = function () {  // method override requires function, not =>
      var diagram = this.diagram;
      if (diagram === null || !diagram.allowSelect) return;
      var e = diagram.lastInput;
      if (!(e.control || e.meta) && !e.shift) {
        var part = diagram.findPartAt(e.documentPoint, false);
        if (part !== null) {
          var firstselected = null;  // is this or any containing Group selected?
          var node = part;
          while (node !== null) {
            if (node.isSelected) {
              firstselected = node;
              break;
            } else {
              node = node.containingGroup;
            }
          }
          if (firstselected !== null) {  // deselect this and select its containing Group
            firstselected.isSelected = false;
            var group = firstselected.containingGroup;
            if (group !== null) group.isSelected = true;
            return;
          }
        }
      }
      go.ClickSelectingTool.prototype.standardMouseSelect.call(this);
    };

    // Nodes and Groups are the absolute minimum template: no elements at all!
    myDiagram.nodeTemplate =
      $(go.Node,
        { background: "rgba(99,99,99,0.2)" },
        new go.Binding("background", "fill"),
        {
          toolTip: $("ToolTip",
            $(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
          )
        }
      );

    myDiagram.groupTemplate =
      $(go.Group, "Auto",
        { layout: null, isSubGraphExpanded: true },
        { background: "rgba(99,99,99,0.2)" },
        new go.Binding("background", "fill"),
        {
          toolTip: $("ToolTip",
            $(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
          )
        }
      );

    myDiagram.groupTemplateMap.add("collapsed",
      $(go.Group, "Auto",
        { layout: null, isSubGraphExpanded: false },
        { background: "rgba(99,99,99,0.2)" },
        new go.Binding("background", "fill"),
        {
          toolTip: $("ToolTip",
            $(go.TextBlock, new go.Binding("text", "", tooltipString).ofObject())
          )
        }
      ));

    function tooltipString(part) {
      if (part instanceof go.Adornment) part = part.adornedPart;
      var msg = createPath(part);
      msg += "\nsize: " + part.data.size;
      if (part instanceof go.Group) {
        var group = part;
        msg += "\n# children: " + group.memberParts.count;
        msg += "\nsubtotal size: " + group.data.total;
      }
      return msg;
    }

    function createPath(part) {
      var parent = part.containingGroup;
      return (parent !== null ? createPath(parent) + "/" : "") + part.data.text;
    }

    // generate a tree with the default values
    rebuildGraph();
  }

  function rebuildGraph() {
    var minNodes = document.getElementById("minNodes").value;
    minNodes = parseInt(minNodes, 10);

    var maxNodes = document.getElementById("maxNodes").value;
    maxNodes = parseInt(maxNodes, 10);

    var minChil = document.getElementById("minChil").value;
    minChil = parseInt(minChil, 10);

    var maxChil = document.getElementById("maxChil").value;
    maxChil = parseInt(maxChil, 10);

    // create and assign a new model
    var model = new go.GraphLinksModel();
    model.nodeGroupKeyProperty = "parent";
    model.nodeDataArray = generateNodeData(minNodes, maxNodes, minChil, maxChil);
    myDiagram.model = model;
    myTreeDepthLimited = false;
  }

  // Creates a random number (between MIN and MAX) of randomly colored nodes.
  function generateNodeData(minNodes, maxNodes, minChil, maxChil) {
    var nodeArray = [];
    if (minNodes === undefined || isNaN(minNodes) || minNodes < 1) minNodes = 1;
    if (maxNodes === undefined || isNaN(maxNodes) || maxNodes < minNodes) maxNodes = minNodes;

    // Create a bunch of node data
    var numNodes = Math.floor(Math.random() * (maxNodes - minNodes + 1)) + minNodes;
    for (var i = 0; i < numNodes; i++) {
      var size = Math.random() * Math.random() * 10000;  // non-uniform distribution
      nodeArray.push({
        key: i,  // the unique identifier
        isGroup: false,  // many of these turn into groups, by code below
        parent: undefined,  // is set by code below that assigns children
        text: i.toString(),  // some text to be shown by the node template
        fill: go.Brush.randomColor(),  // a color to be shown by the node template
        size: size,
        total: -1  // use a negative value to indicate that the total for the group has not been computed
      });
    }

    // Takes the random collection of node data and creates a random tree with them.
    // Respects the minimum and maximum number of links from each node.
    // The minimum can be disregarded if we run out of nodes to link to.
    if (nodeArray.length > 1) {
      if (minChil === undefined || isNaN(minChil) || minChil < 0) minChil = 0;
      if (maxChil === undefined || isNaN(maxChil) || maxChil < minChil) maxChil = minChil;

      // keep the Set of node data that do not yet have a parent
      var available = new go.Set();
      available.addAll(nodeArray);
      for (var i = 0; i < nodeArray.length; i++) {
        var parent = nodeArray[i];
        available.remove(parent);

        // assign some number of node data as children of this parent node data
        var children = Math.floor(Math.random() * (maxChil - minChil + 1)) + minChil;
        for (var j = 0; j < children; j++) {
          var child = available.first();
          if (child === null) break;  // oops, ran out already
          available.remove(child);
          // have the child node data refer to the parent node data by its key
          child.parent = parent.key;
          if (!parent.isGroup) {  // make sure PARENT is a group
            parent.isGroup = true;
          }
          var par = parent;
          while (par !== null) {
            par.total += child.total;  // sum up sizes of all children
            if (par.parent !== undefined) {
              par = nodeArray[par.parent];
            } else {
              break;
            }
          }
        }
      }
    }
    return nodeArray;
  }

  var myTreeDepthLimited = false;
  function toggleDepth() {
    myTreeDepthLimited = !myTreeDepthLimited;
    const maxDepth = parseInt(document.getElementById("maxDepth").value);
    myDiagram.commit(diag => {
      myDiagram.nodes.each(grp => {
        if (!(grp instanceof go.Group)) return;
        const lev = grp.findSubGraphLevel();  // zero-based
        if (lev !== maxDepth-1) return;
        //grp.isSubGraphExpanded = myTreeDepthLimited ? false : true;
        grp.category = myTreeDepthLimited ? "collapsed" : "";
      });
    });
  }
  window.addEventListener('DOMContentLoaded', init);
</script>

<div id="sample">
  <div style="margin-bottom: 5px; padding: 5px; background-color: aliceblue">
    <span style="display: inline-block; vertical-align: top; padding: 5px">
      <b>New Tree</b><br />
      MinNodes: <input type="number" width="2" id="minNodes" value="300" /><br />
      MaxNodes: <input type="number" width="2" id="maxNodes" value="500" /><br />
      MinChildren: <input type="number" width="2" id="minChil" value="2" /><br />
      MaxChildren: <input type="number" width="2" id="maxChil" value="5" /><br />
      MaxDepth: <input type="number" width="2" id="maxDepth" value="3" /><br />
      <button type="button" onclick="rebuildGraph()">Generate Tree</button>
      <button type="button" onclick="toggleDepth()">Toggle Tree Depth</button>
    </span>
  </div>
  <div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 500px"></div>
</div>
</body></head>