Preventing Node positions from changing when expanding a tree

I’m attempting to create an application which allows us to control where nodes are placed on the diagram, think electrical schematic. Drag and placing the nodes is simple. Additional nodes are added to the diagram by expanding nodes currenty placed within the diagram. The issue we’re encountering is that expanding the nodes, thus adding more nodes, rerenders/redraws the entire diagram and thus repositioning the nodes which have already been placed. I don’t want these nodes to move. (We are using LayerDigraphLayout). How can I prevent the diagram from rerendering/redrawing whenever I expanded a node?

The simple answer is to set Layout.isOngoing to false on your Diagram.layout. But there might be other times in which you want automatic layouts to happen, so you might want to read GoJS Layouts -- Northwoods Software

I expanded a tree node and child nodes appeared as expected. Next, I set the Layout.isOngoing to false. Finally, I expanded one of the child nodes expecting nodes to appear and none of the previous nodes to be repositioned. Instead, nothing happened. What am I missing?

Ah, that’s different – you said that “Additional nodes are added to the diagram by expanding nodes currently placed within the diagram.” I assumed that you would be adding the nodes explicitly in the “Button” click event handler and would be positioning those new nodes programmatically.

A layout will (normally) automatically happen when nodes or links are added or removed or become visible or hidden. That can happen due to expanding or collapsing a tree node. If you disable the layout invalidation, then the layout won’t be performed, so the newly visible tree descendants don’t get laid out nicely.

By the way, using “TreeExpanderButton” might not work the way that you want if there are multiple “parents” for a node. I mention that because you mentioned using LayeredDigraphLayout. But it is possible to implement different tree expansion and collapsing policies. For example: Different Criteria for Hiding "Children" of Collapsed Nodes

I’ll take a look at the customExpandCollapse.html. I read Layout invalidation documentation.

" Finally, you can set Part.isLayoutPositioned to false in order for the Layout to completely ignore that Part. But you will have to make sure that that Part does have a real Part.location, since no layout will set it for you. Without a real location the part will not be visible anywhere in the diagram. Furthermore if a node has isLayoutPositioned set to false, Layouts will not only ignore that node but also all links connecting with that node. Because the node will not be moved by the layout, it might overlap with the laid-out nodes and links".

I was expecting this to work. I added the following binding to my node template, new go.Binding(“location”).makeTwoWay() to make sure the Part has a location and then for each expanded Node I set isLayoutPositioned=false. When I expanded the next node I was expecting none of the previous nodes to be repositioned. Unfortunately, all diagram nodes got repositioned. Could this be due to the TreeExpanderButton?

Did you set/bind that for the links too?

Yes. In the link template: new go.Binding(“points”).makeTwoWay(),

No, I meant Part.isLayoutPositioned. Otherwise the showing or hiding of the Link is going to invalidate the responsible Layout…

Here’s the model after I expand the root node.
{ “class”: “GraphLinksModel”,
“nodeDataArray”: [
{“key”:0, “text”:“0”, “type”:“S2”, “fill”:"#aea7d4", “isLayoutPositioned”:false, “location”:{“class”:“go.Point”, “x”:121.5, “y”:23}, “everExpanded”:true},
{“key”:1879, “text”:“0”, “type”:“S3”, “fill”:"#ccd7aa", “isLayoutPositioned”:false, “location”:{“class”:“go.Point”, “x”:21.5, “y”:119}},
{“key”:4289, “text”:“1”, “type”:“M5”, “fill”:"#b9fafb", “isLayoutPositioned”:false, “location”:{“class”:“go.Point”, “x”:121.5, “y”:119}},
{“key”:4324, “text”:“2”, “type”:“S3”, “fill”:"#8f85df", “isLayoutPositioned”:false, “location”:{“class”:“go.Point”, “x”:221.5, “y”:119}}
],
“linkDataArray”: [
{“from”:0, “to”:1879, “color”:“blue”, “isLayoutPositioned”:false, “points”:[121.5,46,121.5,56,121.5,67,21.5,67,21.5,78,21.5,88]},
{“from”:0, “to”:4289, “dash”:[ 3,2 ], “color”:“blue”, “isLayoutPositioned”:false, “points”:[121.5,46,121.5,56,121.5,66,121.5,66,121.5,76,121.5,86]},
{“from”:0, “to”:4324, “color”:“blue”, “isLayoutPositioned”:false, “points”:[121.5,46,121.5,56,121.5,67,221.5,67,221.5,78,221.5,88]}
]}

