Skip to main content

· One min read
Simon Porritt

Updates

  • Fixed issue with path editor incorrectly trying to edit an edge that had been aborted by a beforeStart interceptor.

Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· One min read
Simon Porritt

Updates

  • Fixed an issue with the surface's destroy method when a background image was in use.
  • Added autoSaveType option to Surface, allowing you to specify which exporter to use when auto save is switched on
  • Added optional type to AutoSave constructor, allowing you to specify which exporter to use when saving.

Breaking changes

  • The exportData method no longer writes a ports section into the output. You can use legacy-json as the type when exporting data if you still need this, but in 7.x that will be removed.

Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 4 min read
Simon Porritt

Version 6.11.0 has some nice new functionality.

Nested Elastic Groups

We've continued the work that we began in 6.10.0 on elastic groups, by adding support for nested elastic groups:

nested elastic group resizing - JsPlumb, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

When an elastic group is inside some other elastic group and the user is dragging a child element, they are shown a preview of how all the groups will resize as a result of the drag. In the gif above we show two levels of nested groups but this works to an arbitrary depth...it's turtles all the way down.

You can read more about elastic groups in our documentation.

Play around with our groups starter app to get a feel for how elastic groups sizing works.

Exporting a selection or path as SVG/PNG/JPG

You can now export a selection, path, or the current selection for some Toolkit instance, to an SVG/PNG/JPG:

exporting a selection to SVG - JsPlumb - Angular, React, Vue, Svelte diagramming library

Grid Layout

We've added a handy new layout in 6.11.0 - the GridLayout:

nested elastic group resizing - JsPlumb - Angular, React, Vue, Svelte diagramming library

A number of different alignment options are available, and you can optionally fix the number of rows or columns the grid will use. Documentation for this layout is available here.

Ad-hoc group layouts

You can now run an ad-hoc layout on a group:


someSurface.adHocGroupLayout(aGroup, {
type:GridLayout.type,
options:{
horizontalAlignment:GridLayoutHorizontalAlignments.right
}
})

Vertex sizes in model

One of our most popular starter apps is the Flowchart Builder, in which users can resize elements. This width/height information is stored in the model, and prior to 6.11.0 had to be explicitly referenced by your vertex templates:

<div style="width:{{width}}px;height:{{height}}px">{{label}}</div>

In 6.11.0 we have introduced a useModelForSizes flag, along with a bunch of default size settings, which allows you to do away with the style shown in the template above - the Toolkit will take care of the size:


const surface = myToolkit.render(someElement, {
...,
useModelForSizes:true,
...
})

Now our template can just focus on what it needs to focus on:

<div>{{label}}</div>

When a given vertex does not have width/height information, it can be supplied either from a node/group definition in your view, or from instance defaults. If all else fails and you haven't provided a value anywhere, the Toolkit will use something it considers reasonable.

This is discussed in detail in our docs.


Changelog

New functionality

  • Added support for exporting a Path or Selection (including the current selection for some Toolkit instance) to an SVG/PNG/JPG.

  • New layout GridLayout added. This layout places elements into a grid, optionally with a fixed number of rows or columns, and with the ability to align items to the left/top/bottom/right of each cell.

  • Group size preview pane shown when dragging an element inside an elastic group is now displayed on ancestors of the group if they are also elastic.

  • Added the ability to run an ad-hoc layout on a group, via the adHocGroupLayout method on a surface.

  • Support for setting node/group sizes from the model has been added, including support for specifying default node/group sizes in the event that the vertex data does not contain size information. Read more about this in our docs - https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/nodes-and-groups#setting-nodegroup-size

Updates

  • Internal updates to React integration to improve performance and reduce code duplication.
  • Refactoring of various positioning methods to remove duplicated code
  • Improvements to group auto sizing when nested groups are expanded or collapsed.

Breaking changes

  • React integration no longer supports setting asynchronous to false on the JsPlumbToolkitSurfaceComponent. Asynchronous is mandated.

Further reading


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 2 min read
Simon Porritt

