Custom Layout with two model (Subtree layout in GraphLinksModel)

I have created a diagram by using Gojs Graphlinksmodel. The sample code is available in jsfiddle(Edit fiddle - JSFiddle - Code Playground).
Now I want to add sub tree layout into some of the nodes (not to all nodes).

When I tried to change the model as Tree layout, it is changing the whole diagram structure. If I have these kinds of sub tree layouts for one or two nodes, it should automatically adjust the height of the full diagram. How can I achieve this custom layout (two different layouts in same diagram)?

I don’t understand what results you want. Could you please show a screenshot or sketch of how you want that graph to be laid out?

By the way, you should finish building the model with data before you assign the model to the diagram.

Thanks walter for quick reply. Shared the screenshot for your reference. Let me know if you need any additional information.
Default Rendered output:


Expanded Mode: (Expansion option should available for few nodes.)

Have you seen the SerpentineLayout extension? Serpentine Layout
I think that is what you want to use for the Diagram.layout.

Then for nodes like “Node 2” and “Node 7”, you would implement them as regular Nodes that are in their own Groups. The group template would not have to show anything, but would set the Group.layout to a TreeLayout. Something like:

myDiagram.groupTemplate = 
  $(go.Group,
    {
      layout: $(go.TreeLayout, { ...  })
    },
    $(go.Placeholder));

Where the TreeLayout is configured how you want. You could start with the layout in GoJS Tree View

The nodes would need "TreeExpanderButton"s.

What is crucial, though, is that for example “Node 2” and all of its tree-children are members of a single Group. That way when that tree is collapsed, the group only takes up the area of the root node. When it is partially or fully expanded, the group is as big as it needs to be and it the SerpentineLayout adjusts to compensate.

Thanks for the reply walter.

  1. I tried Serpentine Layout for my requirement. I have added isGraph, graph properties to nodeDataArray. And added groupTemplate to myDiagram as well. But Group layout is not added in the UI.
  2. I have tried your solution (tree layout in groupTemplate). But the nodes under group it’s not aligned correctly. I have added some tree layout properties(angle, spacing etc) as well based on api document. After that also it’s not aligned correctly (Edit fiddle - JSFiddle - Code Playground).
    Note: If we apply tree layout details to myDiagram, I can achieve my sub tree requirement. Same logic is not working in groupTemplate. Refer (GoJS Sample for JSFIDDLE - JSFiddle - Code Playground). Let me know if i missed anything here.

The complexity here comes from trying to treat “Node 2” or “Beta” both as a simple Node and as a Group. In some respects, for example the links to and from “Beta”, you want to treat it just as a simple Node. But we want the layout to treat “Beta” as a Group, so that when it is expanded it occupies the whole area of the Group, not just the “Beta” Node.

Although some other layouts are sophisticated enough to handle groups, SerpentineLayout is not. So I’ve had to modify it to handle what you want. Both the modified sample and the modified SerpentineLayout are appended here. First, the result:

The adapted sample:

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

  <div id="sample">
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:500px; min-width: 200px"></div>
  </div>

  <script id="code">
var $ = go.GraphObject.make;

myDiagram =
  $(go.Diagram, "myDiagramDiv",  // create a Diagram for the DIV HTML element
    {
      layout: $(SerpentineLayout, { isRealtime: false })  // defined in SerpentineLayout.js
    });

myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    $(go.Shape,
      { figure: "RoundedRectangle", fill: "white" },
      new go.Binding("fill", "color")),
    $(go.Panel, "Horizontal",
      $(go.TextBlock,
        { margin: 4 },
        new go.Binding("text")),
      $("TreeExpanderButton")
    )
  );

myDiagram.linkTemplate =
  $(go.Link,
    { routing: go.Link.AvoidsNodes, corner: 5 },
    new go.Binding("isTreeLink", "isTopLevel", t => !t).ofObject(),
    $(go.Shape),
    $(go.Shape, { toArrow: "Standard" }));

myDiagram.groupTemplate = 
  $(go.Group,
    {
      avoidableMargin: new go.Margin(-30, 0, 0, 0),
      layout: $(go.TreeLayout,
        {
          alignment: go.TreeLayout.AlignmentStart,
          layerSpacing: 20,
          layerSpacingParentOverlap: 1,
          nodeIndentPastParent: 1.0,
          nodeIndent: 10,
          nodeSpacing: 10,
          portSpot: new go.Spot(0.0001, 1, 5, 0)
        })
    },
    $(go.Placeholder));

