Skip to main content

6 posts tagged with "community"

View All Tags

· 6 min read
Simon Porritt

This morning we've released version 6.0.0 of both the Toolkit and Community editions of jsPlumb.

What's new?

There is no functional change between version 6.0.0 and the last 5.x version, 5.13.7. What has changed, though, is the way that jsPlumb is packaged.

In 5.x we distributed both the Toolkit and Community editions as a set of packages, with the intention being that users could keep their code size down by omitting packages they did not need. In practice, though, all Toolkit users needed the core Toolkit packages - which were the largest - and all Community users needed the core Community packages, also the largest. The gain of distributing the code amongst several packages was negligible.

From a development perspective, the practice of spreading code around the various packages meant that the developer needed to know which package to go to in order to import something. For Toolkit users in particular, this could get tedious. Is the import in the Toolkit edition core? The Community edition core? Common? A renderer? etc.

So in 6.x we're distributing everything in a single Toolkit package and a single Community package, and users of each edition need only import one single package - either @jsplumbtoolkit/browser-ui or @jsplumb/browser-ui.

The packages contain a Common JS module, an ES6 module, and a UMD. For users who have a tree shaker incorporated in their build chain this setup will be good for their bundle sizes: exporting everything from a single package means the tree shaker can be very granular.

These packages are available now. For Community Edition users, @jsplumb/browser-ui version 6.0.0 is in the public NPM repository.

For Toolkit users who have an active subscription to new releases and to our NPM repository, @jsplumbtoolkit/browser-ui version 6.0.0 is in our NPM repository and can be downloaded from the download page.

We're still catching up on updating the docs and demonstrations - expect to see these changes over the next few days.

How to migrate?

The process of migrating to 6.x is straightforward.

Toolkit Edition

We've created a 6.x branch of the Flowchart Builder demonstration.

There were three things we needed to do:

  1. Update dependencies:

Previously:

"dependencies": {
"@jsplumbtoolkit/browser-ui-vanilla-2": "^5.12.0",
"@jsplumbtoolkit/drop": "^5.12.0",
"@jsplumbtoolkit/labels": "^5.12.0",
"@jsplumbtoolkit/print": "^5.12.0",
"@jsplumbtoolkit/dialogs": "^5.12.0",
"@jsplumbtoolkit/connector-editors-orthogonal": "^5.12.0",
"@jsplumbtoolkit/connector-orthogonal": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-drawing-tools": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-miniview": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-lasso": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-background": "^5.12.0"
}

Now:

"dependencies": {
"@jsplumbtoolkit/browser-ui": "^6.0.0"
}
  1. Update imports:

Previously:

import * as Dialogs from "@jsplumbtoolkit/dialogs"

import {
SurfaceRenderOptions,
Surface,
EVENT_TAP,
EVENT_CANVAS_CLICK,
EVENT_SURFACE_MODE_CHANGED,
SurfaceMode,
Connection,
BlankEndpoint,
ArrowOverlay,
LabelOverlay,
AnchorLocations,
DEFAULT,
ready,
newInstance
} from "@jsplumbtoolkit/browser-ui-vanilla-2"

import {
Edge,
Vertex,
ObjectInfo,
AbsoluteLayout,
uuid,
forEach,
EVENT_UNDOREDO_UPDATE,
UndoRedoUpdateParams,
ObjectData,
extend
} from "@jsplumbtoolkit/core"

import { EdgePathEditor } from "@jsplumbtoolkit/connector-editors"
import { createSurfaceManager } from "@jsplumbtoolkit/drop"
import { registerHandler } from "@jsplumbtoolkit/print"
import {DrawingToolsPlugin} from "@jsplumbtoolkit/browser-ui-plugin-drawing-tools"
import {MiniviewPlugin} from "@jsplumbtoolkit/browser-ui-plugin-miniview"
import {OrthogonalConnector} from "@jsplumbtoolkit/connector-orthogonal"

import * as ConnectorEditorOrthogonal from "@jsplumbtoolkit/connector-editors-orthogonal"
import {LassoPlugin} from "@jsplumbtoolkit/browser-ui-plugin-lasso"
import {CancelFunction} from "@jsplumbtoolkit/dialogs"

import {GeneratedGridBackground, GridTypes, BackgroundPlugin} from "@jsplumbtoolkit/browser-ui-plugin-background"

ConnectorEditorOrthogonal.initialize()

Now:

