DEMOS
DOCS
FEATURES
DOWNLOAD
PURCHASE
CONTACT
BLOG

Layouts

The Surface component in the Toolkit has support for layouts for the nodes 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.



Applying a 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:

var toolkit = jsPlumbToolkit.newInstance();

... load data...

var 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"
});

Parameters

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


Refresh vs Relayout

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:

var t = jsPlumbToolkit.newInstance();
var 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


AdHoc Layout Calls

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:

var 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


Magnetization

The AbstractLayout class (and therefore every other Layout) has the ability to apply "magnetization" to the Nodes in the layout in order to push everything apart so that no two nodes 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.

Permanent Magnetization

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.

Layout Default

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.

AdHoc Magnetization

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 Nodes as the center of magnetization
var surface = toolkitInstance.render({...});
surface.magnetize();
Magnetize using some specific point as the center of magnetization
var surface = toolkitInstance.render({...});
surface.magnetize({
  origin:[50,50]
});
Magnetize using the location of some Event as the center of magnetization
var surface = toolkitInstance.render({...});
surface.bind("canvasClick", function(event) {
  surface.magnetize({
    event:event
  });
});

TOP


Decorators

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.

Creating a Decorator

The first step is to write the skeleton for your decorator and register it on the Toolkit:

jsPlumbToolkit.Layouts.Decorators["Example"] = function(decoratorParams, surface) {

    this.reset = function(params) {

        // Contents of params:

        // remove(el, doNotRepaint)
    };

    this.decorate = function(params) {

        // Contents of params:

        // adapter (a Surface)
        // append(el, id, pos)
        // fixElement(el, axes, pos)
        // bounds 
        // jsPlumb
        // layout 
        // setAbsolutePosition(el, xy)
        // toolkit
    };
};
Lifecycle
  • reset is called before every relayout or refresh that occurs on the layout.
  • decorate is called before every relayout or refresh, immediately after reset.
Lifecycle Method Parameters

Each method is passed a params object, the contents of which are as follows:

reset
  • remove(el, doNotRefresh)

This is a function you can call to have the Toolkit remove some element from the canvas (or a floated element). You should use this method in preference to some other way of removing content since it ensures everything is cleaned up appropriately.

decorate
  • adapter

This is the underlying Surface that is rendering your content. In future releases of the Toolkit that support server side rendering, this may be some other object, the interface it implements will be the same.

  • append(el, id, pos)

A function you can call to append some element to the canvas. Elements appended in this way are panned and zoomed with the rest of the content. Only DOM elements are supported as el (not jQuery selectors). id is optional, and pos is location data in the form {left:..., top:...}

  • floatElement(el, pos)

A function you can call to append some element to the viewport at a given position. Elements added to the viewport float over the content and remain fixed at the given location. Note that we do not currently support specifying right or bottom properties in this method - the values in pos are expected to be [left, top].

  • fixElement(el, axes, pos)

This behaves like append in that it adds the given element to the canvas so that it is panned and zoomed with the rest of the content, but the axes argument allows you to specify that the element should not be allowed to exit the viewport in one or more axes. An example will be best to explain this:

 var el = document.createElement("div");
 el.innerHTML = "I'm a label";

 params.fixElement(el, { left:true }, [ 50, 50 ]);

In this example we have requested that our label div be placed on the content at position [50, 50]. But when the content is panned to the left to the extent that the label would be less than 50 pixels from the edge of the viewport, its location is adjusted so that it remains fixed in place in the x axis. In case it is not obvious, you could also specify top:true in the axes argument.

  • bounds

This is an array of [ xmin, ymin, xmax, ymax ] locations, giving the extents of the content. Remember that in the Toolkit, the origin of the canvas does not necessarily correlate with the top left corner of your layout's extents. It depends on what the layout chooses to do. All of the layouts that ship with the Toolkit, for instance, routinely draw into the negative in both axes. So the point here is that it may be the case that if you want something to appear at the origin of your layout's extents, that point will not be [0,0]. It will, though, be the first two values in bounds, and you are safe to pass bounds to either append or fixElement:

  var el = document.createElement("div");
  el.innerHTML = "I'm a label";

  params.fixElement(el, { left:true }, params.bounds);

Here we have requested that our label appear at the origin of the layout's extents, and for it to stay there in the X axis as the user pans to the left.

  • jsPlumb

The underlying jsPlumb renderer. The majority of use cases will not need to access this.

  • layout