If I expand a child node then the entire layout of nodes is repositioned.

If you have Bindings for both Node.isLayoutPositioned and Link.isLayoutPositioned, it appears that none of your nodes will ever be laid out automatically – they all depend on the Binding on Node.location to get a real location. And only an explicit call to perform a layout will cause the Diagram.layout to have any effect.

If that is what you want, then you should be all set. So what is the problem?

Hi Walter, First, thank you for help.
I’m looking to prevent nodes previously positioned by the user not to be repositioned when attempting to expand another node.
What I’m attempting to do is the following is Step 1: Have a user first expand nodes and then position these nodes within the diagram. From this point, I want these nodes to remain in the location I’ve placed them in. Step 2: Expand another node to add more nodes to the diagram. All the nodes in the previous diagram have now be repositoned. I do not want this to occur.

Step 1:
image

Step 2:
image

Here’s the code I’m using

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.LayeredDigraphLayout, { direction: 90, columnSpacing: 20, layerSpacing: 20 }),
          "ModelChanged": function(e) {
            if (e.isTransactionFinished) {
              document.getElementById("savedModel").textContent = e.model.toJson();
            }
          }
        });

    // 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>

OK, I don’t think you want to bind Part.isLayoutPositioned after all.

If you were using TreeLayout you could set TreeLayout.arrangement to go.TreeLayout.ArrangementFixedRoots.

If you really want to use LayeredDigraphLayout as the Diagram.layout, you might want to use a custom “Button” that:

  1. collects the Nodes and Links that you do want to be repositioned,
  2. calls myDiagram.layout.doLayout(collection)
  3. calls myDiagram.moveParts(collection, offset, false) where you compute the offset you need to put all of the nodes and links where you want them to be. You can call myDiagram.computePartsBounds(collection) to figure out what area is occupied by those parts. And you know the button.part.actualBounds.

I tried option #1 using a TreeLayout. The top level/root node now stays in place but, the other nodes continue to get repositioned. Is there a way to designate a node as a root node to prevent it from being repositioned.

Step #1: (I repositioned the node circled in red. I do not want this node to be repositioned when I
expand another node).

image

Step #2 (Expanded another node and node circleed in red was repositioned. I do not want this node to get repositioned. Note: Root did not move.)image

Thank you for starting to describe your requirements more carefully, so that I can understand the distinctions you are making.

It sounds like you want to implement a “SelectionMoved” DiagramEvent listener that sets Part.isLayoutPositioned to true.
https://gojs.net/latest/intro/events.html#SelectionMoved

$(go.Diagram, . . .,
  { . . .,
    "SelectionMoved" function(e) {
      e.subject.each(function(p) {
        if (p instanceof go.Node) p.isLayoutPositioned = false;
      });
    }
  })

If you want that information saved in your model, add a TwoWay Binding to your node template(s) for that property.

Walter, Much better, almost there.

Step 1:

Step 2: Expanded a node and node did not move. Awesome !!

Step 3: I expanded the repositioned node:

Is there a way to force the node expand downward and to keep the tree structure?

Looks like you need to do this after all:

Actually, you won’t have to move the nodes if you are using a TreeLayout with go.TreeLayout.ArrangementFixedRoots and the nodes and links are all descendants of that node. BTW, use Node | GoJS API for step 1.

Hey Walter, can you elaborate a little more on this possible solution. I’m ok collecting nodes and links to be repositioned but confused about calling the doLayout method and what it actually does. I think the go.TreeLayout.ArrangementFixedRoots pertains only to the rootNode. Also, do I still set the isLayoutPositioned any node which is manually repositioned? Sorry for the confusion.

Yes, if you want to remember that a user-moved node shouldn’t be moved by automatic layouts, then you still want that “SelectionMoved” listener.

You can see how “Button” and “TreeExpanderButton” are defined at extensions/Buttons.js, so you can either copy and adapt that definition for your own button definition or modify the standard “Button” in your node template.

click: function(e, button) {
  var node = button.part;
  node.diagram.startTransaction("expanding or collapsing tree");
  if (node.isTreeExpanded) {
    node.diagram.commandHandler.collapseTree(node);
  } else {
    node.diagram.commandHandler.expandTree(node);
    var coll = new go.Set();
    coll.add(node);
    coll.addAll(node.findTreeParts());
    node.diagram.layout.doLayout(coll);
  }
  node.diagram.commitTransaction("expanded or collapsed tree");
}

I haven’t tried this code, so please pardon any goofs.

Hmmm, you probably want to set TreeLayout.isOngoing to false.