Skip to main content

· 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, I 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.


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.


npm i @jsplumb/docusaurus-plugin-statcounter


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.


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.


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:""            }          }        ]      }    }  };

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.


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

· 3 min read
Simon Porritt

Over the last several months, jsPlumb - both the Community and Toolkit editions - has been ported into Typescript, undergoing various refactoring and API changes along the way, with the goal of producing a version of jsPlumb that is:

  • smaller
  • faster
  • tree shakeable
  • better documented

In version 5.x, the version numbering for the Toolkit and Community editions will be kept in sync: a release containing changes only to the Community edition will result in a corresponding Toolkit edition release, even though it contains no changes, and vice-versa. Keeping the version numbers in sync in this way takes a certain amount of guesswork out of the equation.

I'm pleased to share that we're almost ready to release version 5.0.0. Over the next few weeks I'll be running a series of "howto" posts, short posts that discuss one aspect of using jsPlumb, both the Community and Toolkit editions, in a modern web app.


Community edition#

You can already try version 5.x of the Community edition - it's on npm (albeit with an RC version suffix):

npm i @jsplumb/browser-ui

This package is the equivalent of the jsPlumb window global that users of 2.x will be familiar with. It's been on npm for a couple of months now and is used quite extensively already, so don't let the RC version suffix put you off. You can browse the documentation here.

Toolkit edition#

For licensees or evaluators of the Toolkit edition, version 5.x is also available, hosted in jsPlumb's own npm repo. If you'd like to check it out, you can register your interest here.

Documentation for version 5.x of the Toolkit edition can be found 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.

For licensees/evaluators of version 2.x of the Toolkit edition, when 5.x is released the current version will switch 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. When version 5.x of the Toolkit is released it will also not be possible for new evaluators/licensees to download version 2.x of the Toolkit edition.

· 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.


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:

· 5 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.


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.

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:


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"}]