Skip to main content

26 posts tagged with "png"

View All Tags

· 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 Toolkit, leading alternative to GoJS and JointJS

undefined - jsPlumb Toolkit, leading alternative to GoJS and JointJS

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 Toolkit, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

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.

· 3 min read
Simon Porritt

NEW FUNCTIONALITY

  • The ImageExporter class now supports an optional width or height for the exported image. Width takes precedence; the aspect ratio of the underlying content is always maintained.

  • Added optional onShow and onDimensionsChanged callbacks to ImageExporterUI.

  • Added the ability to ImageExporterUI to specify a list of sizes from which the user should be able to choose for their exported image.


Specifying image export size

You can now programmatically specify the width or height of a PNG or JPG image export, either as an argument to the low-level ImageExporter class, or to the ImageExporterUI.

Here, we've updated our demo from the 6.8.0 release notes to include support for exporting PNG and JPG images. When you export an image from this example it will be exported at the size of the content bounds:

We do this via:

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

const surface = ....

new ImageExporterUI(surface).export()

You're not limited to the size of the content when exporting a PNG or JPG, though - you can provide a desired width or height for the exported image:

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

const surface = ....

new ImageExporterUI(surface).export({width:3000})

If you click an export link above you'll get an image with width 3000 pixels. You can also specify height instead of width if you prefer, but if you specify height and width we'll only use the width you provide - the exporter always maintains the aspect ratio of the original image.

Specifying a set of exportable dimensions

If you'd like your users to be able to pick the dimensions of their exported image you can also do that:

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

const surface = ....

new ImageExporterUI(surface).export({
dimensions:[
{ width:3000 },
{ width:1200 },
{ width:800 }
]
})

The entries in dimensions can have either width or height, but if you supply both we will, as discussed above, only honour the width. When you click an export link in the next code demo, you'll see a dropdown from which you can pick the size of the export:

choosing png or jpg export size for diagram export - jsPlumb Toolkit, leading alternative to GoJS and JointJS

Further reading

You can read up on this in our documentation - https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/svg-png-jpg-export.


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

  • Shape libraries now support defs - snippets of SVG that you can declare in the header of a shape set and reference in the individual templates. This reduces bloat and increases readability. See below for more detail.

UPDATES

  • Improvements to the hierarchy layout's crossing stage to reduce the chance of overlapping edges
  • Improvements to SVG/PNG/JPG export UI
  • Improvements to SVG export output: edges are drawn first in the SVG so that nodes will be placed on top in non-browser settings.

Shape Library Defs

In most UIs there is some amount of markup that is common amongst a number of vertices. For instance, in the network topology diagram below, there are three types of nodes - terminal, cloud and switch, each of which has its own icon representing it:

In version 6.8.0 we have added support for a defs element to shape sets - it is a wrapper for the defs element in SVG. The benefits of using defs are that you can reduce the number of elements in your DOM, and when exporting the SVG the resulting file will be smaller and easier to comprehend.