import {
Dialogs,
SurfaceRenderOptions,
Surface,
EVENT_TAP,
EVENT_CANVAS_CLICK,
EVENT_SURFACE_MODE_CHANGED,
SurfaceMode,
Connection,
BlankEndpoint,
ArrowOverlay,
LabelOverlay,
AnchorLocations,
DEFAULT,
ready,
newInstance,Edge,
Vertex,
ObjectInfo,
AbsoluteLayout,
uuid,
forEach,
EVENT_UNDOREDO_UPDATE,
UndoRedoUpdateParams,
ObjectData,
EdgePathEditor,
createSurfaceDropManager,
registerHandler,
DrawingToolsPlugin,
MiniviewPlugin,
OrthogonalConnector,
LassoPlugin,
CancelFunction,
GeneratedGridBackground,
GridTypes,
BackgroundPlugin
} from "@jsplumbtoolkit/browser-ui"

Note also that it is not necessary to invoke ConnectorEditorOrthogonal.initialize() in version 6.x of the Toolkit.

  1. Update the Dialogs constructor

Since everything is exported from a single package there were a few method name clashes - multiple packages, for example, exported a newInstance method. Most of those newInstance methods were just convenience methods and didn't do anything particularly useful. For example, this is how we rewrote the code that creates a Dialogs instance:

Previously:

const dialogs = Dialogs.newInstance({
...
})

Now:

const dialogs = new Dialogs ({
...
});

Sometime over the next few days we'll merge all of the 6.x demonstration branches into main.

Community Edition

All of the Community Edition demonstrations have been ported to use 6.0.0 now on the main branch. Here's what we did to migrate the Flowchart demonstration (this is for the Typescript demonstration):

  1. Update dependencies

Previously:

  "dependencies": {
"@jsplumb/browser-ui": "^5.0.0",
"@jsplumb/connector-flowchart": "^5.0.0"
},

Now:

  "dependencies": {
"@jsplumb/browser-ui": "^6.0.0"
},
  1. Update imports

Previous:

import {
ContainmentType,
EVENT_CLICK,
EVENT_CONNECTION_ABORT,
EVENT_CONNECTION_DRAG,
newInstance,
ready
} from "@jsplumb/browser-ui"

import {AnchorLocations, AnchorSpec,} from "@jsplumb/common"

import {
Connection,
ConnectionDetachedParams,
ConnectionMovedParams,
DotEndpoint,
EVENT_CONNECTION_DETACHED,
EVENT_CONNECTION_MOVED,
LabelOverlay
} from "@jsplumb/core"

import {FlowchartConnector} from "@jsplumb/connector-flowchart"

Now:

import {
ContainmentType,
EVENT_CLICK,
EVENT_CONNECTION_ABORT,
EVENT_CONNECTION_DRAG,
newInstance,
ready,
AnchorLocations,
AnchorSpec,
Connection,
ConnectionDetachedParams,
ConnectionMovedParams,
DotEndpoint,
EVENT_CONNECTION_DETACHED,
EVENT_CONNECTION_MOVED,
LabelOverlay,
FlowchartConnector
} from "@jsplumb/browser-ui"

What are the breaking changes?

We'll be updating the changelog in the documentation over the next few days, but for convenience here we include the changelog for each edition.

Toolkit breaking changes

  • Support for the original templates syntax (where variable interpolations are of the form ${...}) has been dropped, throughout the Vanilla toolkit and the dialogs. The code from the previous templates-2, dialogs-2 and browser-ui-vanilla-2 packages has been retained in the @jsplumbtoolkit/browser-ui package, and the code from templates, dialogs and browser-ui-vanilla has been dropped.

  • The Spring layout has been removed. Use ForceDirected instead.

In order to support the single @jsplumbtoolkit/browser-ui package, several factory methods were renamed/removed:

  • newInstance(surface) in previous @jsplumbtoolkit/browser-ui-anim removed. Use new SurfaceAnimator(surface) instead.

  • createManager in previous @jsplumbtoolkit/drop package is now exposed as createDropManager in @jsplumbtoolkit/browser-ui

  • createSurfaceManager in previous @jsplumbtoolkit/drop package is now exposed as createSurfaceDropManager in @jsplumbtoolkit/browser-ui

  • newInstance(surface:Surface, options?:ConnectorEditorOptions) from previous @jsplumbtoolkit/connector-editors method removed - use new EdgePathEditor(surface:Surface, options?:ConnectorEditorOptions) instead.

  • initialize() method from previous @jsplumbtoolkit/connector-editors-bezier is now exposed as initializeBezierConnectorEditors() on @jsplumbtoolkit/browser-ui.

  • initialize() method from previous @jsplumbtoolkit/connector-editors-orthogonal is now exposed as initializeOrthogonalConnectorEditors() on @jsplumbtoolkit/browser-ui.

  • newInstance(params:DialogsOptions) from the previous @jsplumbtoolkit/dialogs-2 package has been removed. To instantiate Toolkit dialogs now, use new Dialogs(params).

  • The HORIZONTAL and VERTICAL axis identifiers from the HierarchicalLayout have been moved into the enum HierarchicalLayoutOrientations.

  • The code from the related -drop package for each library integration (@jsplumbtoolkit/browser-ui-angular-drop etc) has now been pulled into the library integration's main package