We've had a few queries recently regarding the possibility of adding support for animated edges to the Toolkit. Like this:

We've never exposed an API to do this because it's never really occurred to us do so, given that achieving this effect is about as easy as falling off a log. There are three parts to this:

1. Define the CSS animation

Define a CSS animation with a single keyframe that sets the dash offset:

@keyframes dashdraw {
0% {
stroke-dashoffset:10;
}
}

2. Map the animation to a CSS rule

.animated-edge {
stroke-dasharray:5;
animation: dashdraw .5s linear infinite;
}

3. Use the cssClass property in an edge definition to map to this class

toolkit.render(someContainer, {
...,
view:{
edges:{
animated:{
cssClass:"animated-edge"
}
}
},
...
})
Animated Edges

That's it! That's all you need to do. As easy as falling off a log.

Library support

We tend to avoid polluting the Toolkit with functionality that is easily created such as this is, but if you're a licensee or evaluator of the Toolkit - or just someone with an opinion - and you've got suggestions for how we could usefully build support for this into the library, we'd definitely like to hear them.


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 5 min read
Simon Porritt

6.10.0 is a significant release for the Toolkit, with a great new "elastic groups" feature, plus an overhaul of how group sizing works in general. In addition we've added a few new useful things such as the ability to enable/disable the snaplines plugin, the ability to provide a style block for an SVG export, and a bunch of other things.

Under the hood we continue to refactor the codebase to make it faster and more streamlined, as we steadily approach our 7.x release, and we've put in a couple of minor updates to ensure the Toolkit works with SvelteKit SSR.

Elastic Groups

We are pleased to announce that we now support "elastic" group sizing:

elastic group resizing - JsPlumb, industry standard diagramming and rich visual UI Javascript library

As users drag child elements around inside a group, the size of the child content is computed and a frame appears showing the user how the group's size/position will change on mouseup. If you have a minSize or maxSize set on a group the elastic resizer will take that into account (in the video below the group has a minimum size of 250px in each axis).

You can read more about elastic groups in our documentation.

Play around with our groups starter app to get a feel for how elastic groups sizing works.

Snaplines Plugin

A new method setEnabled(boolean) was added to the Snaplines plugin, allowing you to switch it on and off during the lifecycle of your app.

elastic group resizing - JsPlumb, build diagrams and rich visual UIs fast

Svelte Kit SSR

From 6.10.0 onwards the Toolkit is fully compatible with Svelte Kit SSR.

SVG export styles

We've added the capability to the SVG exporter to take a styles parameter, either as a string or a JS object, which is then injected into the SVG that we export:


import { SvgExporter } from "@jsplumbtoolkit/browser-ui"

const exporter = new SvgExporter(surface)

const svg = exporter.export({
style:`@font-face {
font-family: 'FiraSans';
font-style: normal;
font-weight: 400;
src:url(https://fonts.gstatic.com/s/firasans/v15/va9E4kDNxMZdWfMOD5Vvl4jO.ttf)
}
text {
font-family: FiraSans;
fill: black;
}`
})
import { SvgExporter } from "@jsplumbtoolkit/browser-ui"
const svgExport = new SvgExporter(surface).export({
style: {
"@font-face": {
"font-family": "'FiraSans'",
"font-style": "normal",
"font-weight": 400,
"src": "url(https://fonts.gstatic.com/s/firasans/v15/va9E4kDNxMZdWfMOD5Vvl4jO.ttf)"
},
"text": {
"font-family": "FiraSans",
"fill": "black"
}
}
})

Changelog

