DoubleTree Layout with Groups

Hi,

I’m trying to implement the DoubleTree layout example in my project. It works great for nodes but I am trying to get it working for groups also. I have updated the separatePartsByLayout method below to find all the groups for each side of the tree and adding these to the correct collection, however, the diagram is still rendering the tree ignoring the groups.

separatePartsByLayout(diagram, leftParts, rightParts): void {
    var root = diagram.findNodesByExample({ isRoot: true }).first();
    if (root === null) return;
    var leftGroups = diagram.findNodesByExample({isGroup: true, dir: 'left'});    
    var rightGroups = diagram.findNodesByExample({isGroup: true, dir: 'right'});    
    // the ROOT node is shared by both subtrees!
    leftParts.add(root);
    rightParts.add(root);
    // add the groups to the correct collection
    leftParts.addAll(leftGroups);
    rightParts.addAll(rightGroups);
    // look at all of the immediate children of the ROOT node
    root.findTreeChildrenNodes().each(function (child) {
      // in what direction is this child growing?
      var dir = child.data.dir;
      var coll = (dir === "left") ? leftParts : rightParts;
      // add the whole subtree starting with this child node
      coll.addAll(child.findTreeParts());      
      // and also add the link from the ROOT node to this child node
      coll.add(child.findTreeParentLink());
    });
  }

I don’t suppose you know what I might be doing wrong?

If I apply the same data to a normal tree (without running the DoubleTree code) it shows the nodes and groups correctly. I’ve added some screenshots below. They both have the same nodespacing and leyerspacing (50)

Rendered with single TreeLayout

Rendered with DoubleTree (left nodes only)

Thanks,
Gary

My guess is that because there are no links directly connecting the groups, in your code there appears to be no relationships between the groups. TreeLayout operating on its own is smart enough to recognize these situations (but not necessarily all possibilities), but maybe not with the Nodes and Links that you’ve given.

Have you set Group.layout to null? Although that might not help your code.

Yeah, in the diagram I’m trying to create, the nodes represent the tree, but then the groups represent the containing systems for the nodes in the tree so will have no links to each other.

I have tried setting the Group.layout to null and it doesn’t seem to make a difference.

That’s right – you need to make sure each call to doLayout you include both the nodes and the links that you want it to lay out.

Okay thanks.

Just to clarify, are you saying that doLayout will ignore the groups if they don’t have links to or from them?

If I wanted a treelayout which accounted for the containing groups when laying out the tree would it need to be a custom layout?

Thanks,
Gary

No, TreeLayout already understands what you want to do. That’s why when you have TreeLayout operate on all of the Diagram’s top-level Nodes and Links, it works. Because then (in your example) it sees five nodes connected by four links.

Is that also the case in your code?

I ran a new test with a more complex tree using the standard TreLayout on the whole diagram, and it seems that it doesn’t account for groups either:

So it’s my groups causing the issue. I’m guessing the TreeLayout lays out the tree (7 nodes and 6 links) then just renders the containers around them without allocating space for them in the tree or something?

Maybe I’m just missing some settings.

You haven’t left Group.layout as null, have you? That’s what you would do to pretend that the groups weren’t there at all, which you do not want to do.

Morning Walter,

My bad, that’s exactly what I’d done.

I’ve created a simple app which demo’s my issue (below). I think I’m just misunderstanding how the layouts work.

In the demo my diagram does not have a layout, I then get all the nodes, links and groups from the diagram and add them to a collection, which I pass into the TreeLayout.doLayout() method. In my mind this should be the same as applying TreeLayout to the diagram, but maybe I’m oversimplifying it as this renders the diagram as if the groups are not there (or the group layout is null).

<script>

    var $ = go.GraphObject.make;

    function runLayout(diagram) {

        diagram.startTransaction("Tree Layout");

        var coll = new go.Set(go.Part);

        var layout =
            $(go.TreeLayout,
                {
                    angle: 0,
                    arrangement: go.TreeLayout.ArrangementFixedRoots
                });

        // get the root
        var root = diagram.findNodesByExample({ isRoot: true }).first();
        // get all the groups
        var groups = diagram.findNodesByExample({ isGroup: true });
        // add root to the collection
        coll.add(root);
        // add all the groups to the collection
        coll.addAll(groups);
        // find and add all tree children
        root.findTreeChildrenNodes().each(function (child) {
            coll.addAll(child.findTreeParts());
            coll.add(child.findTreeParentLink());
        });

        // collection should now contain all nodes, links & groups? (it's size is 14)
        layout.doLayout(coll);
        
        diagram.commitTransaction("Tree Layout");
    }

    function init() {

        myDiagram =
            $(go.Diagram, "myDiagramDiv", {
                initialContentAlignment: go.Spot.Center,
                // layout: $(go.TreeLayout, {
                //     angle: 0
                // })
            });

        myDiagram.model =
            $(go.GraphLinksModel,
                {
                    nodeDataArray: [
                        { key: 1, name: "A", group: 6, isRoot: true },
                        { key: 2, name: "B", group: 7 },
                        { key: 3, name: "C", group: 8 },
                        { key: 4, name: "D", group: 9 },
                        { key: 5, name: "E", group: 10 },
                        { key: 6, name: "Group F", isGroup: true },
                        { key: 7, name: "Group G", isGroup: true },
                        { key: 8, name: "Group H", isGroup: true },
                        { key: 9, name: "Group I", isGroup: true },
                        { key: 10, name: "Group J", isGroup: true }
                    ],
                    linkDataArray: [
                        { from: 1, frompid: "Task 1", to: 2 },
                        { from: 1, frompid: "Task 2", to: 3 },
                        { from: 2, frompid: "Task 1", to: 4 },
                        { from: 3, frompid: "Task 1", to: 5 }

                    ]
                }
            );

        runLayout(myDiagram);

    }
