Shape Libraries
JsPlumb allows you to use arbitrary HTML/SVG to render the elements in your UI - inside a view, you map some node/group type to a definition that contains either a template, when using JsPlumb Vanilla, some JSX with JsPlumb React, or a component, with JsPlumb Angular, Vue and Svelte.
In some applications - particularly diagramming applications - the only thing that is different between the representations of the vertices in your app is that they are drawn with different shapes. With this type of app, maintaining a separate mapping inside your view for every available shape is not desirable, and that's where a ShapeLibrary
comes in.
A ShapeLibrary
contains one or more sets of named shapes that you will use to render SVG inside the vertices in your application. A ShapeLibrary
is a wrapper around one or more Shape sets - you can read about the available shapes (and how to create your own set) below. The type
of each vertex is used by JsPlumb to look up an appropriate shape to render, and when you have more than one set of shapes, JsPlumb uses each vertex's category
value to pick the set to use for a given shape.
Creating a shape library
You'll first need to create a ShapeLibraryImpl
and pass in a set of shapes to render:
import { FLOWCHART_SHAPES, ShapeLibraryImpl } from "@jsplumbtoolkit/browser-ui"
const shapeLibrary = new ShapeLibraryImpl(FLOWCHART_SHAPES)
You can use this shape library to do a couple of things:
- render SVG elements into your nodes
- create a
ShapeLibraryPalette
from which users can drag new SVG shapes onto your canvas. - export the Surface to an SVG, PNG or JPG
Required data properties
In order for the shape library to be able to pick the appropriate shape for some vertex, the vertex data must contain:
- A
type
property that maps to the ID of one of the shapes in your library - A
category
property that maps to some shape set ID. - A
width
andheight
value for each node. The SVG shapes usepath
elements internally, which require absolute coordinates, and so they need to know the current size of your vertices:
{
"id": "1",
"name": "Some Node",
"type": "process",
"category": "flowchart",
"width": 200,
"height": 200
}
In this example we have these two nodes:
[
{
"id":"1",
"left":50,
"top":50,
"type":"process",
"category":"flowchart",
"width":100,
"height":100,
"fill":"white",
"outline":"black",
"name": "Node 1"
},
{
"id":"2",
"left":150,
"top":250,
"type":"terminus",
"category":"flowchart",
"width":100,
"height":100,
"fill":"white",
"outline":"black",
"name": "Node 2"
}
]
Notice how each of the nodes has a type
, width
and height
property in its data. Also note the fill
and outline
values - these are optional, and can be used to control the, er, fill and outline. If you do not provide values for these you'll get the default SVG behaviour, which is, in most browsers, to use a fill
of "black". You can use CSS to apply a style to every node at once if individual fill and outline is not something you need to control:
.jtk-node svg {
fill:white;
stroke:black;
}
Rendering a shape
In Vanilla JsPlumb, you need to do two things to wire up the shape library to a surface:
- Provide a
shapes
render option that contains a reference to the shape library - Use a
<jtk-shape/>
tag in your vertex template. This is a custom tag that the surface adds when we provide the shape library in its render options.
import { newInstance, ShapeLibraryImpl } from "@jsplumbtoolkit/browser-ui"
const toolkit = newInstance()
const shapeLibrary = new ShapeLibraryImpl(FLOWCHART_SHAPES)
const renderParams = {
shapes:{
library:shapeLibrary
},
view:{
nodes:{
default:{
template:`<div>
<jtk-shape/>
</div>`
}
}
},
consumeRightClick:false
}
toolkit.render(someElement, renderParams)
SVG shape tag
An alternative way to register the tag is manually, with a name you choose. This is only recommended if, for some reason, you don't want to use jtk-shape
in your templates, or if you have more than one shape library. To do this you replace the shapes
entry with tags
:
const renderParams = {
tags:{
"jtk-shape":shapeLibrary.getShapeTagDefinition()
},
layout:{
type:AbsoluteLayout.type
},
etc
Displaying labels
You can request that the shape library render a label in the center of each of your nodes/groups:
To add labels in Vanilla JsPlumb, set the showLabels
property in your shapes
config:
const renderParams = {
shapes:{
library:shapeLibrary,
showLabels:true
}
}
By default, the shape library will look for a data property label
in your vertex data. We can override that and tell JsPlumb to get the label from some other property if we wish:
shapes:{
library:shapeLibrary,
showLabels:true,
labelAttribute:"name"
}
SVG Shape palettes
A common requirement in apps that use a library of shapes is the ability to drag new shapes onto the canvas. JsPlumb offers a simple yet powerful method for configuring this behaviour, in Vanilla JS and when using one of our integrations.
import { newInstance, ShapeLibraryImpl } from "@jsplumbtoolkit/browser-ui"
const toolkit = newInstance()
const shapeLibrary = new ShapeLibraryImpl(FLOWCHART_SHAPES)
const renderParams = {
shapes:{
library:shapeLibrary
},
view:{
nodes:{
default:{
template:`<div>
<jtk-shape/>
</div>`
}
}
},
consumeRightClick:false
}
const surface = toolkit.render(surfaceDOMElement, renderParams)
const palette = new ShapeLibraryPalette({
container:paletteDOMElement,
surface,
shapeLibrary
})
When you attach a shape library palette to a surface, the palette will automatically register the jtk-shape
tag for you: you do not need to include the tags
section in your render params as shown in the examples at the top of this page.
The full list of options for the ShapeLibraryPalette
class are as follows:
Interface ShapeLibraryPaletteOptions
Name | Type | Description |
---|---|---|
allowClickToAdd? | boolean | When in tap mode, allow addition of new vertices simply by clicking, instead of requiring a shape be drawn. (When this is true, the drawing method also still works) |
autoExitDrawMode? | boolean | Defaults to true: when in 'tap' mode and a new group/node has been drawn on the canvas, the surface is set back to pan mode. |
canvasStrokeWidth? | number | Stroke width to use for shapes dropped on canvas. Defaults to 2. |
container | HTMLElement | The element to draw the palette into. |
dataGenerator? | DataGeneratorFunction | Optional data generator to allow you to specify initial data for some element to be dragged. Note that you cannot
override the object's type with this function. The palette will set the new object's type to match the type of
the element that the user is dragging from the palette. |
dragSize? | Size | Optional size to use for dragged elements. |
fill? | string | Optional fill color to use for dragged elements. This should be in RGB format, _not_ a color like 'white' or 'cyan' etc. |
iconSize? | Size | Optional size to use for icons. Defaults to 150x100 pixels. If you provide this but not dragSize this size will
also be used for an icon that is being dragged. |
initialSet? | string | Optional ID of the first set to show, hiding the others. |
mode? | DropManagerMode | Mode to operate in - 'drag' or 'tap'. Defaults to 'drag' (DROP_MANAGER_MODE_DRAG). |
onVertexAdded? | Optional callback to invoke when a new vertex has been added | |
outline? | string | Optional color to use for outline of dragged elements. Should be in RGB format. |
paletteStrokeWidth? | number | Stroke width to use for shapes in palette. Defaults to 1. |
selectAfterDrop? | boolean | When true (which is the default), a newly dropped vertex will be set as the underlying Toolkit's selection. |
shapeLibrary? | ShapeLibrary | The shape library to render. |
showAllMessage? | string | Message to use for the 'show all' option in the shape set drop down when there is more than one set of shapes.
Defaults to Show all . |
showLabels? | boolean | Optionally show each icon's label underneath it |
surface | Surface | Surface to attach the drag/drop to. |
Example - dragging SVG shapes
Here, elements can be dragged from the palette onto the canvas. Note that when you drag an element from the palette it is automatically scaled to match the zoom of the canvas at that time:
Getting notification of a new vertex
You might want to be notified when the user has dragged a new vertex onto the canvas.
As with the SurfaceDropManager
that the ShapeLibraryPalette
wraps, you can provide an onVertexAdded
callback, which will be invoked whenever a new shape was dragged onto the canvas:
const palette = new ShapeLibraryPalette({
container:paletteDOMElement,
surface,
shapeLibrary,
onVertexAdded(vertex, dropTarget) => {
alert(`New vertex ${vertex.id} added`)
}
})
vertex
is the new vertex that was added. In the event that the new vertex was dropped on top of some existing node, dropTarget
will be provided, containing information about the node onto which the new vertex was dropped, as well as its position and size. dropTarget
is of this type:
Try dragging a new shape onto this canvas - we'll pop up an alert giving you its ID:
Displaying Labels
The SVG shape palette will not by default display labels for the shapes it contains, but you can switch that on if you wish:
As with the SurfaceDropManager
that the ShapeLibraryPalette
wraps, you can provide an onVertexAdded
callback, which will be invoked whenever a new shape was dragged onto the canvas:
const palette = new ShapeLibraryPalette({
container:paletteDOMElement,
surface,
shapeLibrary,
showLabels:true
})
Tap mode and vertex drawing
The shape library palette component can be used in conjunction with the Vertex drawing plugin. To do that, you'll need to create the shape library palette in "tap" mode:
import { DROP_MANAGER_MODE_TAP, ShapeLibraryPalette } from "@jsplumbtoolkit/browser-ui"
const palette = new ShapeLibraryPalette({
container:paletteDOMElement,
surface,
shapeLibrary,
mode:DROP_MANAGER_MODE_TAP
})
Tap on an element in the palette to select it, then drag in the canvas to draw a new vertex:
Export to SVG, PNG and JPG
You can export a UI that uses a shape library to SVG, PNG or JPG. See this page for more details.
CSS Classes
Shape sets
A ShapeSet
is a set of named shapes that a ShapeLibrary
can use to render SVG into vertex elements or a palette. JsPlumb currently ships with two sets of shapes - FLOWCHART_SHAPES
and BASIC_SHAPES
, and it is a straightforward process to make your own set.
Structure
This is the basic structure - here we show the header and a few shapes from the flowchart shapes that ship with JsPlumb:
export const FLOWCHART_SHAPES = {
id:"flowchart",
name:"Flowchart",
shapes:{
"process": {
type: "process",
label: "Process",
template: '<svg:rect x="0" y="0" width="{{width}}" height="{{height}}"/>'
},
"decision": {
type: 'decision',
label: "Decision",
template: '<svg:path d="M {{width/2}} 0 L {{width}} {{height/2}} L {{width/2}} {{height}} L 0 {{height/2}} Z"/>'
},
"collate": {
type: "collate",
template: `<svg:g><svg:path d="M 0 0 L {{width}} 0 L {{width/2}} {{height/2}} Z"/>
<svg:path d="M 0 {{height}} L {{width}} {{height}} L {{width/2}} {{height/2}} Z"/>
</svg:g>`
}
}
}
Each shape must have at least a type
and a template
value; label
is optional, and description
is also supported.
Mapping to a shape
To map a vertex to a shape, the vertex's backing data needs to have a type
value and a category
, for instance:
{
"id":"myNode",
"type":"process",
"category":"flowchart"
}
category
is mapped to the ID of some shape set - in the above example, flowchart
. Then type
identifies the specific shape inside that shape set.
Template format
The template
must be in the format supported by JsPlumb's default templating engine, ie. XHTML, with every tag properly closed, and using a namespace prefix for SVG elements. Additionally, the template must have a single root element. Notice, for instance, how in the collate
template we use a group element as the root, and put the other SVG elements inside of that.
Variable interpolation
When a shape library renders a shape, it presents two variables to the template engine:
-
width
When rendering a vertex this value will have been sourced from the vertex's backing data. When rendering a shape in a palette the palette sets a fixed value for this. -
height
When rendering a vertex this value will have been sourced from the vertex's backing data. When rendering a shape in a palette the palette sets a fixed value for this.
The template
you provide is inserted as a child element of an SVG element, whose viewBox
is set to 0 0 width height
. With this arrangement, your SVG will scale seamlessly if the size of a node changes.
Defs
Shape sets support the concept of defs
, which is a wrapper around the defs element in SVG. With defs, you can declare snippets of code in one place and reuse them across your vertices without duplicating the markup. This keeps the DOM size down and is also good when you export to an SVG - you get a smaller file size and better readability.
Configuring defs
defs
are declared in the top level of your shape sets, and then referenced in the standard svg way - with the use
element.
const shapes = {
id: "networkTopology",
name: "NetworkTopology",
defs:{
"jtk-switch":`<svg:svg viewBox="0 0 1024 1024">
<svg:path d="..."/>
</svg:svg>`,
"jtk-cloud":`<svg:svg viewBox="0 0 100 100">
<svg:path d="..."/>
</svg:svg>`,
"jtk-terminal":`<svg:svg viewBox="0 0 32 32">
<svg:path d="..." />
</svg:svg>`
},
shapes: {
"cloud": {
type: "cloud",
template: `<svg:use xlink:href="#jtk-cloud" x="0" y="0" width="60" height="60"/>`,
label:"Cloud"
},
"switch": {
type: "switch",
template: `<svg:use xlink:href="#jtk-switch" x="0" y="0" width="60" height="60"/>`,
label:"Switch"
},
"terminal": {
type: "terminal",
template: `
<svg:g>
<svg:use xlink:href="#jtk-terminal" x="0" y="0" width="60" height="60"/>
<svg:text x="30" y="25" text-anchor="middle" dominant-baseline="middle" stroke-width="0.25px">{{label}}</svg:text>
</svg:g>`,
label:"Terminal"
}
}
}
We've truncated the actual SVG elements for readability's sake, but these are the things to note:
- Each
def
has ansvg
element at its root, with aviewBox
. This is not essential, but in this case our icons came from various different places and had different base sizes. TheviewBox
allows us to map them to the size we want. - Each def has a single root element. This is mandatory. Use an
svg:g
element as the root if you want to group a few elements. - The markup for each def is parsed by JsPlumb's default template engine and therefore must include namespaces and be XHTML, ie. no unclosed tags.
- The IDs of the various defs must be unique in the window, so choose carefully. We typically prefix with a namespace (
jtk-
here).
Available sets
JsPlumb currently ships with two shape sets:
Flowchart
import { FLOWCHART_SHAPES, ShapeLibraryImpl } from "@jsplumbtoolkit/browser-ui"
const myLibrary = new ShapeLibraryImpl(FLOWCHART_SHAPES)
Basic
import { BASIC_SHAPES, ShapeLibraryImpl } from "@jsplumbtoolkit/browser-ui"
const myLibrary = new ShapeLibraryImpl(BASIC_SHAPES)
Custom shape sets
It's straightforward to make your own shape set. Here we have made a set of faces:
const shapes = {
id:"faces",
shapes:{
impassive:{
type:"impassive",
template:`<svg:g>
<svg:circle cx="{{width/2}}" cy="{{height/2}}" r="{{(width/2)}}"/>
<svg:path d="M {{width/4}} {{height*3/4}} L {{width*3/4}} {{height*3/4}}"/>
<svg:circle cx="{{width/4}}" cy="{{height/4}}" r="10"/>
<svg:circle cx="{{width*3/4}}" cy="{{height/4}}" r="10"/>
<svg:circle cx="{{width/2}}" cy="{{height/2}}" r="10"/>
</svg:g>`,
label:"Impassive"
},
pleased:{
type:"pleased",
template:`<svg:g>
<svg:circle cx="{{width/2}}" cy="{{height/2}}" r="{{(width/2)}}"/>
<svg:circle cx="{{width/4}}" cy="{{height/4}}" r="10"/>
<svg:circle cx="{{width*3/4}}" cy="{{height/4}}" r="10"/>
<svg:circle cx="{{width/2}}" cy="{{height/2}}" r="10"/>
<svg:path d="M {{width/4}} {{height*3/4}} C {{width/4}} {{height*7/8}}, {{width*3/4}} {{height*7/8}} {{width*3/4}} {{height*3/4}}"/>
</svg:g>`,
label:"Pleased"
},
notpleased:{
type:"notpleased",
template:`<svg:g>
<svg:circle cx="{{width/2}}" cy="{{height/2}}" r="{{(width/2)}}"/>
<svg:circle cx="{{width/4}}" cy="{{height/4}}" r="10"/>
<svg:circle cx="{{width*3/4}}" cy="{{height/4}}" r="10"/>
<svg:circle cx="{{width/2}}" cy="{{height/2}}" r="10"/>
<svg:path d="M {{width/4}} {{height*3/4}} C {{width/4}} {{height*5/8}}, {{width*3/4}} {{height*5/8}} {{width*3/4}} {{height*3/4}}"/>
</svg:g>`,
label:"Not Pleased"
}
}
}