Layouts / Overview

Layouts

The Surface component in the Toolkit has support for layouts for the nodes/groups in your UI. The Toolkit ships with support for four layout types, and exposes an API for you to write your own.

You do not interact directly with a Layout when using the Toolkit: the methods you generally need to use are exposed through the Surface class.

Every Layout supports the concept of "magnetization" - adjusting the positions of elements such that no two elements overlap. This is a useful mechanism for making small adjustments to the UI without running a layout all over again. You can also apply layouts in an 'adhoc' fashion - just once, without changing the main layout.

Each instance of the Toolkit has zero or more associated Surface components; it is to these objects that a Layout is applied. You can either do this in the render call itself:

let toolkit = jsPlumbToolkit.newInstance();

... load data...

let surface = toolkit.render({
    container:"someElement",
    layout:{
      type:"Hierarchical",
      parameters:{
        padding:[ 50, 50 ]
      }
    }
});

... or you can call setLayout on a Surface at any time to change the Layout in use:

surface.setLayout({
  type:"Circular"
});

Each layout supports its own set of parameters. You set these inside a parameters member in the object that you pass to the layout member of a render call, or in the object you pass to a setLayout call.

In the first example above, for instance, we can see that the Hierarchical layout supports a padding parameter, which is an array of [ x, y ] padding values. For a discussion of the supported parameters for each layout, see the sections below.

TOP


All layouts offer two basic methods for user interaction (although, as mentioned above, you do not call these methods directly on a layout, you call them on a Surface): refresh and relayout.

relayout

Calling relayout will cause a Layout to clear all positioning data and start over from scratch. This is what is automatically called at the end of a data load, and can also be called manually should you wish to. For deterministic Layouts such as Hierarchical, you can be sure that relayout will always produce the same result from some given input. For other layouts, such as Spring, this is not the case: the Spring layout initially positions Nodes according to some random scheme, and so each time you run the algorithm you get a unique result.

Another distinction between relayout and refresh is that with relayout you can pass in a new set of parameters.

refresh

Calling refresh will cause a Layout to be re-run without first resetting itself. As mentioned above, for most layouts this will not be any different from a relayout. For other types of layout - Hierarchical, for instance - there is no discernible difference between a refresh and a re-layout, except for the fact that the relayout method allows you to pass in a new set of parameters for the layout to use.

Whenever a change is made to the data model, the default behaviour is to make an internal call to refresh. This keeps the state of the UI as constant as possible for the user. You can suppress this behaviour by setting refreshAutomatically:false to a render call on an instance of the jsPlumb Toolkit. Should you wish to force a refresh or re-layout yourself, the Surface component offers methods for you to do so:

let t = jsPlumbToolkit.newInstance();
let surface = t.render( { 
  refreshAutomatically:false,
  ...options... 
} );

//
// things happen
//
surface.refresh();

and

surface.relayout({ ...options... });

Note that when you call relayout you can pass a new set of options to the layout.

Another important consideration is that refresh retains existing Nodes, and therefore any event listeners you have registered, whereas relayout empties the DOM and creates everything anew, requiring that you re-register any event listeners. The Toolkit fires events whenever either of these methods are executed - see the documentation on Events.

TOP


The Surface widget offers a method to run a layout on your data without making the layout permanent. Say, for instance, you were using an Absolute layout:

let surface = toolkit.render({
  container:"someElement",
  layout:{
    type:"Absolute"
  }
});

And at some point you wanted to apply a Spring layout just to clean up the UI. You can call adHocLayout for this:

surface.adHocLayout({
    type:"Spring"
});

The format of the argument to adHocLayout is the same as for any call to setLayout or for a layout specified in a view. If you have your own layouts registered with the Toolit they can of course be referenced by this method too.

TOP


The AbstractLayout class (and therefore every other Layout) has the ability to apply "magnetization" to the nodes/groups in the layout in order to push everything apart so that no two objects overlap. Magnetization can be switched on permanently (executed after every refresh or relayout), or it can be run only on demand.

This functionality is very useful for helping users maintain a legible UI.

To switch on permanent magnetization, set magnetize:true in the layout options to a render method call:

toolkit.render({
  container:"someElement",
  layout:{
    type:"Spring",
    padding:[30,30],
    magnetize:true
  }
});

Here we have instructed the Layout to run the magnetizer after every refresh or relayout, and to ensure there is 30 pixels padding in each axis between any two Nodes.

It is possible for a layout to declare that it wishes permanent magnetization to be switched on by default. The Spring layout does this, for example.

If you've written a custom layout for which you'd like to switch on permanent magnetization, see below.

To run the magnetizer manually, you call the magnetize method on a Surface object. There are three ways you can call this method:

Magnetize using the center of all objects as the center of magnetization
let surface = toolkitInstance.render({...});
surface.magnetize();
Magnetize using some specific point as the center of magnetization
let surface = toolkitInstance.render({...});
surface.magnetize({
  origin:[50,50]
});
Magnetize using the location of some Event as the center of magnetization
let surface = toolkitInstance.render({...});
surface.bind("canvasClick", function(event) {
  surface.magnetize({
    event:event
  });
});

TOP


A Decorator is a class that can add arbitrary extra content to the UI after a layout has been run. Content can be added in three different ways:

  • Appended to the work area. In this case, the decoration will pan and zoom with the nodes and edges in your UI.
  • Floated over the work area. Use this to add layout-specific control elements, for example.
  • Appended to the work area but fixed in one or both axes so that it never leaves the viewport.