</script>
<div id="sample">

    <div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>

</div>

The difference is that setting Diagram.layout will cause all top-level Nodes and Links to be laid out by a TreeLayout, and only top-level nodes and links. You need to do the same when using two separate TreeLayouts and calling doLayout. In other words, do not pass nested Groups to the layout.

Ah right okay.

In my example there are no top level links, the only top level items are groups. So I’m confused as to what I should pass into the layout.

If I pass only top level items, this will just be a collection of groups, which doesn’t produce a tree.

If I pass top level groups, links and connected nodes (ignoring the nested groups in between), it ignores the groups in the tree.

Select one of those links and look at its Part | GoJS API property. All of your Links are top-level Parts. A Link belongs to the Group that is the “least common denominator” container for the LInk.fromNode and Link.toNode.

You know that this works because if you set Diagram.layout to be a TreeLayout, it basically lays everything out in the manner that you want, and that’s because that TreeLayout is operating on all top-level nodes and links. Assuming Group.layout has not been set to null.

Okay, that makes sense. So if I’ve understood correctly, if I pass a collection of top level nodes and links into the TreeLayout.doLayout method, this should output the same as setting Diagram.layout to be a TreeLayout?

If so, does the order of the collection matter? I tested the above theory with the following code, and it didn’t output a tree:

        diagram.startTransaction("Tree Layout");

        var coll = new go.Set(go.Part);

        var layout =
            $(go.TreeLayout,
                {
                    angle: 0
                });
        var coll = new go.Set(go.Part);

        diagram.links.each(function (l){
            if (l.isTopLevel) {
                coll.add(l);
            } 
        });

        diagram.nodes.each(function (n) {
            if (n.isTopLevel) {
                coll.add(n);
            } 
        });

        layout.doLayout(coll);

        diagram.commitTransaction("Tree Layout");

Apologies if I’m missing something obvious.

I just tried your unmodified code in a random diagram that contains groups and links between member nodes (no links directly with groups), and it worked as I would have expected.

Which code are you referring to? The latest snippet I sent?

Yes, the one with a duplicate line:

Also, why does your Diagram not have any templates? It should have worked anyway, but without any of your expected node and link appearances and behaviors. Maybe you haven’t initialized your diagram as you thought you had.

Morning Walter,

I stripped everything out into a simple GoJS app, to rule out any issues with templates and others things. This has no templates, it just created default nodes, links and groups.

I’ve put the same code into my basic application (below) and it still does not work for me. So I’m out of ideas.

<script src="go.js"></script>

<script>
    var $ = go.GraphObject.make;

    var myDiagram;

    function runLayout(diagram) {

        diagram.startTransaction("Tree Layout");

        var coll = new go.Set(go.Part);

        var layout =
            $(go.TreeLayout,
                {
                    angle: 0,
                    nodeSpacing: 50,
                    layerSpacing: 50
                });

        diagram.nodes.each(function (n) {
            if (n.isTopLevel) {
                coll.add(n);
            }
        });

        diagram.links.each(function (l) {
            if (l.isTopLevel) {
                coll.add(l);
            }
        });

        layout.doLayout(coll);

        diagram.commitTransaction("Tree Layout");
    }

    function init() {

        myDiagram =
            $(go.Diagram, "myDiagramDiv", {
                initialContentAlignment: go.Spot.Center
            });

        myDiagram.model =
            $(go.GraphLinksModel,
                {
                    nodeDataArray: [
                        { key: 1, name: "A" },
                        { key: 2, name: "B", group: 7 },
                        { key: 3, name: "C", group: 8 },
                        { key: 4, name: "D", group: 9 },
                        { key: 5, name: "E", group: 10 },
                        { key: 7, name: "Group G", isGroup: true },
                        { key: 8, name: "Group H", isGroup: true },
                        { key: 9, name: "Group I", isGroup: true },
                        { key: 10, name: "Group J", isGroup: true }                            
                    ],
                    linkDataArray: [
                        { from: 1, to: 2 },
                        { from: 1, to: 3 },
                        { from: 2, to: 4 },
                        { from: 3, to: 5 }

                    ]
                }
            );

        runLayout(myDiagram);

    }
</script>
<div id="sample">

    <div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 600px"></div>

</div>

I think there’s a problem with trying to perform the layout before the Groups have all had a chance to perform their own layout. For example, delaying calling runLayout works:

    function init() {
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            initialContentAlignment: go.Spot.Center,
            initialLayoutCompleted: function(e) { runLayout(e.diagram); }
          });
      myDiagram.model =
        $(go.GraphLinksModel,
          { nodeDataArray: [ . . . ], linkDataArray: [ . . . ] });
      //runLayout(myDiagram);
    }

Alternatively, call Diagram.layoutDiagram first:

      myDiagram.layoutDiagram();
      runLayout(myDiagram);

When I tried your code before, the layouts of the Groups had all been performed already.

Morning Walter,

Apologies, I have been away for a week.

I tried your suggestions, and they worked great! I’m going to try and implement this in the Double Tree layout, but it sounds like should work there too.

Thanks a lot for your help.