Skip to main content

5 posts tagged with "typescript"

View All Tags

· 3 min read
Simon Porritt

One of the core concepts when working with components in Angular is that of @Inputs and @Outputs. For instance, maybe you have some component that expects to be told what text to use as its title, and which will output an event when the user clicks a button:

@Component({
selector:"app-my-component",
template:`
<div>
<h1>{{title}}</h1>
<button class="btnEmit" (click)="emitEvent('test')">Click Me</button>
</div>
`
})
export class MyComponent {
@Input() title:string

@Output() outputEvent = new EventEmitter<string>()

emitEvent(value:string) {
this.outputEvent.emit(value)
}
}

From version 5.6.2, the Toolkit's Angular integration now offers a couple of ways of working with these that earlier versions did not support.

Using @Input in a node component

Imagine the component shown above was being used to render some node type in your UI - with a couple of small changes we can make it so:

import {BaseNodeComponent} from "@jsplumbtoolkit/browser-ui-angular"
import {EventEmitter, Component, Input, Output} from "@angular/core"

@Component({
template:`
<div>
<h1>{{title}}</h1>
<button class="btnEmit" (click)="emitEvent('test')">Click Me</button>
</div>
`
})
export class NodeComponent extends BaseNodeComponent {
@Input() title:string

@Output() outputEvent = new EventEmitter<string>()

emitEvent(value:string) {
this.outputEvent.emit(value)
}
}

We've removed selector from the Component spec, since it's not relevant when rendering via the Surface, and we've wired up the component to extend BaseNodeComponent, but other than that the component is the same as in the first example. Prior to version 5.6.2, this line:

@Input() title:string

would be ignored by the Toolkit. From 5.6.2 onwards, though, the Toolkit will attempt to set this input from the backing data for the node. So if, for instance, you had this data object for a node:

{
"id": "1",
"title": "My First Node"
}

then the Toolkit will set "My First Node" on the title input. This is of course really just a form of syntactic sugar - you could just easily render the title in your template like this (remembering that obj is set on the component by the Toolkit when it creates it):

<h1>{{obj.title}}</h1>

as you can by using the input on the class:

<h1>{{title}}</h1>

but if you're like me, you might find that the approach using @Input feels a little more nicely formalised.

Using @Output in a node component

You can also now wire up listeners to @Outputs in your components. The process to do this involves providing a vertexEvent callback to the Surface component. For example, imagine you embed a surface component in one of your components:

<div class="my-component">
<h3>Here is my component</h3>
<jsplumb-surface surfaceId="surface"
toolkitId="myToolkit"
[view]="view"
[renderParams]="renderParams"
(vertexEvent)="vertexEvent($event)">
</jsplumb-surface>
</div>

Remember from above the @Output() outputEvent... declaration on the node component? Whenever that output emits something, the Surface will invoke vertexEvent:


import { Vertex } from "@jsplumbtoolkit/core"
import { ComponentRef } from "@angular/core"

vertexEvent(detail:{
vertex:Vertex,
event:Event,
payload:any,
component:ComponentRef<any>}) {

}

The callback is given the Toolkit's vertex, the original event, the payload (ie. whatever was emitted from the output), and the Angular ComponentRef. You can access the specific Angular component via the instance field on this component ref.


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.

· 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

Following on from part 1 of this series, in which we created a visualisation for the progress of each of the group stages, in today's installment we're going to take a look at drawing the post-group stages using the Toolkit's Hierarchical layout.

note

This post is from June 2018 but has been updated to reflect how to code this using version 5.x of the Toolkit. With another world cup around the corner I'm keen to dust this off and be ready to use it again.

These stages start with the "round of 16", then go to the quarter finals, then semis, and then the final and the third place playoff. A suitable structure for visualising this is an inverted tree with two root nodes.

The result

Again we'll jump to the result of my endeavours and then we'll go through how I got to this point. On the 25th of June 2018 the group phases were not finished, but this visualisation shows the current 1st and 2nd each team in group:

Remember I mentioned this blog post is being updated after the fact? We now have all the match data for the 2018 world cup, so at the end of this post I've included a version of this visualisation with every match result.

The dataset

The model for this visualisation is discussed in part one of this series.

Processing the data