Community breaking changes

  • newInstance method removed from @jsplumb/browser-ui-lists. Use new JsPlumbListManager(instance, params) instead.

  • BeforeStartDetachInterceptor renamed to BeforeStartConnectionDetachInterceptor

  • BeforeDetachInterceptor renamed to BeforeConnectionDetachInterceptor

  • BeforeDropInterceptor renamed to BeforeConnectionDropInterceptor


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.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 2 min read
Simon Porritt

jsPlumb uses Docusaurus, a super handy static site generator, for various parts of our ecosystem, including our Documentation and also our recently released stand alone components product.

While developing the site for jsPlumb Components, we wanted to include a couple of pieces of information that would vary depending on whether we were running in development mode locally (ie docusaurus start) or building for production. We looked into the various options available, all based on the dotenv plugin for Webpack, but couldn't find what we were looking for: a solution that worked, with minimal manual intervention, in both development mode and when building for production.

So we ended up building our own plugin. This plugin reads values from a JSON file and makes them available to your site via the customFields map in the Docusaurus siteConfig.

note

When I say "Docusaurus" in this post I am talking about v2. I've not used v1.

Installation

npm i @jsplumb/docusaurus-plugin-env-loader-json

Configuration

You just have to add the plugin to the list of plugins in docusaurus.config.js:

plugins:[
"@jsplumb/docusaurus-plugin-env-loader-json"
],

and then create an env.json file in the Docusaurus project directory.

env.json

Your environment variables should be keyed under a section that identifies the environment you are targetting - "development" when using docusaurus start and "production" when running a build. The plugin reads the environment name from the environment variable NODE_ENV.

{
"production":{
"SERVER_URL":"https://some.server.com/anEndpoint"
},
"development":{
"SERVER_URL":"http://localhost:4200/anEndpoint"
}
}

Accessing values

You can access these values via the customFields section of the Docusaurus site config:

import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

export function MyApp {

const {siteConfig} = useDocusaurusContext();
const serverUrl = siteConfig.customFields.SERVER_URL

...

}

Changing the configuration file name

If you want to specify some file other than env.json in your project's root, you can do so by setting the sourceFile option of the plugin:

plugins:[
[
"@jsplumb/docusaurus-plugin-env-loader-json",
{
"sourceFile":"path/to/aFile.json"
}
]
],

This path should be specified relative to the project root. No leading slash is required.


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.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 14 min read
Simon Porritt
outdated code

The code snippets in this post are only valid for versions of the Toolkit prior to 6.9.0. We've disabled the live examples and will be writing an updated version of this post for 6.9.0+ soon.

The jsPlumb Toolkit has five connector types available:

  • Bezier
  • Straight
  • Orthogonal
  • StateMachine
  • Segmented

Together these connectors cater for quite a few use cases, but if none of these are exactly what you need, it is possible to define your own custom connectors. In this post we'll take a look at how to do that, by defining a connector that provides a line taking the form of a triangle wave between its two endpoints.

CONNECTOR CONCEPTS

A connector is basically a path between two points. jsPlumb represents a connector as a series of segments, of which there are three types:

  • Straight
  • Bezier
  • Arc

The various connectors that ship with jsPlumb consist of combinations of these basic segment types. A Straight connector, for instance, consists of a single Straight segment. Bezier and StateMachine connectors consist of a single Bezier segment. An Orthogonal connector consists of a series of Straight segments, and if cornerRadius is set, then each pair of Straight segments has an Arc segment in between.

These three basic segment types have so far been sufficient to define all of the connectors in jsPlumb, and for the triangle wave example I will be modelling the connector as a series of straight segments. But it is feasible that at some stage in the future there will be a need for a segment that models an arbitrary path. If you're reading this and you find that might apply to you, get in touch and we'll see what we can do.

THE MATH

It helps to first sketch up what you're aiming for. Here I'm using an HTML canvas to draw how I want the triangle wave connector to look. Using a canvas has the obvious advantage that once I get it how I want I've got most of the hard work done! Obviously it also has the disadvantage that if you're looking at this site in IE<9 you won't see anything. That's ok. If you're looking at this page with a view to doing anything about it, then you're a web developer...you have a real browser kicking around somewhere.

The basic approach to creating a triangle wave is to get the equation for the line joining the two endpoints, then create a parallel line above and below this line. These parallel lines are the lines on which the peaks of the wave will sit.

const wavelength = 10, amplitude = 10;
const anchor1 = [ a, b ],
anchor2 = [ c, d ];