New functionality

  • New flag elastic available for group rendering. An elastic group shrinks and grows as the nodes/groups inside of it are dragged around.

  • Added new panFilter optional function to Surface options, allowing you to control at runtime whether or not panning should be enabled.

  • Added support for optional minSize parameter in group definitions in the view. This is of type Size.

  • Added support for optional padding parameter in group definitions in the view. When autoSize is set on a group, this padding will be set around the child nodes/groups of an auto sized group. Default value is 0.

  • Added support for optional allowShrinkFromOrigin flag on group definitions in the view. Implicitly true when autoShrink or autoSize is true, you can set this to default if you don't want your groups to be able to shrink from the left/top edge.

  • When dragging a child element from a group, the parent group is assigned a CSS class of .jtk-drag-original-group. You can attach a z-index rule for this class to ensure that nodes/groups you drag from a given group will appear on top of any target group.

  • Added setEnabled(boolean) method to the Snaplines plugin, allowing you to enable/disable the plugin at runtime.

  • Added support for passing in styles to SVG exporter, for inclusion in a style element in the SVG export.

Updates

  • Improvements to orthogonal routing to handle vertical alignment in Hierarchy layout

  • Updates to the browser UI core code to fix a couple of issues that were causing the code to fail to load in SvelteKit SSR.

  • The behaviour of autoShrink and autoSize has been improved to support shrinking a group from its left/top edge where applicable. Previous versions of the Toolkit could only shrink a group from its right/bottom edges.

  • Fixed an issue with group auto size when an element is dragged into the negative axis. Previous versions of the Toolkit would size the group to fit the new content bounds but leave the dragged element hanging out of the group. In 6.10.0 the group is shifted to adjust for this.

  • Several improvements were made to the interaction between a Toolkit instance and its associated undo/redo manager, to ensure that not stray commands are tracked by the undo/redo manager during data load/append.

Breaking changes

  • The maxSize option to group definitions is now a Size object, with a w and h property, instead of an array of [w,h].

  • autoSize switches on autoShrink by default now. Consider just using autoGrow if this is not what you want.

  • When autoSize or autoShrink is set, groups may now adjust their left/top edges to fit the content (taking into account any minSize set on a group). You can set allowShrinkFromOrigin:false to suppress this behaviour.

  • When dragging an element that is a child of a group, the original group is now assigned .jtk-drag-active and .jtk-drag-hover CSS classes, where in previous versions it was not. We think this provides a better experience for users but if you prefer the old behaviour you can use the fact that the .jtk-drag-original-group class is now added to the parent group to setup rules to mimic the old behaviour.


Further reading


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· One min read
Simon Porritt

UPDATES

6.9.1 is a minor release but also fairly significant - we've made a few key changes in the React integration so that it fully supports being used in NextJS applications:

  • Updates to React integration to better support NextJS dynamic unload/reload
  • Updated the wheel listener to check for existence of document before testing for available event (SSR fix for NextJS)

In conjunction, we've pushed NextJS versions of three of our most popular starter apps:

Flowchart Builder

flowchart builder - JsPlumb - JavaScript and Typescript diagramming library that fuels exceptional UIs

Chatbot Builder

chatbot builder - JsPlumb, industry standard diagramming and rich visual UI Javascript library

Schema Builder

database schema builder - JsPlumb, build diagrams and rich visual UIs fast


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 3 min read
Simon Porritt

NEW FUNCTIONALITY

  • Support for generating edge routing information was added to the Hierarchy layout.

  • New plugin edgeRouting added. This plugin can ingest the edge routing information generated by a Hierarchy layout and adjust the routes of the edges in the UI, with support for separated orthogonal, bus orthogonal and direct line modes.

  • Support for the number input type was added to Dialogs

  • Support for the number input type was added to Inspectors

UPDATES

  • A fix was added to the Orthogonal connector's geometry import routine to catch an unexpected formatting error in the geometry.

  • We've made some internal updates to reduce the number of classes created by connectors and connections, instead using more plain old javascript objects.

BREAKING

  • The Community ingest code has been removed. The Community and Toolkit editions in fact diverged several versions back and this code was likely not working in many scenarios. No Toolkit licensees use(d) this code.

  • Constant TYPE_CONNECTOR_SEGMENTED was renamed to CONNECTOR_TYPE_SEGMENTED


Edge routing

Edge routing is a feature that people have been asking about for a while, and we're introducing it in stages, starting with support built in to the Hierarchy layout.