We build on part 1 by first processing the groups, which provides the information we need about who is placed first and second in each group, as the round of 16 matches are played between the first and second placed teams of the various groups.

In this visualisation, each node in the Toolkit represents a single match, which has two teams, a score, and optionally a penalty count. So the dataset consists of a node for each of the round of 16 matches, quarter finals and semi finals, and one for each of the final and the third place playoff - a total of 16 nodes. Each node's payload is in this format:

{
"id": "40",
"score": [ 1, 1 ],
"penalties": [ 3, 4 ],
"date": 20180704,
"teams": [
{ "id": "co" },
{ "id": "en" }
]
}

Edges in the visualisation model a team's progression through the stages, and consist of just the ID of the two matches:

{
"source": "40",
"target": "41"
}

The rendering

In this post we're using the Hierarchical layout to display an inverted tree, with the round of 16 as the top row, and the final/ and third place playoff in the bottom row.

Instantiating a Toolkit instance

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

const toolkit = newInstance();

Rendering the dataset

const surface = toolkit.render(container, {
layout:{
type:"Hierarchical",
options:{
invert:true
}
},
view:{
nodes:{
default:{
template:'<div class="match-node">' +
'<div class="team">' +
'<div class="flag">' +
'<img class="flag" src="/img/flags/1x1/${teams[0].code}.svg" alt="${teams[0].name} Flag"/>' + '</div>' +
'<div class="name">${teams[0].name}</div>' +
'<div class="score" data-penalties="${penalties[0]}">${score[0]}</div>' + '' +
'</div>' +
'<div class="team">' +
'<div class="flag">' +
'<img class="flag" src="/img/flags/1x1/${teams[1].code}.svg" alt="${teams[1].name} Flag"/>' +
'</div>' +
'<div class="name">${teams[1].name}</div>' +
'<div class="score" data-penalties="${penalties[1]}">${score[1]}</div></div>' +
'</div>'
}
}
},
defaults:{
connector:"StateMachine",
endpoint: "Blank",
anchors: ["ContinuousTop", "ContinuousBottom"]
},
zoomToFit: true
});

This is a summary of what's happening in the render call:

  • We use a layout of type Hierarchical, with invert:true set.
  • We map a single node type "default" to a template, which writes out a simple element that contains the names of the two teams, their flags, the score, and also the penalty count, if present. Note that in the template we don't need to take into account the value being null: the Toolkit's template engine handles this for us.
  • We don't need to map an edge type as there's nothing special going on with edges - no overlays, no click listeners etc.
  • We provide default values for all edges - the StateMachine connector, a Blank endpoint, and for anchors we use ContinuousTop as the source anchor and ContinuousBottom as the target. This means that each connection gets assigned its own anchor point on the source and target nodes, and the Top and Bottom suffixes restrict the choice of face to those options. The layout is inverted, so in fact edges have the later stage match as their source and the earlier stage match as their target.
  • In this visualisation, unlike in part 1 of this series, wheel zoom and pan is enabled (they are by default; we just took out the directives that set them to false in part 1).
  • We instruct the Surface to zoom the contents to fit after a data load operation via the zoomToFit:true parameter.

The complete dataset

Fast forward a few years from the original date of this post and we've got all the match results for the 2018 world cup, so let's take a look. In this visualisation, though, we'll switch the orientation of the layout to vertical, to make better use of our screen real estate:


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.

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.

· 6 min read
Simon Porritt

It's world cup time again and I've been looking for a good overview of how it's all progressing in the group stages. Being a computer programmer I of course spent a small amount of time looking for one done by someone else, and then decided to just do it myself. With the trusty jsPlumb Toolkit at my disposal I figure it'll be a doddle.

note

This post was originally written in 2018, and at the time I was hoping to re-use this code when the next World Cup came around. For 2022 we've pulled this post into a standalone site - check it out at https://fifaworldcup.jsplumbtoolkit.com

The result

Let's jump to the result of my endeavours and then we'll go through how I got to this point. Here's what all the groups look like on the 25th of June 2018:

A
B
C
D
E
F
G
H

The dataset

The model for this visualisation is broken up into several files. For starters we have the list of teams, in this format (this is obviously not the full list):

[
{"code": "ru", "name": "Russia"},
{"code": "uy", "name": "Uruguay"},
{"code": "eg", "name": "Egypt"}
]