// find delta in x and y, the length of the line joining the two anchors,
// the gradient of that line, and the gradient of a normal to that line.
const dx = c - a,
dy = d - b,
d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)),
m = dy / dx,
n = -1 / m;

// calculate how many peaks there will be, and also how much to shift by to
// have the line fit nicely between the two anchors
const peaks = Math.round(d / wavelength),
shift = (d - (peaks * wavelength) / 2);

// generate points. start at anchor1, travel along line between the two anchors,
// and alternate between projecting peaks from upper to lower.
let points = [ anchor1 ], upper = true;
for (let i = 0; i < peaks; i++) {
const xy = pointOnLine(shift + ((i+1) * wavelength)),
pxy = translatePoint(xy, upper);
points.push(pxy);
upper = !upper;
}

points.push(a2);

Here, pointOnLine and translatePoint are, respectively, functions to find a point on the line between the two anchors, and to project a point from the line between the two anchors onto the upper or lower parallel line. The code for these is included in the full code listing at the end of the post.

CONNECTOR CODE

This is the basic skeleton of a custom connector:


import { AbstractConnector, Connectors, StraightSegment } from "@jsplumbtoolkit/browser-ui"

export class TriangleWaveConnector extends AbstractConnector {

static type = "TriangleWave";
type = TriangleWaveConnector.type;

_compute(paintInfo /*PaintGeometry*/, paintParams /*ConnectorComputeParams*/) {
// your math here, resulting in a series of calls like this:
this._addSegment(StraightSegment, { ... params for segment ... });
}

getDefaultStubs()/*: [number, number]*/ {
return [0,0]
}

transformGeometry(g /*Geometry*/, dx /*number*/, dy /*number*/)/*: Geometry */{
return g
}
}

Connectors.register(TriangleWaveConnector.type, TriangleWaveConnector);

It has to extend AbstractConnector, and it needs to implement 3 methods:

  • _compute This is where you calculate the segments in your connector.
  • getDefaultStubs Optional, it tells jsPlumb whether or not you want stubs by default, and if so, how long they should be
  • transformGeometry Optional, and outside the scope of this article. This is for connectors whose paths can be subsequently manipulated by the user (such as the Orthogonal connector in the Toolkit edition)

Computing segments

The _compute method is what jsPlumb will call at paint time, and it is the contents of the paintInfo object you'll be interested in - it contains a lot of parameters, many of which you don't need, but here are the ones you might find useful:

paintInfo: {    
sx: 442.6, // start anchor, x axis
sy: 0, // start anchor, y axis
tx: 0, // end anchor, x axis
ty: 51, // end anchor, y axis
startStubX: 442.6, // end of start stub, x axis. may be equal to sx.
startStubY: 0, // end of start stub, y axis. may be equal to sy.
endStubX: 0, // end of end stub, x axis. may be equal to tx.
endStubY: 51, // end of end stub, y axis. may be equal to ty.
w: 442.6, // distance in x between start and end.
h: 51, // distance in y between start and end.
mx: 221.3, // midpoint in x between start and end.
my: 25.5, // distance in y between start and end.
opposite: true, // true if the orientations of the two anchors
// are 180 degrees apart.
orthogonal: false, // true if the orientations of the two anchors
// are the same
perpendicular: false, // true if the orientations of the two anchors
// are 90 degrees apart.
segment: 3, // Segment of circle in which lies the angle of a
// line from the start anchor to the end anchor.
so: [ 1, -1 ], // orientation of start anchor. See jsPlumb docs.
to: [ 0, -1 ], // orientation of end anchor. See jsPlumb docs.
}

The most interesting values in here for the majority of connectors are sx, sy, tx and ty, which give the location of the source and target anchors. [ sx, sy ] and [ tx, ty ] are the equivalent of the anchor1 and anchor2 values in our pseudo code above. A simple straight line connector, for instance, could (and does!) just add a single segment from [sx, sy] to [tx, ty].

So now we have enough to put together the code for the connector - we'll use the skeleton code and plug in our maths.


import { AbstractConnector, Connectors, StraightSegment } from "@jsplumbtoolkit/browser-ui"

export class TriangleWaveConnector extends AbstractConnector {

static type = "TriangleWave"
type = TriangleWaveConnector.type

wavelength = 10
amplitude = 10

constructor(connection, params) {
super(connection, params)
}

_compute(paintInfo, computeParams) {

let dx = paintInfo.tx - paintInfo.sx,
dy = paintInfo.ty - paintInfo.sy,
d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)), // absolute delta
m = Math.atan2(dy,dx),
n = Math.atan2(dx, dy),
origin = [ paintInfo.sx, paintInfo.sy ],
current = [ paintInfo.sx, paintInfo.sy ],
peaks = Math.round(d / this.wavelength),
shift = d - (peaks * this.wavelength),
upper = true;