The layout used to render this data. Typically, a decorator and a layout work in tandem: the layout knows the key bits of information a decorator needs. Take a process flow diagram as an example: the layout has created the lanes and placed nodes into these lanes; it is the layout that can tell the decorator where the lanes start and end.

  • setAbsolutePosition(el, pos)

A function you can use to set the position of some element on the canvas. el is a DOM element, and pos is an array of [left,top] pixel values.

  • toolkit

The underlying jsPlumb Toolkit instance.

Invoking a decorator

You invoke decorators by specifying them in the layout section of a render method call:

var renderer = myToolkitInstance.render({
    container:someElement,
    ...,
    layout:{
        type:"Hierarchy",
        decorators:[ "Example" ]
    }
});

You can attach an arbitrary number of decorators.

You may have noticed in the constructor for the Example decorator above there were two arguments: decoratorParams and surface. The second of these is the underlying Surface widget. The first argument would be an empty object in the arrangement shown above, but you can pass constructor parameters into a decorator like this:

 var renderer = myToolkitInstance.render({
     container:someElement,
     ...,
     layout:{
         type:"Hierarchy",
         decorators:[ 
            [ "Example", { foo:1, bar:25 } ]
         ]
     }
 });

Simple Example

In our Example decorator we will float an element near the top left corner, and draw a blue background around the entire contents:

jsPlumbToolkit.Layouts.Decorators["Example"] = function(options) {

    var label, background;

    this.reset = function(params) {
        label && params.removeElement(label);
        background && params.removeElement(background);
    };

    this.decorate = function(params) {

        label = document.createElement("div");
        label.className = "aLabel";
        label.innerHTML = "My Decorator";
        params.floatElement(label, [ 50, 50 ]);

        background = document.createElement("div");
        background.className = "background";
        var w = params.bounds[2] - params.bounds[0], 
            h = params.bounds[3] - params.bounds[1];
        background.style.width = (w + 40) + "px";
        background.style.height = (h + 40) + "px";
        var xy = [ params.bounds[0] - 20, params.bounds[1] - 20 ];

        params.append(background, null, xy);
    };
};

Here we've floated the label, and then created a background element that will overlap the content by 20 pixels on each side. 20 pixels, though...what if we wanted to set that?

Decorator Options

You can pass options to a decorator using the syntax with which you may be familiar from anchors etc in theCommunity edition:

var renderer = myToolkitInstance.render({
    container:someElement,
    ...,
    layout:{
        type:"Hierarchy",
        decorators:[ 
            [ "Example", { padding:20 } ]
        ]
    }
});

The options are passed in to the constructor of the decorator. So we can rewrite our decorator now:

jsPlumbToolkit.Layouts.Decorators["Example"] = function(options) {

    var label, background;

    var padding = options.padding || 20;

    this.reset = function(params) {
        label && params.removeElement(label);
        background && params.removeElement(background);
    };

    this.decorate = function(params) {

        label = document.createElement("div");
        label.className = "aLabel";
        label.innerHTML = "My Decorator";
        params.floatElement(label, [ 50, 50 ]);

        background = document.createElement("div");
        background.className = "background";
        var w = params.bounds[2] - params.bounds[0], 
            h = params.bounds[3] - params.bounds[1];
        background.style.width = (w + (padding * 2)) + "px";
        background.style.height = (h + (padding * 2)) + "px";
        var xy = [ params.bounds[0] - padding, params.bounds[1] - padding ];

        params.append(background, null, xy);
    };
};

Z Index

The Toolkit will not take care of z-index for you. Webapps come in a million different shapes and sizes; it would be too invasive for the Toolkit to infer anything. Keep this in mind when you write your decorators...

Hierarchy Decorator

From version 1.1.0, the Toolkit ships with a decorator you can use to draw a background behind a Hierarchical layout. You use it like this:

toolkit.render({
  container:"someElement",
  layout:{
    type:"Hierarchical",
    decorators:"Hierarchy"
  }
});

This decorator is used in the Toolkit's Hierarchical Layout demonstration.

TOP


Custom Layouts

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:

var 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() {
    var _super = jsPlumbLayout.AbstractLayout.apply(this, arguments),
        position = 0,
        nodeCount = 0,
        padding;

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

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

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

        // step across in the x direction
        var x = counter;

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

        // increment counter
        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.

Magnetization

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;
    var _super = jsPlumbLayout.AbstractLayout.apply(this, arguments),
    ...