To setup edge routing you need to instruct your Hierarchy layout to generate routing information, and you need to configure the new edge routing plugin:

toolkit.render(someElement, {
...,
layout:{
type:HierarchyLayout.type,
options:{
generateRouting:true
}
},
plugins:[
{
type:EdgeRoutingPlugin.type,
options:{
mode:'orthogonal' or 'direct',
orthogonalMode:'bus' or 'separate'
}
}
]
})

Orthogonal routing

Orthogonal routing is the practise of organising an edge into a series of horizontal and vertical segments.

Separate routes per edge

In the orthogonal routing mode, lines are stacked across the channels in the layout to keep them separate:

Hierarchy layout with orthogonal edge routing - JsPlumb, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

Bus routing

In the bus mode for orthogonal routing, lines are grouped together instead of being stacked individually:

Hierarchy layout with orthogonal edge bus routing - JsPlumb - JavaScript and Typescript diagramming library that fuels exceptional UIs

Direct routing

Here we see the same dataset rendered in direct mode - we still assign a separate location on each vertex for each edge, but we do not route the edges in an orthogonal way:

Hierarchy layout with direct edge routing - JsPlumb, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

You can see all of these in action in the Hierarchy layout demonstration, and to find out how to setup edge routing in your own apps, checkout the edge routing plugin page in our documentation.


Number inputs

We've added support for inputs of type number to both our dialogs and inspectors. The inspector will respond to change events caused by the user clicking on the increment/decrement buttons, enter keypress events, and blur events.

You can see number inputs in use in our Image Processor starter app, for some of the various transformations and filters:

number input on invert filter - JsPlumb, industry standard diagramming and rich visual UI Javascript library


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 11 min read
Simon Porritt

jsPlumb and HTML canvas go back a long way, right to the start, in fact. We got our initial inspiration from Yahoo Pipes, making use of the fact that you can draw bezier curves into canvas elements. Over time the canvas element has become more and more powerful, and it can now do some pretty fancy image processing tasks. For our image processor starter app we wrote a bunch of methods to perform various filtering and transformation operations, collating them from various corners of the internet, and so we thought maybe it would be useful for others if we documented them.

We've released these functions on NPM as an MIT licensed library - take a look at the project page on Github for installation instructions.

Super fast canvas primer

If you're completely new to HTML canvas then there are few things you need to know:

  • a canvas has a width and height and it's definitely best to set these as attributes on the element rather than relying on CSS.
  • all operations on a canvas are not executed on the canvas itself but rather on a context, of which there are a few types, but the one you'll use for basic drawing is '2d'.

You can get a canvas context like this:

const canvas = document.getElementById("myCanvas")
const ctx = canvas.getContext("2d")

You'll see this call throughout the code for our image processor. There's a good primer on the canvas element on MDN.

Image Processing

Two main types of operations when processing images are:

  • Filtering A filter takes an input image and some optional parameters and returns an output image - things such as converting an image to grayscale, inverting the pixel values, adjusting contrast/brightness, etc.

  • Transformation Transformations take one or more images and some parameters and return an output image - things such as blending images, overlaying one image onto another, cropping, resizing, etc.

There are plenty of other parts to an image processor, of course - getting images in and out being not the least of them, but in this article we're going to focus on filters and transformations, and how to do these things with an HTML canvas.

Converting to and from canvas

You'll see references in the code snippets below to a method called canvasToImage. It's responsible for converting the contents of some canvas into an Image.

export async function canvasToImage(canvas:HTMLCanvasElement, type?:string, quality?:number):Promise<HTMLImageElement> {

return new Promise(function(resolve, reject) {

const d = canvas.toDataURL(type, quality)
const oo = new Image()
oo.onload = function () {
resolve(oo)
}

oo.src = d
})
}

type is a string such as "image/png" or "image/jpeg". Theoretically the supported values for this argument are browser specific but nowadays all browsers support PNG and JPEG, and PNG is always the default (ie. if you do not provide a value). quality is used when you're exporting a JPG.

Filtering with an HTML canvas

