Expanding Node, in a tree layout, is expanding in the wrong direction

I’m creating an application which will allow a user to first move and position a node. Once the node is placed, the user needs to expand this node. I’m having an issue that when the node is expanded, in a tree layout, the node expands in the wrong direction.

I’m using a tree layout and the layout direction is set to 90 (Top-Down). When I move the node I set the isLayoutPositioned=false to keep the node in place. Unfortunately, the node expands upward instead of downward.

Initial View after node was moved.

View after the node was expanded. (Expanded upward)

Code:

POC
<script src="../release/go.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 =
      $(go.Diagram, "myDiagramDiv",  // must be the ID or reference to div
        {
          initialAutoScale: go.Diagram.UniformToFill,
          initialContentAlignment: go.Spot.TopCenter,
          layout: $(go.TreeLayout, { angle: 90 }),
          "ModelChanged": function(e) {
            if (e.isTransactionFinished) {
              document.getElementById("savedModel").textContent = e.model.toJson();
            }
          },
          "SelectionMoved": function(e) {
            console.log('moved');
            e.subject.each(function(p) {
              if (p instanceof go.Node) {
                p.isLayoutPositioned = false;
              }
            });
          }
        });

    // define the Node template
    myDiagram.nodeTemplate =
      $(go.Node, "Vertical",
        { locationSpot: go.Spot.Center,
          isTreeExpanded: false,
          isTreeLeaf: false },
        new go.Binding("location").makeTwoWay(),
        $(go.Panel, "Auto", { name: "BODY" },
          $(go.Shape, "RoundedRectangle",
            new go.Binding("fill"),
            new go.Binding("stroke")),
          $(go.TextBlock,
            { font: "bold 12pt Arial, sans-serif"}),
            new go.Binding("text")),
        $("TreeExpanderButton",
          {
            name: 'TREEBUTTON',
            width: 20, height: 20,
            alignment: go.Spot.TopRight,
            alignmentFocus: go.Spot.Center,
            // customize the expander behavior to
            // create children if the node has never been expanded
            click: function(e, obj) {  // OBJ is the Button
              obj.visible = false;
              var node = obj.part;  // get the Node containing this Button
              if (node === null) return;
              e.handled = true;
              expandNode(node);
            }
          }
        )  // end TreeExpanderButton
      );

    // define the Link template to be minimal
    myDiagram.linkTemplate =
      $(go.Link,
        { selectable: false, routing: go.Link.AvoidsNodes, corner: 10},
        new go.Binding("points").makeTwoWay(),
        $(go.Shape,
          { strokeWidth: 1, stroke: "blue" },
          new go.Binding("stroke", "color"),
          new go.Binding("strokeWidth", "width"),
          new go.Binding("strokeDashArray", "dash")
        ),
      );
    let nodeArray = [];
    nodeArray.push({
      key: 1,
      text: 'Start',
      fill: go.Brush.randomColor(),
      isLayoutPositioned: false
    });
    myDiagram.model.nodeDataArray = nodeArray;
  }

  function expandNode(node) {
    var diagram = node.diagram;

    var data = node.data;
    if (!data.everExpanded) {
      // only create children once per node
      diagram.model.setDataProperty(data, "everExpanded", true);

      var numchildren = Math.floor(Math.random() * 4) + 2;
      for (let i = 0; i < numchildren; i++) {
        let newNode = {
          key: Math.floor((Math.random() * 10000) + 1),
          text: i.toString(),
          fill: go.Brush.randomColor(),
          isLayoutPositioned: false,
        };
        diagram.model.addNodeData(newNode);
        diagram.model.addLinkData({from: node.data.key, to: newNode.key, color: 'blue',  isLayoutPositioned: false});
      }
    }
    // this behavior is generic for most expand/collapse tree buttons:
    if (node.isTreeExpanded) {
      diagram.commandHandler.collapseTree(node);
    } else {
      diagram.commandHandler.expandTree(node);

    }
    diagram.commitTransaction("CollapseExpandTree");
  }

</script>
<textarea id="savedModel" style="width:100%;height:250px"></textarea>

But the tree is laid out growing downwards, isn’t it? That’s what you specified when you set TreeLayout.angle to 90. Since you set Part.isLayoutPositioned to false on that initial node, it no longer has any bearing on the results of the layout on the rest of the nodes and links. It remains where it was and the tree is laid out starting at a y position of 0.

Maybe you want to keep the root node where it is, but you want it to participate in the layout. You can get that for root nodes by setting TreeLayout.arrangement to go.TreeLayout.ArrangementFixedRoots. This is instead of setting or binding Part.isLayoutPositioned.

And perhaps you want expanding a node to only lay out the subtree starting at that node, pretending that node is a root node? You can do that by customizing the behavior of the expand button.

I’m not sure exactly what behavior you want, but maybe something like:

  $("TreeExpanderButton",
    {
      click: function(e, button) {
        e.diagram.commit(function(d) {
          var node = button.part;
          node.isTreeExpanded = !node.isTreeExpanded;
          if (node.isTreeExpanded) {
            // assumes TreeLayout.arrangement is set to TreeLayout.ArrangementFixedRoots
            d.layout.doLayout(node.findTreeParts());
          }
        })
      }
    })

If I first expand the tree and move a child node the same situtation occurs:

Initial View

Updated View

Is there a way to set the node that just I moved to a root node before expanding it in order to guarantee the node expands downward?