Decorators are quite a large topic and have been moved out to a dedicated page.

TOP


For many UIs, the layouts that ship with the Toolkit will provide sufficient functionality. However you may find that you need to write a custom layout for your UI - the Toolkit allows you to do this.

In this section we'll go through the steps involved in writing your own layout.

1. Create and declare your layout

;(function(exports) {
  exports.myLayout = function() {

  };
})(jsPlumbToolkit.Layouts);

2. Subclass the abstract layout

;(function(exports) {
  exports.myLayout = function() {
    var _super = jsPlumbToolkit.Layouts.AbstractLayout.apply(this, arguments);
  };
})(jsPlumbToolkit.Layouts);

_super offers various operations that a layout needs: getting node data and element sizes, getting edge data, setting positions etc.

3. Implement lifecycle methods

The Renderer in the jsPlumb Toolkit will never directly call your layout (unless you accidentally override one of the methods in AbstractLayout, that is!); instead it interfaces with AbstractLayout, which works with your layout via a few lifecycle methods. All of these methods, except step, are optional.

The Layout Lifecycle

AbstractLayout has two methods that a Renderer will call:

  • layout() runs the layout, without resetting it.
  • relayout([parameters]) runs the layout, first resetting it.

You should not override these methods. Your layout will not work if you do so.

relayout([parameters])

When relayout() is called, AbstractLayout resets the current parameters, then calls its own private _reset() method, which calls

this.reset()

...if the subclass has defined such a method. It then calls this.layout().

layout()

When layout() is called, AbstractLayout calls

this.begin(toolkit, parameters);

if the subclass has defined it. It then repeatedly calls

this.step(toolkit, parameters);

which subclasses are expected to implement, until the subclass calls

_super.setDone(true);

At this point, AbstractLayout converts the node positions and actually places nodes on screen.

Last, the AbstractLayout calls

this.end(toolkit, parameters);

...if the subclass has defined such a method.

Note that in all these examples, parameters is an object containing the parameters you set on the Layout, merged on top of the default parameter values for the Layout in effect.

Default Parameters

Each layout can expose a member containing default parameter values:

this.defaultParameters = {
  foo:"bar",
  etc
};

When you make a render call and supply some parameters, they are merged on top of a copy of the default parameters before being supplied to the Layout's lifecycle methods:

let renderer = someToolkitInstance.render({
  container:"someElement",
  layout:{
    type:"Hierarchical",
    parameters:{
      orientation:"vertical"
    }
  }
});

The default value of the orientation parameter on the Hierarchical layout is horizontal. So in this example the value is overridden by vertical, but the padding values used will be the defaults.

The same principle applies when you call relayout with some new parameters - say we want to reapply the layout above at some point, but switch it back to horizontal with more padding:

renderer.relayout({
  orientation:"horizontal",
  padding:[100,100]
});
Positioning Nodes

Obviously it is the subclass's responsibility to place all the nodes. The return value from AbstractLayout's constructor has several methods you can use to find out information you need:

  • getPosition(nodeId) gets a node's current position, creating and randomly assigning one if it has not yet been placed.

  • setPosition(nodeId, x, y) sets a node's new position.

  • getSize(nodeId) Returns the size of a node's element in the display.

Viewport Size

The size of the enclosing viewport is exposed on the _super object with two properties:

  • width the width of the viewport, in pixels.
  • height the height of the viewport, in pixels.

You may or may not need to care about the size of the viewport. Many Layouts do not, since the renderer provides a pannable surface whose extents may lay outside the bounds of the viewport. But some Layouts may wish to paint themselves entirely inside their viewport.

Example

Taking the code from above, here is a layout that draws all the nodes in the Toolkit instance in a straight line across the middle of the viewport, with 50 pixels spacing (by default; you can pass this value in as a parameter) between them:

;(function(exports) {
  exports.myLayout = function() {
    let _super = jsPlumbLayout.AbstractLayout.apply(this, arguments),
        position = 0,
        nodeCount = 0,
        padding,
        counter;

    // called by superclass on relayout      
    this.reset = function() {
      position = 0;
      nodeCount = 0;      
    };

    // called by superclass on relayout OR refresh  
    this.begin = function(graph, toolkit, parameters) {
      nodeCount = toolkit.getNodeCount();
      padding = parameters.padding || 50;
      counter = 0;
    };

    this.step = function(graph, toolkit, parameters) {
      for (let i = 0; i < nodeCount; i++) {
        let node = toolkit.getNodeAt(i),
            size = toolkit.getNodeSize(node.id);

        // position around middle of the y axis
        let y = (_super.height - size[1]) / 2;

        // set the position for this node
        _super.setPosition(node.id, counter, y);

        // increment counter for x direction
        counter += (size[0] + padding);        
      }

      // inform the super that the layout is done.
      _super.setDone(true);
    };

    this.end = function(graph, toolkit, parameters) {
      // unused.
    };
  };
})(jsPlumbToolkit.Layouts);

Note in this example how we call _super.setDone(true) right at the end of the first step call. This is because this layout is able to do everything in one pass. Some types of layouts - Spring, for instance - work iteratively until some condition is met.

As discussed above, it is possible for a layout to switch on magnetization by default. If you wish to do this for your custom layout, you must set defaultMagnetized:true in your constructor before calling the AbstractLayout constructor:

;(function(exports) {
  exports.myLayout = function() {
    this.defaultMagnetized = true;
    let _super = jsPlumbLayout.AbstractLayout.apply(this, arguments),
    ...