Skip to main content

20 posts tagged with "png"

View All Tags

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

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

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

Chatbot Builder

chatbot builder - jsPlumb Toolkit, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

Schema Builder

database schema builder - jsPlumb Toolkit, 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.

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

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 Toolkit, build diagrams and rich visual UIs fast

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 Toolkit, build diagrams and rich visual UIs fast

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

undefined - jsPlumb Toolkit, industry standard diagramming and rich visual UI Javascript library

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 - JavaScript diagramming library that fuels exceptional UIs

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 - JavaScript diagramming library that fuels exceptional UIs

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.