Doesn’t my code do that for you? It did for me, although perhaps you have a different configuration or different expectations.

First I set Diagram.layout to:

layout: $(go.TreeLayout,
          { isOngoing: false,
            arrangement: go.TreeLayout.ArrangementFixedRoots,
            ... })

Second, in the node template I included the code from my previous post for the custom “TreeExpanderButton” behavior.

Now collapsing a node causes its subtree to disappear but otherwise no nodes move. And expanding causes its subtree to appear but laid out in a nice tree structure relative to the expanded node without moving any of the previously visible nodes, including the expanded one.

Good Morning Walter,

Something is missing, just can’t figure it out.

Initial View

Expanded Node View

Nodes were layered on top of each other.

Latest code with your recommended changes:

POC
<script src="../release/go.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 =
      $(go.Diagram, "myDiagramDiv",  // must be the ID or reference to div
        {
          initialAutoScale: go.Diagram.UniformToFill,
          initialContentAlignment: go.Spot.TopCenter,
          layout: $(go.TreeLayout, {
            angle: 90,
            isOngoing: false,
            arrangement: go.TreeLayout.ArrangementFixedRoots
          }),
          "ModelChanged": function(e) {
            if (e.isTransactionFinished) {
              document.getElementById("savedModel").textContent = e.model.toJson();
            }
          },
          "SelectionMoved": function(e) {
            console.log('moved');
            e.subject.each(function(p) {
              if (p instanceof go.Node) {
                p.isLayoutPositioned = false;
              }
            });
          }
        });

    // define the Node template
    myDiagram.nodeTemplate =
      $(go.Node, "Vertical",
        { locationSpot: go.Spot.Center,
          isTreeExpanded: false,
          isTreeLeaf: false },
        new go.Binding("location").makeTwoWay(),
        $(go.Panel, "Auto", { name: "BODY" },
          $(go.Shape, "RoundedRectangle",
            new go.Binding("fill"),
            new go.Binding("stroke")),
          $(go.TextBlock,
            { font: "bold 12pt Arial, sans-serif"}),
            new go.Binding("text")),
        $("TreeExpanderButton",
          {
            name: 'TREEBUTTON',
            width: 20, height: 20,
            alignment: go.Spot.TopRight,
            alignmentFocus: go.Spot.Center,
            // customize the expander behavior to
            // create children if the node has never been expanded
            click: function(e, button) {
              e.diagram.commit(d => {
                var node = button.part;
                node.isTreeExpanded = !node.isTreeExpanded;
                console.log('isTreeExpanded:' + node.isTreeExpanded);
                if (node.isTreeExpanded) {
                  addNodes(node);
                  // assumes TreeLayout.arrangement is set to TreeLayout.ArrangementFixedRoots
                  d.layout.doLayout(node.findTreeParts());
                }
              });
            }
            //click: function(e, obj) {  // OBJ is the Button
            //  obj.visible = false;
            //  var node = obj.part;  // get the Node containing this Button
            //  if (node === null) return;
            //  e.handled = true;
            //  expandNode(node);
            //}
          }
        )  // end TreeExpanderButton
      );



    // define the Link template to be minimal
    myDiagram.linkTemplate =
      $(go.Link,
        { selectable: false, routing: go.Link.AvoidsNodes, corner: 10},
        new go.Binding("points").makeTwoWay(),
        $(go.Shape,
          { strokeWidth: 1, stroke: "blue" },
          new go.Binding("stroke", "color"),
          new go.Binding("strokeWidth", "width"),
          new go.Binding("strokeDashArray", "dash")
        ),
      );
    let nodeArray = [];
    nodeArray.push({
      key: 1,
      text: 'Start',
      fill: go.Brush.randomColor(),
      //isLayoutPositioned: false
    });
    myDiagram.model.nodeDataArray = nodeArray;
  }

  function addNodes(node) {
    var diagram = node.diagram;

    var numchildren = Math.floor(Math.random() * 4) + 2;
    for (let i = 0; i < numchildren; i++) {
      let newNode = {
        key: Math.floor((Math.random() * 10000) + 1),
        text: i.toString(),
        fill: go.Brush.randomColor(),
        isLayoutPositioned: false,
      };
      diagram.model.addNodeData(newNode);
      diagram.model.addLinkData({from: node.data.key, to: newNode.key, color: 'red'});
    }
  }

  function expandNode(node) {
    var diagram = node.diagram;

    var data = node.data;
    if (!data.everExpanded) {
      // only create children once per node
      diagram.model.setDataProperty(data, "everExpanded", true);
      addNodes(node);
    }
    // this behavior is generic for most expand/collapse tree buttons:
    if (node.isTreeExpanded) {
      diagram.commandHandler.collapseTree(node);
    } else {
      diagram.commandHandler.expandTree(node);
    }
    diagram.commitTransaction("CollapseExpandTree");
  }

</script>
<textarea id="savedModel" style="width:100%;height:250px"></textarea>

It works fine if you remove the setting of Part.isLayoutPositioned as I recommended above.

But you do have a bug with trying to bind the “text” property of a Panel, and that class does not have any such property. You misplaced a parenthesis, terminating the TextBlock early. If you were using go-debug.js you would get a warning about that Binding.

BTW, you might want to consider setting:

          "draggingTool.dragsTree": true,

in your Diagram initialization.