Skip to main content

Layouts

A core piece of functionality offered by the Toolkit is support for layouts - a means to control the positioning of the vertices in your application. Layouts are renderer agnostic - they don't know about the DOM - and are shipped either in the @jsplumbtoolkit/core package or in a separate package, depending on the layout. The available layouts are:

  • Absolute This layout positions vertices dependent on values in each vertex's backing data. For many applications in which the position of vertices is under the control of a user, this layout is a good choice. It can be combined with other layouts, such as the Spring layout, to implement a scheme whereby vertices untouched by a user are placed automatically and vertices touched by a user are placed wherever the user chose.

  • Spring This is a force directed layout, positioning vertices in an optimum position relative to vertices to which they are connected. As mentioned above, this layout is an extension of the Absolute layout, which can be instructed to honour user-supplied values for vertices if present.

  • Balloon This layout groups vertices into clusters. This is a useful layout for certain types of unstructured data such as mind maps.

  • Hierarchical Positions vertices in a hierarchy, oriented either vertically or horizontally. The classic use cases for this layout are such things as a family tree or an org chart. NOTE: from 5.5.0 onwards, you might want to try the Hierarchy layout instead, which is a more flexible version of this layout that will typically handle real world use cases a little better.

  • Hierarchy Implements a modified version of the Sugiyama algorithm to position vertices.

  • Circular Arranges all the nodes/groups in the toolkit instance into a circle, with a radius sufficiently large that no two nodes/groups overlap.

In the UI, layouts are used by the Surface component for positioning the vertices that are visible on the canvas, and also inside each group that is rendered by the Surface. For a discussion of layouts inside of groups, see the groups documentation.

Applying a layout

Each instance of the Toolkit has zero or more associated Surface components. Each of these has an associated layout, which can be set when the surface is instantiated, or after the fact. You do not interact directly with a layout when using a surface: the methods you generally need to use are exposed through the surface class.

const toolkit = jsPlumbToolkitBrowserUI.newInstance()

... load data...