myDiagram.model = new go.GraphLinksModel(
  {
    nodeDataArray: [
      { key: 1, text: "Alpha", color: "coral", group: -1 },
      { key: -1, text: "Alpha Group", isGroup: true },
      { key: 101, text: "Alpha 1", group: -1 },
      { key: 102, text: "Alpha 2", group: -1 },
      { key: 2, text: "Beta", color: "tomato", group: -2 },
      { key: -2, text: "Beta Group", isGroup: true },
      { key: 201, text: "Beta 1", group: -2 },
      { key: 202, text: "Beta 2", group: -2 },
      { key: 3, text: "Gamma", color: "goldenrod" },
      { key: 4, text: "Delta", color: "orange" },
      { key: 5, text: "Epsilon", color: "coral" },
      { key: 6, text: "Zeta", color: "tomato" },
      { key: 7, text: "Eta", color: "goldenrod", group: -7 },
      { key: -7, text: "Eta Group", isGroup: true },
      { key: 701, text: "Eta 1", group: -7 },
      { key: 702, text: "Eta 2", group: -7 },
      { key: 703, text: "Eta 3", group: -7 },
      { key: 704, text: "Eta 4", group: -7 },
      { key: 8, text: "Theta", color: "orange" },
      { key: 9, text: "Iota", color: "coral" },
      { key: 10, text: "Kappa", color: "tomato" },
      { key: 11, text: "Lambda", color: "goldenrod" },
      { key: 12, text: "Mu", color: "orange" },
      { key: 13, text: "Nu", color: "coral" },
      { key: 14, text: "Xi", color: "tomato" },
      { key: 15, text: "Omicron", color: "goldenrod" },
      { key: 16, text: "Pi", color: "orange" },
      { key: 17, text: "Rho", color: "coral" },
      { key: 18, text: "Sigma", color: "tomato" },
      { key: 19, text: "Tau", color: "goldenrod" },
      { key: 20, text: "Upsilon", color: "orange" },
      { key: 21, text: "Phi", color: "coral" },
      { key: 22, text: "Chi", color: "tomato" },
      { key: 23, text: "Psi", color: "goldenrod" },
      { key: 24, text: "Omega", color: "orange", group: -24 },
      { key: -24, text: "Omega Group", isGroup: true },
      { key: 2401, text: "Omega 1", group: -24 },
      { key: 2402, text: "Omega 2", group: -24 },
    ],
    linkDataArray: [
      { from: 1, to: 2 },
      { from: 2, to: 3 },
      { from: 3, to: 4 },
      { from: 4, to: 5 },
      { from: 5, to: 6 },
      { from: 6, to: 7 },
      { from: 7, to: 8 },
      { from: 8, to: 9 },
      { from: 9, to: 10 },
      { from: 10, to: 11 },
      { from: 11, to: 12 },
      { from: 12, to: 13 },
      { from: 13, to: 14 },
      { from: 14, to: 15 },
      { from: 15, to: 16 },
      { from: 16, to: 17 },
      { from: 17, to: 18 },
      { from: 18, to: 19 },
      { from: 19, to: 20 },
      { from: 20, to: 21 },
      { from: 21, to: 22 },
      { from: 22, to: 23 },
      { from: 23, to: 24 },

      { from: 1, to: 101 },
      { from: 1, to: 102 },

      { from: 2, to: 201 },
      { from: 2, to: 202 },
      
      { from: 7, to: 701 },
      { from: 701, to: 702 },
      { from: 701, to: 703 },
      { from: 7, to: 704 },
      
      { from: 24, to: 2401 },
      { from: 24, to: 2402 },
    ]
  });

  myDiagram.layout.root = myDiagram.findNodeForKey(1);
  </script>
</body></html>

And the updated layout. I’m not sure yet how much of this will go into the published extension.

"use strict";
/*
*  Copyright (C) 1998-2022 by Northwoods Software Corporation. All Rights Reserved.
*/

// A custom Layout that lays out a chain of nodes in a snake-like fashion

/*
* This is an extension and not part of the main GoJS library.
* Note that the API for this class may change with any version, even point releases.
* If you intend to use an extension in production, you should copy the code to your own source directory.
* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/

/**
* @constructor
* @extends Layout
* @class
* This layout assumes the graph is a chain of Nodes,
* positioning nodes in horizontal rows back and forth, alternating between left-to-right
* and right-to-left within the {@link #wrap} limit.
* {@link #spacing} controls the distance between nodes.
* <p/>
* When this layout is the Diagram.layout, it is automatically invalidated when the viewport changes size.
*/
function SerpentineLayout() {
  go.Layout.call(this);
  this.isViewportSized = true;
  this._spacing = new go.Size(30, 30);
  this._wrap = NaN;
  this.root = null;
}
go.Diagram.inherit(SerpentineLayout, go.Layout);

/**
* @ignore
* Copies properties to a cloned Layout.
* @this {SerpentineLayout}
* @param {Layout} copy
*/
SerpentineLayout.prototype.cloneProtected = function(copy) {
  go.Layout.prototype.cloneProtected.call(this, copy);
  copy._spacing = this._spacing;
  copy._wrap = this._wrap;
};

