Layouts / Hierarchical Layout

Hierarchical Layout

The Hierarchical layout uses the Walker algorithm to position nodes/groups in a hierarchy, oriented either vertically or horizontally. The classic use cases for this layout are such things as a family tree or org chart.

{ "renderParams":{ "layout":{ "type":"Hierarchical" }, "jsPlumb":{ "Anchors":["Bottom", "Top" ], "Endpoint":[ "Dot", { "radius":3 }], "Connector":"StateMachine" }, "zoomToFit":true }, "data":{ "nodes":[ { "id":"1" }, { "id":"2" }, { "id":"3" }, { "id":"4" }, { "id":"5" }, { "id":"6" }, { "id":"7" }, { "id":"8" }, { "id":"9" }, { "id":"10" }, { "id":"11" } ], "edges":[ { "source":"1", "target":"2" }, { "source":"1", "target":"9" }, { "source":"2", "target":"3" }, { "source":"2", "target":"6" }, { "source":"9", "target":"10" }, { "source":"3", "target":"4" }, { "source":"3", "target":"5" }, { "source":"6", "target":"7" }, { "source":"6", "target":"8" }, { "source":"10", "target":"11" } ] } }

Note This layout does not, by default, refresh when edges are added/removed, for performance reasons (as it has to visit the entire dataset). You can set refreshLayoutOnEdgeConnect on a render call to get the layout to refresh when edges are added/removed:

var surface = toolkit.render({
  container:"someElement",
  layout:{
      type:"Hierarchical",
      parameters:{

      }
  },
  refreshLayoutOnEdgeConnect:true
});

  • rootNode Optional. Defines the node/group to use as the root of the tree. This may be provided either as a node/group id or as a node/group object. If this parameter is not specified and multipleRoots is not false then the layout uses the result(s) of the getRootNode function; otherwise it uses the first node found in the dataset.

  • getChildEdges Optional function used to determine the edges to traverse to find children from some node. See discussion below

  • getRootNode Optional. A function that is given the Toolkit instance as argument and is expected to return either a single node/group, or an array of nodes/groups, to use as the root(s) for the layout. See discussion below.

  • multipleRoots Optional, defaults to true. If false, multiple roots are not supported, and assuming you have not overriden getRootNode, the layout uses the first node found in the dataset (otherwise it still uses the result of your getRootNode function).

  • padding Optional [ x, y ] array of values to use as the padding between elements.

  • orientation Optional, defaults to "vertical". Valid values are "vertical" and "horizontal".

The layout at the top of this page shows the horizontal. Here we show the same dataset in the vertical orientation:

{ "renderParams":{ "layout":{ "type":"Hierarchical", "parameters":{ "orientation":"vertical" } }, "jsPlumb":{ "Anchors":["Right", "Left" ], "Endpoint":[ "Dot", { "radius":3 }], "Connector":"StateMachine" }, "zoomToFit":true }, "data":{ "nodes":[ { "id":"1" }, { "id":"2" }, { "id":"3" }, { "id":"4" }, { "id":"5" }, { "id":"6" }, { "id":"7" }, { "id":"8" }, { "id":"9" }, { "id":"10" }, { "id":"11" } ], "edges":[ { "source":"1", "target":"2" }, { "source":"1", "target":"9" }, { "source":"2", "target":"3" }, { "source":"2", "target":"6" }, { "source":"9", "target":"10" }, { "source":"3", "target":"4" }, { "source":"3", "target":"5" }, { "source":"6", "target":"7" }, { "source":"6", "target":"8" }, { "source":"10", "target":"11" } ] } }
  • {Boolean} compress Optional, defaults to false. DEPRECATED: use spacing:"compress". If true, the layout will use a regular spacing between each node and its parent. Otherwise the layout pushes each node down by the maximum size of some element in that level of the hierarchy. For complex hierarchies in which any given node may have children, that is better. But for simple hierarchies, setting compress can give good results.

  • {string} spacing Optional, defaults to "auto". Valid values are:

    • "auto" Spaces each node and its parent according to the size of the biggest node in the given node's level
    • "compress" Uses a regular spacing between each node and its parent
  • {boolean} invert Optional, defaults to false. If true, the layout will be inverted, ie. the root node will be at the bottom for horizontal layouts, and to the right for vertical layouts.

  • {string} align Optional, defaults to "center". Instructs the layout how to place child nodes with respect to their parent nodes. By default, a group of child nodes is centered on its parent. The layout also supports "start" and "end" for this value, which work in much the same way as "flex-start" and "flex-end" do in CSS: for a hierarchical layout with the root at the top of the tree and the child nodes underneath, a value of "start" for align would cause the first child of the root to be placed immediately under the root, with its first child immediately underneath, etc. The remainder of the content would fan out to the right. This option also works in conjunction with invert and orientation:"vertical".