The code snippets presented here come from the open source canvas image processing library we recently released. The basic approach when filtering with a canvas is:


// 1. get the context from the canvas
const ctx = canvas.getContext("2d")
// 2. apply a filter
ctx.filter = "invert(50%)"
// 3. draw into it
ctx.drawImage(someImage, 0, 0)

Grayscale

A grayscale filter desaturates an image.


async function filterGrayscale(image:HTMLImageElement, amount?:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `grayscale(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

Sepia

Apply a sepia effect to an image. amount determines the depth of the effect.


async function filterSepia(image:HTMLImageElement, amount:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `sepia(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

Invert


async function filterInvert(image:HTMLImageElement, amount:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `invert(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

Saturate

Takes a value with a minimum of 0 and no specific maximum, and adjusts the saturation of the image. Between 0 and 100, the saturate filter works as an inverse of the grayscale filter. Values of greater than 100 will oversaturate the image.


async function filterSaturate(image:HTMLImageElement, amount:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `saturate(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

This cow on the right is 250% saturated.

Opacity

Adjusts the opacity of an image. Valid values for amount are 0-100, inclusive.


async function filterOpacity(image:HTMLImageElement, amount:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `opacity(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

Blur

Blurs an image, using a radius of some number of pixels.


async function filterBlur(image:HTMLImageElement, radius:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `blur(${radius}px)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

This cow on the right has been blurred with a radius of 10 pixels (the default in the canvas image processing library):

Blur gets out of hand pretty quickly. Here's a 75 pixel blurred cow:

Of course the effect of the blur depends on the size of the image.

Brightness

Adjusts the brightness of an image. Valid values for amount are anything from 0 up. 100 means standard brightness. 200 - what you see below - is pretty bright.


async function filterBrightness(image:HTMLImageElement, amount:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `brightness(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

Contrast

Adjusts the contrast of an image. Valid values for amount are anything from 0 up. A value of 0 will give you a completely black image; 100 is normal. The default in our canvas processing library for this operation is 200...but that's really just to have a default.


async function filterContrast(image:HTMLImageElement, amount:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.filter = `constrast(${amount}%)`
ctx1.drawImage(image, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

A cow with 200 contrast:


Transforming images with an HTML canvas

Transformations are a little more complex than filters, and may involve one or more images, as well as other arbitrary input values. The power of HTML canvas, though, makes many of these operations very straightforward.

Mirroring an image

Also known as 'flip', if you're a GIMP user like me. Once again you can use a canvas for this:

async function mirrorImage(img:HTMLImageElement, axis:'x' | 'y' | 'x_y'):Promise<HTMLImageElement> {

// get a canvas, set its dimensions to match the image, and get the 2d context
const outputImage = document.createElement("canvas")
outputImage.width = img.naturalWidth
outputImage.height = img.naturalHeight
const ctx = outputImage.getContext("2d");

// setup transformation values for x and y. This method supports flipping in x, y, or both x _and_ y. We use
// the `scale` property of the canvas context to set this up: if it should be flipped in a specific axis, we use a value of
// -1 for that axis, otherwise we use a value of 1.
const dx = axis === 'y' ? 1 : -1,
x = axis === 'y' ? 0 : -outputImage.width

const dy = axis === 'x' ? 1 : -1,
y = axis === 'x' ? 0 : -outputImage.height

// apply the scale
ctx.scale(dx, dy);

// Draw the image on the canvas
ctx.drawImage(img, x, y);

return canvasToImage(outputImage)
}

This cow looks pretty much the same flipped horizontally:

Maybe that's why it's so appealing, that whole thing about symmetry. Just to show you the mirror image is actually working let's flip the cow in both axes:

Applying a threshold

Out of all the operations discussed in this article, this is the one thing we couldn't do just with some canvas tricks. To apply a threshold to an image we have to get in and mess about with its data:

async function imageThreshold(image:HTMLImageElement, threshold:number, value:number = 255):Promise<HTMLImageElement> {

// get a canvas, set its dimensions, and draw the image in.
const outputImage = document.createElement("canvas");
const ctx1 = outputImage.getContext("2d")
outputImage.width = image.naturalWidth;
outputImage.height = image.naturalHeight;
ctx1.drawImage(image, 0, 0)

// extract the underlying image data
const data1 = ctx1.getImageData(0,0, image.width, image.height)

// each pixel in the image data in a canvas consists of 4 bytes: red, green blue and alpha values.
// we loop through and check each value. Where the pixel is above the threshold we clamp it the value we want. otherwise
// we set it to zero.
for(let i = 0; i < data1.data.length; i += 4) {

const red = data1.data[i]
const green = data1.data[i + 1]
const blue = data1.data[i + 2]

data1.data[i] = red >= threshold ? value : 0
data1.data[i+1] = green >= threshold ? value : 0
data1.data[i+2] = blue>= threshold ? value : 0

}
ctx1.putImageData(data1, 0, 0)
return canvasToImage(outputImage)
}

This is a cow with a threshold of 120 applied to it (and the default of 255 for value):

This is the same threshold but with a value of 120:

Cropping an image

Canvas makes it easy to crop an image:

async function cropImage(image:HTMLImageElement, x:number, y:number, w:number, h:number):Promise<HTMLImageElement> {

// get a canvas and set its dimensions
const c1 = document.createElement("canvas")
c1.width = w
c1.height = h
const ctx1 = c1.getContext("2d")

// draw the image in to the canvas, starting at origin {x,y} on the image, and using the given width and height
ctx1.drawImage(image, x, y, w, h, 0, 0, w, h)
return canvasToImage(c1)
}

Overlaying an image

To overlay an image you just draw both images, one after the other:

async function overlayImage(image1:HTMLImageElement, image2:HTMLImageElement, x?:number, y?:number):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
c1.width = image1.naturalWidth
c1.height = image1.naturalHeight
const ctx1 = c1.getContext("2d")

ctx1.drawImage(image1, 0, 0, c1.width, c1.height)
ctx1.drawImage(image2, x || 0, y || 0, image2.naturalWidth, image2.naturalHeight)

return canvasToImage(c1)
}

Clipping an image

Clipping an image is kind of like overlaying an image - you draw both images into the canvas, but with 2 differences:

  • you draw the mask first
  • you set the globalCompositeOperation to source-in before you draw the image you wish to clip
async function clipImage(image:HTMLImageElement, mask:HTMLImageElement):Promise<HTMLImageElement> {

const c1 = document.createElement("canvas")
const ctx1 = c1.getContext("2d")
c1.width = image.naturalWidth
c1.height = image.naturalHeight

ctx1.drawImage(mask, 0, 0)

ctx1.globalCompositeOperation = "source-in"
ctx1.drawImage(image, 0, 0)

return canvasToImage(c1)
}

Blending images

This is also like overlaying images and clipping images, and it all comes down to the globalCompositeOperation. A good resource for finding out what you can do with the globalCompositeOperation is the MDN documentation.

async function blendImages(image1:HTMLImageElement, image2:HTMLImageElement, blendMode?:GlobalCompositeOperation):Promise<HTMLImageElement> {

const c1 = document.createElement(TAG_CANVAS)
c1.width = image1.naturalWidth
c1.height = image1.naturalHeight

const ctx1 = c1.getContext("2d")
ctx1.drawImage(image1, 0, 0, c1.width, c1.height)

ctx1.globalCompositeOperation = blendMode || GLOBAL_COMPOSITE_MULTIPLY
ctx1.drawImage(image2, 0, 0, c1.width, c1.height)

return canvasToImage(c1)
}

Here, instead of overlaying or clipping our cow and its sunglasses, we'll blend them, using a multiply operation (since that's the default in the jsPlumb canvas image processor that were using):

Blend modes are hours of fun. Here's screen:

This is hard-light:


Further Reading

The key to many of the transformation and filtering operations with an HTML canvas is the globalCompositeOperation, which sets the type of compositing operation that a canvas will use when new content is drawn into it. It's an extremely powerful and versatile concept and if you're planning on seriously getting into image processing in webapps it's well worth your time looking into what it can be used for. Another good article on MDN that talks about this concept is this one about clipping - which we'll discuss a little later in this article.

To mess around in a live setting with these filters and transforms, try our Image Processor starter app

You can download/browse the source of the image processing helpers we created on Github.


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 5 min read
Simon Porritt

What are ports?

Ports are a fundamental part of the Toolkit's data model. The best analogy from the computing world for the Toolkit's concept of ports is that of TCP ports: they are a unique identifier on some vertex that can act as the source and/or target of one or more edges.

Ports are useful when you need granularity in your data model. We use them in several of our starter apps and demonstrations. For instance, in the Schema builder, a table is represented as a node in the data model, and each of the columns in the table is represented as a port on the table vertex. You can see this in the image on the left below. Another example - the purple node on the right below - is from the Chatbot builder. This is a "choice" node, where the path followed depends on which option from a set of choices is selected - each choice (here the choices are PIN or Password) is modelled as a port on the choice vertex.

undefined - JsPlumb, leading alternative to GoJS and JointJS

undefined - JsPlumb, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

So, to take the chatbot as an example, in the data model we'd have something like this as an output of the image above:

edges:[
{
"source":"selectLogin.PIN",
"target":"pinSelected"
},
{
"source":"selectLogin.Password",
"target":"passwordSelected"
}
]

I was prompted to write this article after receiving some feedback that there is "no demo with ports" on our homepage. As we can see from the above two examples, that is not the case, but the UI in the two apps shown above does not make the presence of ports as explicit as in some other demos you'll see kicking around the internet.

Logical ports

As with TCP ports, a port in the Toolkit is at the very least a logical concept: any edges connected to that port will be shown to be connected to the port's vertex in the UI, in the absence of a specific DOM element representing the port. In the two apps shown above, though, we do have a DOM element representing each port, because we clearly want to show edges connected to the ports and not to their parent.

Physical ports

By "physical" we mean we've written out a DOM element to represent each port, and the Toolkit has a powerful declarative mechanism for doing so, using a set of data-jtk- attributes. Here is the markup (slightly abridged for clarity) for the table node from the schema builder:

<div class="jtk-schema-table">
<span>{{name}}</span>
<div class="jtk-schema-columns">
<r-each in="columns" key="id">
<div class="jtk-schema-table-column" data-type="{{datatype}}"
data-primary-key="{{primaryKey}}"
data-jtk-port="{{id}}" data-jtk-scope="{{datatype}}"
data-jtk-source="true" data-jtk-target="true">
<div><span>{{name}}</span></div>
</div>
</r-each>
</div>
</div>

and for the choice node in the chatbot builder:

<div class="jtk-chatbot-choice" data-jtk-target="true">
<div class="jtk-delete"></div>
{{message}}
<div class="jtk-choice-add"></div>
<r-each in="choices" key="id">
<div class="jtk-chatbot-choice-option" data-jtk-source="true"
data-jtk-port-type="choice" data-jtk-port="{{id}}">
{{label}}
<div class="jtk-choice-delete"></div>
</div>
</r-each>
</div>

The Toolkit allows you to use any HTML you like to render DOM elements that represent ports. The data-jtk- attributes, discussed in detail on this page in our docs give you a powerful decoupled mechanism to tell the Toolkit about what parts of your data model the various DOM elements represent.

Rendering ports

So as I said before, I was prompted to write this post to clear up a misunderstanding about whether or not the Toolkit could render ports. I think perhaps there was just nothing that looked like ports. Here, I've embedded a cut-down version of the original chatbot builder, without any of the extra stuff like drag/drop, the inspector, etc. It all still works - try clicking the plus button on the Select logon method node, for example, to add a new port to that node:

And here, I've made a few minor changes to the markup and the CSS, and suddenly our app has a whole new skin (try clicking the plus button again!):

The changes really were very minor. For example the new markup for a choice component is:

<div class="jtk-chatbot-choice jtk-chatbot-2" data-jtk-target="true" data-jtk-port-type="target">
<div class="jtk-delete"></div>
{{message}}
<div class="jtk-choice-add"></div>
<div class="jtk-chatbot-choices">
<r-each in="choices" key="id">
<div class="jtk-chatbot-choice-option" data-jtk-source="true"
data-jtk-port-type="choice" data-jtk-port="{{id}}">
{{label}}
<div class="jtk-choice-delete"></div>
</div>
</r-each>
</div>
</div>

The difference being that I added a <div class="jtk-chatbot-choices"> wrapper around the individual ports; this is styled as a flex row in the CSS and positioned to hang off the bottom of the element.

I also updated the connector type (to Orthogonal) and anchor specification for port elements. Where in the original app we have this anchor for the ports:

anchor:[AnchorLocations.Left, AnchorLocations.Right ]

meaning edges connected to that port can switch from the left to the right depending on where the other end of the edge is connected, in the new version we have this:

anchor:AnchorLocations.Bottom

meaning that an edge connected to one of these ports is fixed to the port's bottom edge.


Further reading

This article only touches on what is a big topic. If you want to delve more into what the Toolkit offers, some good articles to take a look at might be:


Summary

I hope this post gives you a taster for the flexibility and power that the Toolkit's rendering offers. I can surface the ports in my data model to the user in unlimited ways, since it's all just HTML markup. In this case, with a small markup change, a few CSS updates, and one changed line of code I was able to completely alter the appearance and layout of our Chatbot app.


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

· 3 min read
Simon Porritt

NEW FUNCTIONALITY

  • Added support for defaultSize to node/group definitions. Allows you to fix the size for a given vertex type without requiring that the values be in the vertex's backing data.

  • Added support for nodeSize to UI defaults. This is a fallback for the case that your vertices need to be rendered with a specific width/height but the values are not necessarily going to be available in the backing data.

UPDATES

  • Fixed an issue with loading hierarchical-json over ajax - the content type was not correctly recognised and the data not parsed to JSON.

Default node size

When you're creating a diagram builder with the Toolkit chances are you'll be using SVG for your elements. You don't have to use SVG, of course - we've written recently about how cool it is that jsPlumb can render to both HTML and SVG elements - but many times SVG is the element of choice in this situation.

We've been working on a network topology diagram recently with one of our evaluators:

svg elements in a network topology diagram - JsPlumb, leading alternative to GoJS and JointJS

We use a shape library to render the nodes in this app, and we use SVG. This means that we need to have some concept of the width and height of each node, as you can't write out SVG without this information (if you use HTML to render your nodes you can rely on the DOM to figure out the size for you!).

Rather than force our evaluator friend to insert width and height into their dataset - since they already know the size of each element - we added a couple of convenient options to the Surface.

Default size in a node/group definition

In your view you can provide a default size to use for vertices of some specific type:

toolkit.render(someContainer, {
...
view:{
nodes:{
default:{
template:`<div style="width:{{width}}px;height:{{height}}px">
{{label}}
<jtk-shape/>
</div>`,
defaultSize:{w:50, h:50 }
}
}
},
...
})

Why not just write width and height onto the root element of the template? Because the jtk-shape tag needs to know about it - it's the one writing out the SVG.

Default size in surface defaults

If you don't need to be so granular as to provide a default size per type, you can just set it in the defaults for your surface:

toolkit.render(someContainer, {
...
view:{
nodes:{
default:{
template:`<div style="width:{{width}}px;height:{{height}}px">
{{label}}
<jtk-shape/>
</div>`
}
}
},
defaults:{
nodeSize:{w:50, h:50 }
},
...
})

Summary

We're continually striving to improve the usefulness and feature set of the Toolkit, and we think these new additions will really help, particularly when you're making something like we are here - a network topology diagram - and you know your element sizes are fixed.


Start a free trial

Not a user of jsPlumb but thinking of checking it out? There's a whole lot more to discover and it's a great time to get started!


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.