for (let i = 0; i < peaks - 1; i++) {
let xy = pointOnLine(origin, m, shift + ((i+1) * w)),
pxy = translatePoint(xy, n, upper, this.amplitude);

this._addSegment(StraightSegment, {
x1:current[0],
y1:current[1],
x2:pxy[0],
y2:pxy[1]
});
upper = !upper;
current = pxy;
}

// segment to end point
this._addSegment(StraightSegment, {
x1:current[0],
y1:current[1],
x2:paintInfo.tx,
y2:paintInfo.ty
});
};
}

Connectors.register(TriangleWaveConnector.type, TriangleWaveConnector)

And here it is in action. You can drag those boxes around:

CONSTRUCTOR PARAMETERS

So far we have a triangle wave connector with a fixed distance of 10 pixels between the peaks, and a peak height of 10 pixels. What if we want to be able to control these values? For that we'll want to supply constructor parameters. As with the vast majority of objects in jsPlumb, when you specify a connector type you can supply just the name of the connector, or you can supply an array of [ name, { parameters }]. In the second case, jsPlumb will provide the parameters object as an argument to your Connector's constructor. So we might change our usage of the Triangle Wave Connector to specify a 20 pixel gap between the peaks, and a peak height of 7px:

connector:{ 
type:TriangleWaveConnector.type,
options:{ wavelength:20, amplitude:7 }
}

And then the first few lines of our connector will change to take these parameters into account:

import { AbstractConnector, Connectors } from "@jsplumbtoolkit/browser-ui"

export class TriangleWaveConnector extends AbstractConnector {

static type = "TriangleWave"
type = TriangleWaveConnector.type

wavelength
amplitude

constructor(connection, params) {
super(connection, params)
this.wavelength = params.wavelength || 10
this.wavelength = params.amplitude || 10
}

...
}

Connectors.register(TriangleWaveConnector.type, TriangleWaveConnector)

Here's the code from before, but with a wavelength of 20px, and an amplitude of 7px:

OVERLAYS

There's nothing special you need to do to support overlays; they are handled automatically by AbstractConnector in conjunction with the connector segments. Here's the same code again, with a label:

THOSE TRIANGLES LOOK LIKE SPRINGS

Don't they, though? Maybe we could modify the code and make them behave like simple springs too. Let's consider the basic behaviour of a spring: it has a fully compressed state, beyond which it can compress no more, and as you stretch it, the coils separate further and further. Obviously in a real spring, there is a value at which the spring has been stretched beyond the limit at which it can spring back. We're not going to model that here, though. Here we're just going to keep things simple - we'll add a flag defining whether or not to behave like a spring, and define a minimum distance, corresponding to the fully compressed state:

this.wavelength = params.wavelength || 10
this.amplitude = params.amplitude || 10
this.spring = params.spring
this.compressedThreshold = params.compressedThreshold || 5

And let's say that when the two elements are closer than compressedThreshold, the wavelength will be 1 pixel. Beyond that, the wavelength will grow as the two elements separate. By how much? I'm going to pull a number out of thin air here and say that when the spring is not fully compressed, the wavelength will be 1/20th of the distance between the two anchors. Actually I should be honest: I didn't pull this number completely out of thin air. I ran it a few times with different values until I found something I liked the look of.

Now I can configure two elements to be connected with a rudimentary spring:

instance.connect({
source:document.getElemenyById("w7"),
target:document.getElementById("w8"),
connector:{
type:TriangleWaveConnector.type,
options:{
spring:true
}
}
});

WHAT ABOUT STUBS? I WANT STUBS.

Some types of connectors benefit from having a first segment that emanates as a straight line from their anchor, before the real business of connecting comes into play. You can see this in the Flowchart demonstration in jsPlumb. Now that our triangle wave connector can behave like a spring, it strikes that me it would be good to support stubs here too. Fortunately, it isn't very hard to do. Remember the sx/sy/tx/ty parameters from above? If you supply a stub argument to your connector, paintInfo also exposes the location of the end of the stubs, via startStubX/startStubY/endStubX/endStubY.

So we can change the code to use these stub locations as the origin and final point, and then also add a segment for each stub:

let dx = paintInfo.endStubX - paintInfo.startStubX,
dy = paintInfo.endStubY - paintInfo.startStubY,
d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)),
m = Math.atan2(dy, dx),
n = Math.atan2(dx, dy),
origin = [ paintInfo.startStubX, paintInfo.startStubY ],
current = [ paintInfo.startStubX, paintInfo.startStubY ],
...

Here's the result:

instance.connect({
source:document.getElementById("w9"),
target:document.getElementById("w10"),
connector:{
type:TriangleWaveConnector.type,
options:{
spring:true,
stub:[ 20, 20 ]
}
},
anchors:["Right", "Left"]
});