To configure this, we created a shape set like this:

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"/>`
},
"switch": {
type: "switch",
template: `<svg:use xlink:href="#jtk-switch" x="0" y="0" width="60" height="60"/>`
},
"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>
`
}
}
}

I've truncated the actual SVG elements for readability's sake, but these are the things to note:

  • Each def has an svg element at its root, with a viewBox. This is not essential, but in this case our icons came from various different places and had different base sizes. The viewBox 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 the Toolkit'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).

We then render a surface using this shape library:

import { newInstance, ShapeLibraryImpl, DEFAULT } from "@jsplumbtoolkit/browser-ui"

const shapes = {...}
const sl = new ShapeLibraryImpl([shapes])
const tk = newInstance()

tk.render(container, {
...
shapes:{
library:sl
},
view:{
nodes:{
[DEFAULT]:{
template:`
<div data-jtk-target="true" style="width:60px;height:60px;">
<jtk-shape width="60" height="60"/>
</div>`
}
}
}
})

Further reading

You can read up on this in our documentation - https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/shape-sets.


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

The Toolkit has support for layouts baked in, shipping with a Force Directed layout, Hierarchy layout, Balloon layout and Circular layout, as well as supporting Absolute positioned nodes and a powerful and simple api for creating custom layouts.

Sometimes, though, you just want to make small adjustments to the elements in your UI to neaten things up a bit, and the jsPlumb Toolkit offers a very handy utility for doing so - the magnetizer. It's a collection of tools you can use to nudge elements around.

On demand magnetize

In this first example someone has carelessly left node 2 on top of node 1. Don't worry, though - if you tap on one of the nodes we'll invoke the magnetizer and push the other one away:

We set this up with a tap listener on the node definition in our view:

nodes:{
default:{
template:`<div>{{id}}</div>`,
events:{
[EVENT_TAP]:(p) => {
mySurface.magnetize(p.obj)
}
}
}
}

When you invoke the magnetizer in this way it uses the center of the object you pass in as the focus point, and it automatically excludes the object you passed in from being moved.

After drag magnetize

You can also set this up to happen after a drag - try dragging a node in this next canvas onto another node and see what happens:

This was configured in the `magnetizer` options to our render call:
toolkit.render(someElement, {

...,
magnetize:{
afterDrag:true
}

})

Setting this up on your canvas can be a quick usability win for your users.

In the above example the node that you have just dragged displaces any node(s) it was dragged onto, but you can switch that behaviour around and have the dragged node move if you want:

toolkit.render(someElement, {

...,
magnetize:{
afterDrag:true,
repositionDraggedElement:true
}

})

Constant magnetize

It's also possible to have the magnetizer permanently switched on while dragging. This one's quite fun:

toolkit.render(someElement, {

...,
magnetize:{
constant:true
}

})

Magnetize after layout

Here are some nodes that have been positioned absolute and are overlapping:

[
{ id:"1", left:50, top:50 },
{ id:"2", left:100, top:100 },
{ id:"3", left:450, top:0 },
]

You can switch on the magnetizer so that it runs after a layout has run:

toolkit.render(someElement, {

...,
magnetize:{
afterLayout:true
}

})

This is the same few overlapping nodes rendered with afterLayout:true set in the magnetize options:

Gathering elements

As you've probably noticed, the magnetizer operates as if every element in your canvas has the same magnetic charge, and they therefore repel each other. But what if you want the opposite? You can also use the magnetizer to gather elements - try clicking on an element in this canvas:

This is configured in much the same way as the first example, except we use the gather method instead of magnetize:

nodes:{
default:{
template:`<div>{{id}}</div>`,
events:{
[EVENT_TAP]:(p) => {
mySurface.gather(p.obj)
}
}
}
}

Group expand

Here we see a collapsed group and a node positioned nearby. Tap on the group to toggle its collapsed state - the node will be magnetized out of the way:

toolkit.render(someElement, {

...,
magnetize:{
afterGroupExpand:true
}

})

Group collapse

Here we see a group and a node, which is connected to one of the group's children, positioned at some distance. Try tapping on the group now - you'll see node 3 be gathered in next to it:

toolkit.render(someElement, {

...,
magnetize:{
afterGroupCollapse:true
}

})

afterGroupCollapse and afterGroupExpand can of course be used at the same time.


Summary

The magnetizer is a useful tool for adding an extra level of polish to your applications. If you've got users working on diagrams with a lot of elements, having the canvas automatically nudge elements around can be a real productivity boost.


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.

· 6 min read
Simon Porritt

Snaplines plugin

In version 6.7.0 we released a new plugin - Snaplines - which you can see in action in our Flowchart builder demonstration. In this post we're going to look at a couple of methods offered by the surface component which allowed us to implement snaplines in a matter of hours.

This is a picture of the snaplines plugin in action:

Snaplines - align your flowcharts perfectly - jsPlumb Toolkit - JavaScript diagramming library that fuels exceptional UIs

The internal workings of this plugin are quite simple:

  1. At drag start, record the position of the edges and center of each of the other elements in the canvas
  2. As the user drags an element, check these positions to see if one of the element's edges, or its center, is in proximity to an edge or the center of one or more other elements
  3. When the drag element is in proximity to an edge or center of some other element, draw a snapline.

Steps (1) and (2) are just a bit of maths, and we're not going to cover them here. It's step 3 we're here to talk about today - "draw a snapline". As always with computers, it's easy to say something, but how do you actually do it? Fortunately the Toolkit's surface component makes it straightforward.

Fixing elements

The key to the ease with which snaplines was developed is this method that is available on the surface component:

fixElement(el:Element, position:PointXY, constraints?:{left?:boolean, top?:boolean})

Given some DOM element, fixElement will place it on the surface canvas at position - where position is a point in the coordinate space of the elements of the canvas, and then fix it in one or both axes so that it never disappears from view. That's right: fixElement abstracts away all of the considerations about the current zoom level, or where the user has panned the canvas to, and just plonks down the element where you asked it to. When you subsequently pan and/or zoom the canvas, the position of the fixed element is updated so that it stays where you requested. Great!

Snaplines Example

Click on a node in this example and we'll create a horizontal and vertical line, each one passing through the element's center. Then we'll call fixElement for each line:

surface.fixElement(horizontalLine, 
{
x:p.obj.data.left - (p.obj.data.width * 0.5),
y:p.obj.data.top + (p.obj.data.height / 2)
}
)

surface.fixElement(verticalLine,
{
x:p.obj.data.left + (p.obj.data.width * 0.5),
y:p.obj.data.top - (p.obj.data.height / 2)
}
)

Now when you pan/zoom the canvas, the red cross stays in place where it was fixed. Note that it does not follow the node around - you can drag a node that you previously clicked and the cross will not move. This is by design.

It's important to keep in mind that you can fix arbitrary HTML elements using this method - they don't have to be simple red lines like in this example. You could fix any HTML you like.

Unfixing an element

Notice as you click on new nodes the cross moves? And when you click on whitespace it goes away? There is a companion method - unfixElement - which will remove a fixed element from the canvas. We call this whenever a click occurs on a node or the canvas:

surface.unfixElement(horizontalLine)
surface.unfixElement(verticalLine)

Constraints

You may have noticed the constraints argument to the fixElement method - and also that in both examples above we did not provide a value. The snaplines plugin uses fixElement in the simplest way, but it has some extra tricks: using constraints, you can fix an element to a specific position on the canvas and then if that position ever goes out of the viewport the element will be shifted to ensure it is always visible.

In this next example we have positioned a label at {x:50, y:150} and fixed it so that it never goes into the negative x axis, and we've positioned another label at {x:300, y:20} and fixed it so that it never goes into the negative y axis:

surface.fixElement(myLabel, { x:50, y:150}, {left:true})
surface.fixElement(myOtherLabel, { x:300, y:20}, {top:true})

You can of course fix one element in both axes:

surface.fixElement(myLabel, { x:50, y:150}, {left:true, top:true})

Swim Lanes example

You can use fixed elements to implement sticky headers for swim lanes, which is pretty cool. Try dragging the canvas below to pan it and see how the lane headers stay in place when you pan to the left.

We fix the headers above with this code (this is for the first header):
surface.fixElement(l,{x:50, y:50},  {left:true})

Floating Elements

Another way to decorate your canvas is with floating elements - elements that are positioned at some position relative to the viewport origin, and which do not pan and zoom with the content:

const l = document.createElement("div")
l.innerHTML = "I AM FLOATING"
surface.floatElement(l,{x:10, y:10})

Floating inspector example

In this example we use floatElement to float an HTML element which is configured as an inspector - code is shown below. Click on nodes to inspect them and change their label/background color. Click the canvas to reset the selection.

Our nodes are like this:

{ id:"1", label:"Un", left:150, top:50, bg:"darkslateblue" },

We create the element for the inspector and initialize it like this:

const l = document.createElement("div")
l.className = "floating-inspector"

new VanillaInspector({
container:l,
surface:s,
emptyTemplate:`<span>No selection</span>`,
templateResolver:() => {
return `<div>
<div style="display:flex;align-items:center">
<strong>ID:</strong>
{{id}}
</div>
<strong>Label:</strong>
<input jtk-att="label" jtk-focus/>
<strong>Background:</strong>
<select jtk-att="bg">
<option value="darkslateblue">Dark Blue</option>
<option value="#396a1e">Dark Green</option>
</select>
</div>`
}
})

And then we ask the surface to float the element it is mounted in:

surface.floatElement(l, {x:10, y:10})

Summary

fixElement and floatElement are powerful methods that you can use to bring your apps to a new level. They're tightly integrated with the Toolkit ecosystem and they're DOM agnostic - you can use any markup you like, meaning it's simple to keep things on brand. The examples here only scratch the surface - pun not intended, but I'm leaving it in - of what you can do.


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 writing attributes to the xlink namespace in the vanilla template engine
  • Updated template parsing code to better handle extraneous whitespace.
  • Improvements to the preview view of the svg/png/jpg export code
  • Updated jsdocs for Vue 2 and Vue 3 integration packages
  • Added the ability to register a Surface on Vue 2 and Vue 3 manually - allowing you to use vanilla templating inside of a Vue app.

BREAKING

  • The order of arguments in the fixElement method has been switched from (el:Element, constraints:FixedElementConstraints, pos:PointXY) to (el:Element, pos:PointXY, constraints?:FixedElementConstraints). This better reflects the fact that constraints is an optional argument whereas pos is not.

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

Hello World starter app

We've already got a bunch of great starter apps you can use as a basis for your own, but sometimes you want to start from something a little more bare-bones. So we've added a Hello World starter app - just enough to get you started.

Hello World - jsPlumb Toolkit - JavaScript diagramming library that fuels exceptional UIs

Collapsible Hierarchy demonstration

The Toolkit's real strength lies in how flexible it is. In our new Collapsible Hierarchy demonstration we combine our tried and trusted hierarchy layout with dynamic selection rendering to implement a layout in which you can collapse nodes and prune their descendants.

Collapsible hierarchy Italic language family - jsPlumb Toolkit, industry standard diagramming and rich visual UI Javascript library

The dataset we used for this is the Italic languages. Lookout for a separate post on this demo over the next couple of days.

Initial shape set selection in Shape Palettes

We have added support for the initialShape optional parameter as an argument to the shape library palette component in each of our Angular, React, Vue 2 and Vue 3 integrations. If you have a library with multiple shape sets and you want to initialize the palette showing only one set, this is the flag you need.

multiple svg shape sets for diagramming - jsPlumb Toolkit - JavaScript diagramming library that fuels exceptional UIs

Inspector bugfix

We fixed a small issue in our inspectors: prior to 6.7.1 a change in a <select> element would not be detected dynamically, only when the user committed the inspector.


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.

· 6 min read
Simon Porritt

One question we hear a lot from prospective licensees is how does jsPlumb compare as an alternative to JointJS? The people at JointJS have taken several stabs at summarizing this but the results can at best be described as an exercise in creative writing.

In today's post I'm going to jot down a few thoughts of our own, plus those of the many developers who come to us once they've reached the limit of JointJS's capabilities.

What, then, are the key differentiators between jsPlumb and JointJS? We can think of many, but these are what we consider the key ones:

jsPlumb is not limited to SVG but JointJS is

With jsPlumb you can use any valid HTML or SVG to render the elements in your UI - you are not limited to just SVG, which, while useful for a certain class of UIs such as diagrams, quickly becomes a nightmare to work with when you want something a little more bespoke. This was the topic of a recent post of ours, and it's quite fundamental. If you can build it in JointJS you can, naturally, build it in jsPlumb, because HTML is a superset of SVG in the browser. But you cannot say the opposite: there are plenty of UIs that you can build with jsPlumb that are difficult, if not impossible, to build with JointJS.

So, with jsPlumb you can build a UI in which some of your elements are plain old SVG:

<svg viewBox="0 0 173.2 200" width="80" height="80">
<path fill="forestgreen"
d="M86.6 0L173.2 50L173.2 150L86.6 200L0 150L0 50Z"></path>
</svg>

and some of them are arbitrarily complex HTML:

<div class="demo-html" data-cow="show">
<strong>{{label}}</strong>
<select>
<option value="show">Show cow</option>
<option value="hide">Hide cow</option>
</select>

<input type="text" placeholder="enter label"/>
</div>

Notice the cow? You can hide and show that cow. One great advantage of HTML over SVG nodes is that you can use all the power of CSS. We use it here to hide and show the cow:


.my-node {
background:none;
}

[data-cow='show'] {
background-image: url('/img/351032562.jpg');
background-size: cover;
}

When the user makes a selection in the drop down, we simply update the data-cow attribute on the node element.

We've also put a nice box shadow on that node, via CSS. When you build applications with jsPlumb it's much easier to take advantage of all of the underlying technologies.

jsPlumb integrates with Angular, React, Vue (2/3) and Svelte

This blog is a Docusaurus application, meaning it runs on React, and I can embed React components into the page. For instance, how about this <ShowMeACow/> component here - try clicking this link:

This is a functional React component, and obviously a very useful one. The source is:

import React, {useState } from "react"

export default function ShowMeACow({size, startShown}) {

const s = size || 450
const [showCow, setShowCow] = useState(startShown === true)

return <div className="show-me-a-cow">
<button onClick={() => setShowCow(!showCow)}>{showCow ? "Hide that" : "Show me a"} cow</button>
{showCow && <img src="/img/351032562.jpg" width={s}/>}
</div>
}

So what do I mean when I say jsPlumb integrates with React? I mean I can just use the Toolkit's React integration and put that ShowMeACow component right inside a node:


I wrote a component:

const CowComponent = function({ctx}) {
return <div className="show-me-a-cow-demo-node">
<ShowMeACow size={150} startShown={ctx.vertex.data.cowShown}/>
</div>
}

and then I mapped it inside my Toolkit view:

const view = {
nodes: {
default: {
jsx: (ctx) => <CowComponent ctx={ctx}/>
}
}
}

And then I used the JsPlumbToolkitSurfaceComponent from our React integration to draw that canvas above. In one of my nodes I set cowShown:true and in the other one I didn't.

It is not possible to do these things with JointJS, and whilst this charming cow is perhaps a spurious example, it's not hard to see how powerfully rich your UI can be when you can integrate to this level. We offer the same level of support for Angular, Vue 2, Vue 3 and Svelte.

jsPlumb has no external dependencies. JointJS has 3. And more if you want touch events.

To get JointJS to do anything at all you have to include 3 external dependencies:

  • jQuery
  • Backbone
  • Lodash

These are all quite antiquated technologies now, and they do not come from the modern reactive world. The last lodash release was in 2016!

jsPlumb has no external dependencies: everything is tightly integrated, and if we wanted to update some core piece of our library we wouldn't have to wait for an external entity to do it.

This dependency on external libraries in JointJS actually extends to support touch events, surprisingly. In the Touch Gestures demo, the question is asked "Have you been wondering how to implement touch gestures such as pan and pinch in your JointJS application?", and the answer given is "use this third party library". I mean obviously I paraphrased that, but I would do that, since I am here representing jsPlumb, which has no external dependencies and also the smoothest pinch to zoom for miles around.

Summary

Those are 3 key differences between JointJS and JsPlumb, and I of course consider each one of them on its own to be sufficient reason to choose jsPlumb. If you can build it with JointJS you can build it with jsPlumb - faster, and cheaper, probably.

Here's a matrix of our own:

JointJS+jsPlumb
Integration with Angular, React, Vue2, Vue 3 and Svelte
Depends on multiple external libraries
Supports touch events
Access to unminified source code
No restrictions on use of license
Graph Layout Algorithms
Dedicated support available
Export to SVG, PNG and JPG
Full text search
Dialogs module
Transactions
Render dynamic selections

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.