align:"start"
{ "renderParams":{ "layout":{ "type":"Hierarchical", "parameters":{ "align":"start" } }, "jsPlumb":{ "Anchors":["Bottom", "Top" ], "Endpoint":[ "Dot", { "radius":3 }], "Connector":"StateMachine" }, "zoomToFit":true }, "data":{ "nodes":[ { "id":"1" }, { "id":"2" }, { "id":"3" }, { "id":"4" }, { "id":"5" }, { "id":"6" }, { "id":"7" }, { "id":"8" }, { "id":"9" }, { "id":"10" }, { "id":"11" } ], "edges":[ { "source":"1", "target":"2" }, { "source":"1", "target":"9" }, { "source":"2", "target":"3" }, { "source":"2", "target":"6" }, { "source":"9", "target":"10" }, { "source":"3", "target":"4" }, { "source":"3", "target":"5" }, { "source":"6", "target":"7" }, { "source":"6", "target":"8" }, { "source":"10", "target":"11" } ] } }
align:"end"
{ "renderParams":{ "layout":{ "type":"Hierarchical", "parameters":{ "align":"end" } }, "jsPlumb":{ "Anchors":["Bottom", "Top" ], "Endpoint":[ "Dot", { "radius":3 }], "Connector":"StateMachine" }, "zoomToFit":true }, "data":{ "nodes":[ { "id":"1" }, { "id":"2" }, { "id":"3" }, { "id":"4" }, { "id":"5" }, { "id":"6" }, { "id":"7" }, { "id":"8" }, { "id":"9" }, { "id":"10" }, { "id":"11" } ], "edges":[ { "source":"1", "target":"2" }, { "source":"1", "target":"9" }, { "source":"2", "target":"3" }, { "source":"2", "target":"6" }, { "source":"9", "target":"10" }, { "source":"3", "target":"4" }, { "source":"3", "target":"5" }, { "source":"6", "target":"7" }, { "source":"6", "target":"8" }, { "source":"10", "target":"11" } ] } }

The getChildEdges function, which is an optional parameter on the Hierarchical layout, forms the core of how the Hierarchical layout traverses the dataset. It is defined as:

getChildEdges = function(nodeOrGroup:Node|Group, toolkit:JsPlumbToolkit):Edge[];

Its purpose is to return a list of Edge objects for the given node/group, where the edges define the connections from this node/group to what should be considered its children. The default implementation of this is:

function(nodeOrGroup, toolkit) {
    return toolkit.getAllEdgesFor(nodeOrGroup, function(e) {
        return e.source === nodeOrGroup || (params.ignorePorts !== true && e.source.getNode && e.source.getNode() === nodeOrGroup);
    });
}

Which basically says, get all the edges for which this node/group (or port, if connecting via ports is enabled) is the source.

Custom implementations of this function can, for example, test the type of edges, to exclude certain types from determining the structure of the hierarchy:

function(nodeOrGroup, toolkit) {
    return toolkit.getAllEdgesFor(nodeOrGroup, function(e) {
        return e.data.type !== "FLOATING" && e.source === nodeOrGroup || (params.ignorePorts !== true && e.source.getNode && e.source.getNode() === nodeOrGroup);
    });
}

Here we test each edge to see if it is of type FLOATING, and if so, we do not return it. The result would be that the hierarchical layout would be determined by the other edges; any edges of this type would still be drawn, of course. They would simply be painted to/from wherever in the hierarchy the two end nodes/groups were located.


In the Hierarchical layout there are always one or more "root" nodes, which are nodes that are to be placed at the root of the hierarchy. The Hierarchical layout uses the optional getRootNode function to determine the root node(s), the default implementation of which is as follows:

function(toolkit) {
    if (params.multipleRoots !== false) {
        return toolkit.filter(function(o) {
                return (o.objectType === "Node" && o.getTargetEdges().length == 0 && o.group == null) ||
                    (o.objectType === "Group" && o.getTargetEdges().length == 0);
            }).getAll();
    }
    else {
        return (_super.adapter.getNodeCount() > 0) ? _super.adapter.getNodeAt(0) : null;
    }
}

When multipleRoots is enabled (which it is by default) then this function searches for all nodes/groups that are not targets of any edges. When multipleRoots is set to false, the function returns the first node in the dataset. You should keep this default behaviour in mind when using this layout - be sure that the first node in your dataset is your root (or provide your own getRootNode function).


Examples

Simple Example

Here we tell the Surface what the ID of the node to use as the root is, and we specify some padding and a horizontal orientation.

toolkit.render({
  container:"someElement",
  layout:{
    type:"Hierarchical",
    parameters:{
      rootNode:"rootNodeId",
      padding:[ 50, 50 ],
      orientation:"horizontal"
    }
  }
});

Edge Filter Example

Taken from the discussion above: only edges that are not of type FLOATING will determine the structure of the hierarchy.

toolkit.render({
  container:"someElement",
  layout:{
    type:"Hierarchical",
    parameters:{
        getChildEdges:function(nodeOrGroup, toolkit) {
          return toolkit.getAllEdgesFor(nodeOrGroup, function(e) {
              return e.data.type !== "FLOATING" && e.source === nodeOrGroup || (params.ignorePorts !== true && e.source.getNode && e.source.getNode() === nodeOrGroup);
          });
      }
    }
  }
});

Root Nodes Example

Return all nodes whose type is ROOT to use as the roots of the hierarchy.

toolkit.render({
  container:"someElement",
  layout:{
    type:"Hierarchical",
    parameters:{
        getRootNodes:function(toolkit) {
          return toolkit.filter(function(o) {
              return (o.objectType === "Node" && o.type === "ROOT")
          }).getAll();
      }
    }
  }
});