/**
* This method actually positions all of the Nodes, assuming that the ordering of the nodes
* is given by a single link from one node to the next.
* This respects the {@link #spacing} and {@link #wrap} properties to affect the layout.
* @this {SerpentineLayout}
* @param {Diagram|Group|Iterable} coll the collection of Parts to layout.
*/
SerpentineLayout.prototype.doLayout = function(coll) {
  var diagram = this.diagram;
  coll = this.collectParts(coll);

  var root = this.root;
  if (root === null) {
    // find a root node -- one without any incoming links
    var it = coll.iterator;
    while (it.next()) {
      var n = it.value;
      if (!(n instanceof go.Node)) continue;
      if (root === null) root = n;
      if (n.findLinksInto().count === 0) {
        root = n;
        break;
      }
    }
  }
  // couldn't find a root node
  if (root === null) return;

  var spacing = this.spacing;

  // calculate the width at which we should start a new row
  var wrap = this.wrap;
  if (diagram !== null && isNaN(wrap)) {
    if (this.group === null) {  // for a top-level layout, use the Diagram.viewportBounds
      var pad = diagram.padding;
      wrap = Math.max(spacing.width * 2, diagram.viewportBounds.width - 24 - pad.left - pad.right);
    } else {
      wrap = 1000; // provide a better default value?
    }
  }

  // implementations of doLayout that do not make use of a LayoutNetwork
  // need to perform their own transactions
  if (diagram !== null) diagram.startTransaction("Serpentine Layout");

  // start on the left, at Layout.arrangementOrigin
  this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
  var x = this.arrangementOrigin.x;
  var rowh = 0;
  var y = this.arrangementOrigin.y;
  var increasing = true;
  var node = root;
  while (node !== null) {
    var orignode = node;
    if (node.containingGroup !== null) node = node.containingGroup;
    var b = this.getLayoutBounds(node);
    // get the next node, if any
    var nextlink = orignode.findLinksOutOf().first();
    if (nextlink !== null && nextlink.isTreeLink) nextlink = null;  // don't follow links into its own subtree
    var nextnode = (nextlink !== null ? nextlink.toNode : null);
    var orignextnode = nextnode;
    if (nextnode !== null && nextnode.containingGroup !== null) nextnode = nextnode.containingGroup;
    var nb = (nextnode !== null ? this.getLayoutBounds(nextnode) : new go.Rect());
    if (increasing) {
      node.move(new go.Point(x, y));
      x += b.width;
      rowh = Math.max(rowh, b.height);
      if (x + spacing.width + nb.width > wrap) {
        y += rowh + spacing.height;
        x = wrap - spacing.width;
        rowh = 0;
        increasing = false;
        if (nextlink !== null) {
          nextlink.fromSpot = go.Spot.Right;
          nextlink.toSpot = go.Spot.Right;
        }
      } else {
        x += spacing.width;
        if (nextlink !== null) {
          nextlink.fromSpot = go.Spot.Right;
          nextlink.toSpot = go.Spot.Left;
        }
      }
    } else {
      x -= b.width;
      node.move(new go.Point(x, y));
      rowh = Math.max(rowh, b.height);
      if (x - spacing.width - nb.width < 0) {
        y += rowh + spacing.height;
        x = 0;
        rowh = 0;
        increasing = true;
        if (nextlink !== null) {
          nextlink.fromSpot = go.Spot.Left;
          nextlink.toSpot = go.Spot.Left;
        }
      } else {
        x -= spacing.width;
        if (nextlink !== null) {
          nextlink.fromSpot = go.Spot.Left;
          nextlink.toSpot = go.Spot.Right;
        }
      }
    }
    node = orignextnode;
  }

  if (diagram !== null) diagram.commitTransaction("Serpentine Layout");
};

// Public properties

/**
* Gets or sets the {@link Size} whose width specifies the horizontal space between nodes
* and whose height specifies the minimum vertical space between nodes.
* The default value is 30x30.
* @name SerpentineLayout#spacing

* @return {Size}
*/
Object.defineProperty(SerpentineLayout.prototype, "spacing", {
  get: function() { return this._spacing; },
  set: function(val) {
    if (!(val instanceof go.Size)) throw new Error("new value for SerpentineLayout.spacing must be a Size, not: " + val);
    if (!this._spacing.equals(val)) {
      this._spacing = val;
      this.invalidateLayout();
    }
  }
});

/**
* Gets or sets the total width of the layout.
* The default value is NaN, which for {@link Diagram#layout}s means that it uses
* the {@link Diagram#viewportBounds}.
* @name SerpentineLayout#wrap

* @return {number}
*/
Object.defineProperty(SerpentineLayout.prototype, "wrap", {
  get: function() { return this._wrap; },
  set: function(val) {
    if (this._wrap !== val) {
      this._wrap = val;
      this.invalidateLayout();
    }
  }
});

Thanks for this customized version of SerpentineLayout Walter. I can achieve 70% of my requirement with this customized version. But i am having below queries still.

  1. How much this customized version go into published extension? Otherwise i have to maintain the file.
  2. In the above explanation, you applied layout as Tree Layout for Group Template. Based on my understanding this groupTemplate definition is for diagram. But i need Treelayout for some task(node) and serpentine layout for some other task(node). I mean in the same diagram can we apply two different groupTemplate definition.
  1. We always recommend that for each extension that you use, you copy the code into your own project so that you do not need to worry about any changes we make to the extension. That policy also makes it easier for you to modify the behavior of the extension. In this case I have made a change for you, but it is yet unclear how many of these changes in our opinion will be desired by everyone else.

  2. I’m unsure what you mean. Yes, you can have multiple Group templates, each with its own layout. Or you could have one Group template and use a data binding to assign different layouts to the Group.layout property.