let surface = toolkit.render(someElement, {
layout:{
type:"Hierarchical",
options:{
padding: { x:50, y: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 object containing { x, y } padding values. For a discussion of the supported parameters for each layout, see the individual layout pages linked above.

TOP


Refreshing a layout

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 toolkit. Should you wish to force a refresh or re-layout yourself, the surface component offers methods for you to do so:

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

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

and

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

Notice that when you call relayout you can pass a new set of options to the layout (but it is not compulsory).

Another important consideration is that refresh retains existing DOM elements, whereas relayout empties the DOM and creates everything anew. If you've registered any event listeners directly on previously rendered elements, you'll need to re-register them after arelayout. It's generally better to either use the events object of a definition inside of a view to capture events on a vertex, or to use delegated event handlers.

The Toolkit fires events whenever either of relayout or refresh 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:

let surface = toolkit.render(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 Toolkit they can of course be referenced by this method too.

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. We'll provide an example here that lays out your vertices in a diagonal line across the screen; a useless layout, perhaps, but it should suffice to explain what needs to be done.

1. Define parameters for your layout.

This step is in fact optional, but these parameters appear in the class definition and constructor shown in the next step, so we're putting this first.

Let's imagine in our diagonal line layout we're going to be able to supply the distance between node corners and the angle of the line. We need to declare an interface for this, and have that interface extend LayoutParameters:


import { LayoutParameters, JsPlumbToolkit, InternalLayoutOptions, Vertex, Layouts, } from '@jsplumbtoolkit/core';

export interface DiagonalLineLayoutParameters extends LayoutParameters {
distanceBetweenCorners?:number
angleInDegrees?:number
}

2. Extend AbstractLayout


import { LayoutParameters, JsPlumbToolkit, InternalLayoutOptions, Vertex, Layouts, } from '@jsplumbtoolkit/core';

export class DiagonalLineLayout extends AbstractLayout<DiagonalLineLayoutParameters> {

distanceBetweenCorners:number
angle:number

constructor(params:InternalLayoutOptions<DiagonalLineLayoutParameters>) {
super(params)

this.distanceBetweenCorners = params.options.distanceBetweenCorners
this.angle = params.options.angleInDegrees * Math.PI / 180

}

}

3. Implement lifecycle methods

There are 3 lifecycle methods you can implement in your code. Each of these methods is in fact optional, but you'll want to at least supply begin or step, as the only way the layout knows it has completed is when you call, in one of your methods, this.done = true. In some of its internal layouts, the Toolkit does all the work in begin and then in step there's just this.done = true. In others, such as the Spring layout, the layout is iterative and step does the heavy lifting.

We'll provide implementations of all three methods:

import { LayoutParameters, JsPlumbToolkit, InternalLayoutOptions, Vertex, Layouts, } from '@jsplumbtoolkit/core';

export class DiagonalLineLayout extends AbstractLayout<DiagonalLineLayoutParameters> {

distanceBetweenCorners:number
angle:number

curX:number
curY:number

constructor(params:InternalLayoutOptions<DiagonalLineLayoutParameters>) {
super(params)

this.distanceBetweenCorners = params.options.distanceBetweenCorners
this.angle = params.options.angleInDegrees * Math.PI / 180
}

/**
* This is an abstract function that subclasses may implement if they wish. It will be called at the beginning of a layout.
* @param toolkit The associated jsPlumbToolkit instance
* @param parameters Parameters configured for the layout.
*/
begin(toolkit:JsPlumbToolkit, parameters:DiagonalLineLayoutParameters) {
this.curX = 0
this.curY = 0
}

/**
* Step through the layout. For some layouts there is only a single step, but others continue stepping until some
* condition is met. Once the condition is met, the subclass must set `this.done = true`, or the layout will
* continue looping indefinitely.
* @param toolkit
* @param parameters
*/
step(toolkit:JsPlumbToolkit, parameters:DiagonalLineLayoutParameters) {
const nodes = toolkit.getNodes()
nodes.forEach(node => {

this.setPosition(node.id, this.curX, this.curY)
const s = this._getSize(node.id) // _getSize is a protected method in AbstractLayout
const dx = Math.cos(this.angle) * this.distanceBetweenCorners
const dy = Math.sin(this.angle) * this.distanceBetweenCorners
this.curX += (s.w + dx)
this.curY += (s.y + dy)
})

this.done = true // inform the superclass the layout is complete.
}

/**
* This is an abstract function that subclasses may implement if they wish. It will be called at the end of a layout.
* @param toolkit The associated jsPlumbToolkit instance
* @param parameters Parameters configured for the layout.
*/
end (toolkit:JsPlumbToolkit, parameters:DiagonalLineLayoutParameters) {
console.log("End.")
}


}
caution

Always ensure you have called this.done = true at some point in your layout, or the layout will never complete. In this example we do that right after placing the nodes; we only need one pass through. Some layouts check for conditions after each iteration and mark the layout as complete only when these conditions are satisfied, or some maximum number of iterations has been reached.

4. Configure defaults for your parameters

You may have noticed in the above code that we assigned values from what are in fact optional parameters. We did this with confidence because we know default values will have been set, via our implementation of the getDefaultParameters() method:

import { LayoutParameters, JsPlumbToolkit, InternalLayoutOptions, Vertex, Layouts, } from '@jsplumbtoolkit/core';

export class DiagonalLineLayout extends AbstractLayout<DiagonalLineLayoutParameters> {

static type = "DiagonalLine"
type = DiagonalLineLayout.type

distanceBetweenCorners:number
angle:number

curX:number
curY:number

getDefaultParameters(): DiagonalLineLayoutParameters {
return {
distanceBetweenCorners: 150,
angleInDegrees:45
}
}

constructor(params:InternalLayoutOptions<DiagonalLineLayoutParameters>) {
super(params)

this.distanceBetweenCorners = params.options.distanceBetweenCorners
this.angle = params.options.angleInDegrees * Math.PI / 180
}

/**
* This is an abstract function that subclasses may implement if they wish. It will be called at the beginning of a layout.
* @param toolkit The associated jsPlumbToolkit instance
* @param parameters Parameters configured for the layout.
*/
begin(toolkit:JsPlumbToolkit, parameters:DiagonalLineLayoutParameters) {
this.curX = 0
this.curY = 0
}

/**
* Step through the layout. For some layouts there is only a single step, but others continue stepping until some
* condition is met. Once the condition is met, the subclass must set `this.done = true`, or the layout will
* continue looping indefinitely.
* @param toolkit
* @param parameters
*/
step(toolkit:JsPlumbToolkit, parameters:DiagonalLineLayoutParameters) {
const nodes = toolkit.getNodes()
nodes.forEach(node => {

this.setPosition(node.id, this.curX, this.curY)
const s = this._getSize(node.id) // _getSize is a protected method in AbstractLayout
const dx = Math.cos(this.angle) * this.distanceBetweenCorners
const dy = Math.sin(this.angle) * this.distanceBetweenCorners
this.curX += (s.w + dx)
this.curY += (s.y + dy)
})
}

/**
* This is an abstract function that subclasses may implement if they wish. It will be called at the end of a layout.
* @param toolkit The associated jsPlumbToolkit instance
* @param parameters Parameters configured for the layout.
*/
end (toolkit:JsPlumbToolkit, parameters:DiagonalLineLayoutParameters) {
console.log("End.")
}


}

5. Register your layout.

The final step is to register this layout:

Layouts.register(DiagonalLineLayout.type, DiagonalLineLayout)

It isn't strictly necessary to declare a static type member to do this, of course. But if you do this then you can be type safe when you're declaring this layout:

toolkit.render(someElement, {

...,

layout:{
type: DiagonalLineLayout.type,
options:{
angleInDegrees:67
}
},

...

})

Can I see that in Javascript instead?

Sure!

import { LayoutParameters, JsPlumbToolkit, InternalLayoutOptions, Vertex, Layouts, } from '@jsplumbtoolkit/core';

export class DiagonalLineLayout extends AbstractLayout {

static type = "DiagonalLine"
type = DiagonalLineLayout.type

distanceBetweenCorners
angle

curX
curY

getDefaultParameters() {
return {
distanceBetweenCorners: 150,
angleInDegrees:45
}
}

constructor(params) {
super(params)

this.distanceBetweenCorners = params.options.distanceBetweenCorners
this.angle = params.options.angleInDegrees * Math.PI / 180
}

/**
* This is an abstract function that subclasses may implement if they wish. It will be called at the beginning of a layout.
* @param toolkit The associated jsPlumbToolkit instance
* @param parameters Parameters configured for the layout.
*/
begin(toolkit, parameters) {
this.curX = 0
this.curY = 0
}

/**
* Step through the layout. For some layouts there is only a single step, but others continue stepping until some
* condition is met. Once the condition is met, the subclass must set `this.done = true`, or the layout will
* continue looping indefinitely.
* @param toolkit
* @param parameters
*/
step(toolkit, parameters) {
const nodes = toolkit.getNodes()
nodes.forEach(node => {

this.setPosition(node.id, this.curX, this.curY)
const s = this._getSize(node.id) // _getSize is a protected method in AbstractLayout
const dx = Math.cos(this.angle) * this.distanceBetweenCorners
const dy = Math.sin(this.angle) * this.distanceBetweenCorners
this.curX += (s.w + dx)
this.curY += (s.y + dy)
})

this.done = true // inform the superclass the layout is complete.
}

/**
* This is an abstract function that subclasses may implement if they wish. It will be called at the end of a layout.
* @param toolkit The associated jsPlumbToolkit instance
* @param parameters Parameters configured for the layout.
*/
end (toolkit, parameters) {
console.log("End.")
}


}