IN SUMMARY

It's pretty straightforward to add a new connector type to jsPlumb. Most of the work is really in the maths underpinning the connector's path. For reference, below is the "triangle wave" connector's code in full (which turned out to be a spring in disguise!).

Whilst working on the spring stuff at the end of this post it occurred to me that a real spring would impose bounds on the two elements it was joining: for instance, the two elements should not be able to be closer than the spring's compressed size, and there is a point at which the spring will refuse to stretch any further. At first I was tempted to think about ways the connector could help model these behaviours, but of course this connector is just the view; decisions about constraining movement do not belong here. Look out for a future post in which I will discuss the general direction jsPlumb is heading in with respect to these sorts of requirements.

And finally, if you make something awesome, please do consider sharing it with others!

FORK ME ON GITHUB

The code for this connector, along with a small test harness, is available at https://github.com/jsplumb-demonstrations/custom-connector-example

THE FINAL CODE

import { AbstractConnector, Connectors, StraightSegment } from "@jsplumbtoolkit/browser-ui"

// this function takes a point from the midline and projects it to the
// upper or lower guideline.
function translatePoint(from, n, upper, amplitude) {
const dux = isFinite(n) ? (Math.cos(n) * amplitude) : 0;
const duy = isFinite(n) ? (Math.sin(n) * amplitude) : amplitude;
return [
from[0] - ((upper ? -1 : 1) * dux),
from[1] + ((upper ? -1 : 1) * duy)
];
}

// this function returns a point on the line connecting
// the two anchors, at a given distance from the start
function pointOnLine(from, m, distance) {
const dux = isFinite(m) ? (Math.cos(m) * distance) : 0;
const duy = isFinite(m) ? (Math.sin(m) * distance) : distance;
return [
from[0] + dux,
from[1] + duy
];
}

export class TriangleWaveConnector extends AbstractConnector {

static type = "TriangleWave"
type = TriangleWaveConnector.type

wavelength
amplitude
spring
compressedThreshold

constructor(connection, params) {
super(connection, params)
params = params || {}
this.wavelength = params.wavelength || 10
this.amplitude = params.amplitude || 10
this.spring = params.spring
this.compressedThreshold = params.compressedThreshold || 5
}

getDefaultStubs(){
return [0, 0]
}


_compute (paintInfo, paintParams) {

let dx = paintInfo.endStubX - paintInfo.startStubX,
dy = paintInfo.endStubY - paintInfo.startStubY,
d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)), // absolute delta
m = Math.atan2(dy, dx),
n = Math.atan2(dx, dy),
origin = [ paintInfo.startStubX, paintInfo.startStubY ],
current = [ paintInfo.startStubX, paintInfo.startStubY ],
// perhaps adjust wavelength if acting as a rudimentary spring
w = this.spring ? d <= this.compressedThreshold ? 1 : d / 20 : this.wavelength,
peaks = Math.round(d / w),
shift = d - (peaks * w),
upper = true;

// start point to start stub
this._addSegment(StraightSegment, {
x1:paintInfo.sx,
y1:paintInfo.sy,
x2:paintInfo.startStubX,
y2:paintInfo.startStubY
});

for (let i = 0; i < peaks - 1; i++) {
let xy = pointOnLine(origin, m, shift + ((i+1) * w)),
pxy = translatePoint(xy, n, upper, this.amplitude);

this._addSegment(StraightSegment, {
x1:current[0],
y1:current[1],
x2:pxy[0],
y2:pxy[1]
});
upper = !upper;
current = pxy;
}

// segment to end stub
this._addSegment(StraightSegment, {
x1:current[0],
y1:current[1],
x2:paintInfo.endStubX,
y2:paintInfo.endStubY
});

// end stub to end point
this._addSegment(StraightSegment, {
x1:paintInfo.endStubX,
y1:paintInfo.endStubY,
x2:paintInfo.tx,
y2:paintInfo.ty
});
}
}

Connectors.register(TriangleWaveConnector.type, TriangleWaveConnector)


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

Version 5.4.0 of the jsPlumb Toolkit has support for a new concept - the ability to specify at drag start/stop what the location of an anchor should be. This functionality is exposed to users of the Toolkit edition by supporting anchorPositionFinder values in the view.

Example

Try dragging a connection from one element to another. When you drop a connection on the right hand side of an element, its anchor is on the right face; when you drop on the left hand side, the anchor is on the left face. Note that the elements are not draggable in this demonstration, as the entire element is a connection source.

Anchor position finders

These are the render params used to create this:

{
layout:{
type:"Absolute"
},
defaults:{
anchor:"Continuous",
endpoint:"Dot"
},
zoomToFit:true,
templates:{
"jtk-template-default":'<div data-jtk-source="true" data-jtk-target="true" data-jtk-target-port-type="twosides"></div>'
},
view:{
ports:{
twosides:{
anchorPositionFinder:function(el, elxy, vertex) {
var x = elxy.x < 0.5 ? 0 : 1,
ox = x === 0 ? -1 : 1;

return [x, 0.5, ox, 0]
}
}
}
}

Aside from the anchorPositionFinder function itself, the key piece to grok in this setup is the way the node template is hooked up to the view. This is the html for each node:

<div data-jtk-source="true" 
data-jtk-target="true"
data-jtk-target-port-type="twosides">
</div>

So we've got three attributes declared here:

  • data-jtk-source="true" Indicates the node is a connection drag source
  • data-jtk-target="true" Indicates the node is a connection drag target
  • data-jtk-target-port-type="twosides" Tells the Toolkit that this target should use the port definition called twosides, which is where our anchorPositionFinder is declared. Note that in the Toolkit you can make use of logical port mappings like this without actually declaring that the target is a port on the node. In this case, the node itself is still the target, but we're using the twosides port definition to define some behaviour.
note

In this demonstration we don't declare a node type; instead we use jtk-template-default as the ID of the template we provide. In the absence of any other information about how to render a node, the Toolkit will look for a template with that ID.

Another Example

To illustrate the flexibility of this arrangement, let's create something more complex. We'll have three node types - red, blue and green.

  • red nodes will just use the default anchors (Continuous)
  • green nodes will use the anchor strategy from the first example
  • blue nodes will place an anchor exactly where the connection was dropped.
Anchor position finders - complex example

The render params for this example are as follows:

{
layout:{
type:"Absolute"
},
defaults:{
anchor:"Continuous",
endpoint:"Dot"
},
zoomToFit:true,
view:{
nodes:{
red:{
template:'<div data-jtk-source="true" data-jtk-target="true" style="background-color:red"></div>'
},
green:{
template:'<div data-jtk-source="true" data-jtk-target="true" style="background-color:mediumseagreen"></div>',
anchorPositionFinder:function(el, elxy, vertex) {
var x = elxy.x < 0.5 ? 0 : 1,
ox = x === 0 ? -1 : 1;

return [x, 0.5, ox, 0]
}
},
blue:{
template:'<div data-jtk-source="true" data-jtk-target="true" style="background-color:cadetblue"></div>',
anchorPositionFinder:function(el, elxy, vertex) {
return [elxy.x, elxy.y, 0, 0]
}
}
}
}
}

Here, we map a template for each node type, and then for blue and green types, we provide an anchorPositionFinder. The logic in the finder for green is as shown in the example above. For the blue node type, recall that elxy is the proportional location of the drop event on the drop element. This coordinate system is the same that Anchors use, so we can just return the values from elxy directly.

Note also that in this example we have declared anchorPositionFinder on node definitions, whereas in the previous example we declared the position finder on a port definition. You can define them on any vertex definition - node, port or group.

note

If you're new to the Toolkit and you'd like to know more about views, you can read about them in the documentation


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.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 3 min read
Simon Porritt

After several months of development we are pleased to announce that version 5.0.0 of both the Toolkit and Community editions of jsPlumb have recently been released. The 5.x releases are:

  • smaller
  • faster
  • tree shakeable
  • better documented

and offer a simpler, more standardised API - without sacrificing any features. The new codebase for jsPlumb provides a solid platform for us to go on and build the next evolution.

Installation

Community edition

npm i @jsplumb/browser-ui

This package is the equivalent of the jsPlumb window global that users of 2.x will be familiar with.

Toolkit edition

For licensees or evaluators of the Toolkit edition, version 5.x is also available, hosted in jsPlumb's own npm repository.

If you are a licensee and you currently have access to download new releases, you now automatically have access to our npm repository. If you are a licensee whose access to download new releases has expired, you are welcome to request an evaluation of 5.x

Licensees can now check their download expiry access on our downloads page. Just enter your license key and press "Check available version".

Documentation

Toolkit edition

Documentation for version 5.x of the Toolkit edition can be found here.

Community edition

You can browse the Community edition documentation here.

What happens to 2.x?

It depends on which version you're talking about. For the Community edition, 2.x is now very much in maintenance mode, and is unlikely to receive any updates unless the current 2.x version of the Toolkit requires it. Users of the Community edition are strongly encouraged to migrate to 5.x now.

The current version 2.x of the Toolkit edition has now switched over into a lightweight maintenance mode, which is to say that any bugs reported will be fixed, and, wherever possible, any new features added to 5.x that can also be added to 2.x without major dev work will be added to 2.x. When the support window closes for the last 2.x licensee at some point in the future, the 2.x version of the Toolkit will enter a stricter maintenance mode.

All new evaluators will be offered only 5.x from this point.


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.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 5 min read
Simon Porritt

jsPlumb uses Statcounter to keep track of what pages in the documentation people are looking at. Recently, at the tail end of the work to migrate to Typescript and release version 5.x of the Community and Toolkit editions, we started using Docusaurus, a super handy static site generator that not only runs on React but has React baked right into it, allowing us to easily embed working demonstrations throughout our docs, or to embed snippets from the api documentation directly into the main documentation via React components. It's a really handy tool, and if you're looking for a static site generator I recommend giving it a look.

note

When I say "Docusaurus" in this post I am talking about v2. I've not used v1.

This is not a post about how great Docusaurus is, though. This is a quick post about a Statcounter plugin I wrote while working on the jsPlumb API documentation. Docusaurus ships with a plugin for Google Analytics, allowing to quickly add support for GA to all the pages on your site, but jsPlumb doesn't use Google Analytics. Statcounter's interface has a pleasing directness to it and gives us all the information we need. A quick look around the internets came up blank on existing Docusaurus/Statcounter integrations, so I wrote a plugin.

Installation

npm i @jsplumb/docusaurus-plugin-statcounter

Configuration

First you have to add the plugin to the list of plugins in docusaurus.config.js:

plugins:[
"@jsplumb/docusaurus-plugin-statcounter"
],

Then you need to configure the plugin via a statCounter block in the themeConfig of your docusaurus.config.js. It takes two arguments, both required:

themeConfig: {
statCounter:{
projectId: "2222222",
securityCode: "2222222"
},
...
}

projectId and securityCode are available in the Statcounter console for the project you wish to target.

Internals

This is the first plugin I've written for Docusaurus and it doesn't have the most complex requirements, but I was quite impressed with the plugin mechanism. Let's start with the package.json:

{
"main": "src/index.js",
"name": "@jsplumb/docusaurus-plugin-statcounter",
"version": "1.0.0"
}

We deliver a basic package with a single entry point.

Bootstrap

Let's take a look at the first few lines of index.js:


const path = require('path');

module.exports = function (context) {

const {siteConfig} = context;
const {themeConfig} = siteConfig;
const {statCounter} = themeConfig || {};


};

Our module exports a single function that takes a context object, inside of which we can extract the siteConfig, and, from that, the themeConfig, which contains our statCounter settings.

Once we have our statCounter object, we extract the things we need from it, and complain about stuff that is missing:

const {projectId, securityCode} = statCounter;

if (!projectId) {
throw new Error(`The statcounter plugin requires a "projectId" to be set`)
}

if (!securityCode) {
throw new Error(`The statcounter plugin requires a "securityCode" to be set`)
}

Linking with Statcounter

Statcounter works by importing a JS file in your document's head, after setting a couple of global variables. Docusaurus makes this very easy for us - all we have to do is declare an injectHtmlTags() method in our plugin:

return {
name: 'docusaurus-plugin-statcounter',

getClientModules() {
return [path.resolve(__dirname, './statcounter')]
},

injectHtmlTags() {

return {
headTags:[
{
tagName:'script',
innerHTML: `
var sc_project="${projectId}";
var sc_invisible=1;
var sc_security="${securityCode}";
`
},
{
tagName:'script',
attributes:{
src:"https://www.statcounter.com/counter/counter.js"
}
}
]
}
}
};

We inject two tags into the head: first we inject a script element and provide its innerHTML - this sets up the global variables. Next, we inject another script, but this time we set its src attribute, so it loads the JS from Statcounter's site. Documentation for injectHtmlTags(..) can be found here.

Tracking page changes

Given that Docusaurus is an SPA, pages get swapped in and out without new page loads, and Statcounter would be oblivious, were we not to advise it. Fortunately this was also straightforward. Note this block in our plugin's code:

getClientModules() {
return [path.resolve(__dirname, './statcounter')]
}

This instructs Docusaurus to load the module found in ./statcounter.js. The source code for that file looks like this:

import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

export default (function () {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}

return {
onRouteUpdate({location}) {
_statcounter.record_pageview()
},
};
})();

This module hooks into onRouteUpdate, which I think is perhaps undocumented (for inspiration on this bit I used the Google Analytics plugin).

When a route update event occurs, we call _statcounter.record_pageview(). _statcounter is an object in the global space that was added by Statcounter's JS.

Conclusion

That's the whole plugin - perhaps 80 lines of code. Very straightforward - it took me about half an hour to write and deploy. Injecting HTML tags is not the only thing you can do with a Docusaurus plugin, though. Take a look through the lifecycle APIs to get a feel for what's possible.

If you want to follow up on this, the source is on Github at https://github.com/jsplumb/docusaurus-plugin-statcounter.


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.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.