Vinger die wijst naar plattegrond metro

Creating an interactive SVG metro map with JointJS

As a child I was always fascinated with the map prints. It was an absorbing activity to study those beautiful graphic forms and yet stunning to realise this is the form of presentation of actual data. Years passed, I didn’t become a cartographist, but the childhood days fascination remained intact. Knowing that, imagine how happy I was, when asked to join the team that was to implement map functionality into an existing application. It was no ordinary map, though. It had to be metro map. This article is intended to be a report on the enjoyable process of working with metro map. It contains a couple of code pieces explaining and illustrating various features of JointJS library, eventually assembled together into a small demo.

The case

Don Bureau, the advice and engineers company we’re cooperating with for a few years turned up with a request of further development of FMECA tool, one of their products. FMECA is an app which helps DON with performing risk analysis faster, in a more user-friendly and a more efficient way. FMECA tool was developed as an autonomous product for internal use at DON. The benefits from using it were so bold that the client decided on further development and converting it to a SaaS solution. As the relations between various abstract types of data are complicated to imagine, yet one more part of the application - the metro map, detailed overview of data flow - was to be developed.  

The map

So, what actually is the metro map? Just to make things clear; metro map is the visualisation of data containing stations and routes as two basic components. Routes may only run in four (horizontal, vertical, and two 45° diagonal) directions. Below you can find a small visual example. The main purpose of the metro map is staying readable even with a great number of nodes. It does not depict topological locations, but emphasizes correlations between objects making the chart clear and easy to understand. JointJS - metromap Example of how a piece of map meeting all the principles of metro map looks like

The base

The most important factor that affected choice of the library was coverage of the defined app goals with framework’s built-in features, namely:

  • Capability of defining custom routers
  • Support for rich interactivity - click, tap, drag-drop, context menu and so on
  • Speed / performance - tool waits for user and not the other way around
  • As the product was aimed to be intuitive, we also looked for the user-friendliness of built-in mechanisms
  • Available Functions determining relations between nodes

Another factor that had some influence on the technology we decided to use was browser support. As a reasonable compromise the app was set to support evergreen browsers, IE10+, Edge and major mobiles (android and iphone). In the process of web research I’ve encountered many various libraries, like those sample, basic ones, but providing ability to draw a metro map:

It quickly became obvious that those little scripts are not enough and we needed something much more powerful. We turned to JavaScript data visualisation libraries. Data-Driven Documents The first one we looked at was D3, a famous, mighty JS library that allows to visualise really complex diagrams. It is a very powerful tool, but it turned out to have a little too much overhead for our needs, still not providing a really excellent interface for interactively creating maps. Vis.js Another blank shot was Vis.js. We assembled a prototype really quickly and it impressed us with the functionalities of drawing nodes and linking them together. All of that was possible out of the box, easy and fast. It took us really close to the target, but it was a bit cumbersome about creating custom routers which was necessary to use in order to render the metro-like connections between the nodes. Moreover, it is based on canvas element, which always makes working with interactivity a little harder. Joint.js Then, by doing some more research, I’ve stumbled upon Joint.js. Being based on SVG and Backbone it seemed to provide a reliable foundation for further work. There’s a paid extension available. It’s called Rappid, and is a powerful and modern toolkit for building visual tools of various kinds. That is undoubtedly an advantage as it would extend the limits once they’re reached. Another great value asset of JointJS is that it contains a custom router called Metro. A little gawky, but with a bit of tweaking it was ready to be used.  

Proof of concept

I’ve started with creating a static diagram similar to the blueprint provided by the client. Quickly and effortlessly adding some interactions I was able to emulate the final product. The prototype that I’ve created in just a couple of hours went far beyond client’s expectations, exceeding form of a simple proof of concept. I was pleased with the result. The small effort to complete the task made me sure of the choice. JointJS thus, is a reliable and efficient library that is perfectly suitable for carrying out the task of creating an interactive map. Moreover it utilises SVG graphics for rendering, and that makes all the nodes appear as objects in DOM which in the end increases readability for developers and enables standard JS methods.  

What is JointJS?

Follow JointJS website

JointJS is a modern HTML 5 JavaScript library for visualization and interaction with diagrams and graphs. It can be used to create either static diagrams or, and more importantly, fully interactive diagramming tools.

While the most of the JavaScript libraries for visualisation are focused on providing best possible end-user experience and overall display / render matters, JointJS is aimed at interactivity understood more as interacting with data itself than just hover and click events, changing view modes or filters. JointJS was built using BackboneJS inheriting its MVC structure. This allows to manipulate data detached from the view, which is fed with updated data. Basic termsCell vs link As mentioned in the introduction, a basic metro map consists of two elements. Cell (Node, station) is just an abstract object which has a set of properties; name or label, location in the diagram. Cell, traditionally shown as a dot , represents object, process place, person, event or just any other node. Additionally, in this library, cell can contain ports and magnets, which are slots for the links to be connected to. Cell can contain many of them with custom position and appearance. Link (route) on the other hand, represents the relation that happens between cells (nodes). Traditionally, links are represented by lines which can have various arrowheads meaning different things. In the following example, the black link symbolizes just the relation, binding, whilst the pink one represents the flow. JointJS - route JointJS - route Usually, links connect together two nodes, but it may happen that a link is connected to only one node. It should never be the case that a link is not connected to any nodes. The presence of links determines the cell’s neighbours. If it is the case, arrowheads determine the overall direction of the flow in the chart. Both a link and a cell can have some custom attributes or properties. In our case, each cell has status, owner, organisation and a few others. Router Route of the link is determined by a router. It gets positions of the objects, optionally some environmental variables also (as alignment of obstacles, allowed directions or just a grid) and calculates the most efficient (shortest) route. The one used by me was a variation of Dijkstra's Least Cost Algorithm with some penalties for changing directions added.Look at the example gif at right. Lowest Cost Algorithms count the cost of every variation of a path until reaching the target. Each iteration increases the cost by given amount."JointJS Lowest Cost Algorithms"
Paper and its options

Paper is nothing else than the SVG canvas. It’s created in the wrapper element provided as an constructor parameter upon initialisation of paper, and it’s used as a SVG container for every node in the graph. At this point I want to highlight a couple of settings available in constructor of JointJS’s paper, the most handy ones. gridSize - straightforward. JointJS supports grid and snapping to it. If you need a visibile grid, you have to draw it yourself though. defaultLink - the default link object which is used as user-created links. defaultConnector - There are three default connector types available, pretty self-explanatory, see the illustration below; normal (A), rounded (B) and smooth (C). JointJS - connectors JointJS - connectors defaultRouter - here is the default routing function that will calculate and output the route for user created links interactive - set to false to disable dragging cells and creating links. Set to true to enable it, or provide it with a function to be called on interactions. validateMagnet - function that can be used to disable dragging link from magnet validateConnection - function analogical to validateMagnet, only used to prevent dropping link on some of the ports. snapLinks - when enabled, force a dragged link to snap to the closest element/port in the given radius linkPinning - when set to true, links can be pinned to the paper meaning a source or target can be a point on the surface markAvailable  - extremely useful parameter which adds ‘available-magnet’ class to magnets that are ready to be connected and ‘available-cell’ class to cells containing those magnets. async - draws cells asynchronously, it’s possible to provide size of the batch in which cells are to be drawn embeddingMode - if set to true, nodes can be dropped onto another element. Then they are pinned and nested in this element. There’s also validateEmbedding function available.  

The Demo

To illustrate this a bit, I've set up a codepen playground as a really simple JointJS demo. From now on I'll be adding little snippets as examples. This one is basically a demo merging all the little examples.See the Pen Fun with JointJs - DEMO (Full) by soentio (@soentio) on CodePen.  

Router, cell and link in real life

JointJS already contains a so-called Metro router, which is able to omit obstacles drawing path in angle of 45 degrees. Unluckily, it’s out of the box settings led to really curvy paths, as the penalty for direction change was not big enough. Increasing the penalty made it really stick to one direction at most of the times. Another issue was that by default it is configured for starting and finishing the paths in horizontally and vertically only and in some cases, when it adds up penalties for direction change, it is cheaper to go with horizontal/vertical lines without diagonal shortcuts. To the array of allowed start/end directions:

directionMap: {     Right: { x: 1,  y: 0 },     Bottom: { x: 0,  y: 1 },     Left: { x: -1, y: 0 },     Top: { x: 0,  y: -1 } }

I’ve added four new:

Topright:     {x: Math.SQRT2,  y: -Math.SQRT2}, Bottomleft:    {x: -Math.SQRT2, y: Math.SQRT2}, Topleft:    {x: -Math.SQRT2, y: -Math.SQRT2}, Bottomright:   {x: Math.SQRT2,  y: Math.SQRT2}

Minimal setup For making JointJS work, you need three things (besides dependencies): Graph, Paper and a node element to render your diagram to (let’s call it canvas). Graph is a backbone model holding all the cells (elements and links) of the diagram. Paper is the view for the Graph model. It inherits from the Backbone View. When a paper is associated with a graph, the paper makes sure that all the cells added to the graph are automatically rendered. This piece of code gets you going:

var canvas = $(‘#canvas’); var graph = new joint.dia.Graph(); var paper = new joint.dia.Paper({     el: canvas,     width: canvas.outerWidth(),     height: canvas.outerHeight(),     model: graph });

See the Pen Fun with JointJs - Minimal setup by soentio (@soentio) on CodePen.If you inspect the DOM now you can see that an SVG node was added inside of our #canvas. Right. We’ve got our cornerstone ready for getting some serious interactions done. Let’s start with... Adding nodes To create a node, at first you have to create a model for it: new joint.shapes.devs.Model({/* options */});. Most important options are type (usually using provided ones or extending it) and position ({x: 20, y: 20}). You can provide much more options, such as attrs which sets the SVG/HTML attributes on node’s markup. The node model is ready, now it has to be added to the graph:

graph.addCell(ourNewNode);

Paper is automatically updated so our cell gets rendered. One more important property is ports. inPorts: ['center'] will make port named center accessible for dropping... Links If node contains ports, it allows dragging automatically created link out of it. To add link programmatically, analogical to what we’ve done with a node, we do create a Link model: new joint.dia.Link({*/ options */}). Options are a bit different, though. We have to provide a source and a target. Each of those properties can be either an object describing cell we want the link to be attached to, or just XY space coordinates. If we want the link to go from point x: 20 and y: 40, we just use this as a source ({x: 20, y: 40}). To connect the link to a node, you need to provide id and port to which it should be attached:

source: {     id: ourNewNode.id,     port: 'center' }

And then again, just:

graph.addCell(ourNewLink);

We’re all done, look at the demo:See the Pen Fun with JointJs - DEMO AddNode, AddLink by soentio (@soentio) on CodePen.  

Working interactively with JointJS

Adding Event listeners Although you can add event listeners to SVG nodes, it's easier to add them using JointJS methods. Otherwise it would require complicated selectors to target right nodes. To add an event listener onto an element use:

paper.on('cell:pointerclick', function(event) {     /* function body */ });

Clicked Element is available as  event.el. This way you can run your  action dependent on the node type. In the following demo a #message div  is updated with the text content of the node.See the Pen Fun with JointJs - eventListeners by soentio (@soentio) on CodePen. You can use many more events. List of all of them is to be found in JointJS docs. Zooming the canvas To enable zoom and pan functionalities, I’ve used svg-pan-zoom library. You just call it this way svgPanZoom('#zoom-element', {/* options */}); Options are, well, optional :) This is an excerpt containing most interesting ones:

zoomEnabled: true, panEnabled: true, controlIconsEnabled: true, minZoom: 0.5, maxZoom:2, zoomScaleSensitivity: 0.5

I think they’re quite obvious, maybe except for controlIconsEnabled. Setting this to true enables a little widget that helps navigating the image, as shown below: JointJS - widgetSee the Pen Fun with JointJs - svg-pan-zoom by soentio (@soentio) on CodePen. Filtering Diagrams in our app can contain dozens of process nodes and they’re an overview for all people involved in particular flow. To make it more convenient, client wanted to have an ability to filter the nodes on various properties (like process’ phase, status or owner) so that one can easily see for example running processes that he’s responsible for. Just think of how easier it is to spot 10 colored nodes among 100 grayed-out ones. Filtering is just visually hiding items not matching your query, or - in our case - highlighting items that do match it. It does not really change anything in the data itself. For this reason, so to separate display and data layers, and taking advantage of the fact that JointJS uses SVG, I’ve chosen filters made in CSS. This is also more performant solution, as filtering is actually done by browser while rendering css. As mentioned before, you can set the attributes on the JointJS cells. That’s what we do. Passed as option when creating model or set with attr() function later on, i’m adding data-parameters to the nodes to allow them to be distinguished by css rules.

ourNode.attr('.element-node/data-color', 'pink');

Now, to target specific nodes, we have to add some data-attribute on our container, just like . We can bind it’s value to the value of some . That’s simplest you can do. What is left to be done now, is adding some smart rules using SCSS’ loops:

$elementsColors: pink #f1207d, gray #ABABAB, black #000000; #canvas {     .element-node {         opacity: 0.2;     } each $elementColor in $elementsColors { &[data-filter="#{nth($elementColor, 1)}"] { .element-node[data-color="#{nth($elementColor, 1)}"] { opacity: 1; } } } &[data-filter="all"] { .element-node { opacity: 1;         } } }See the Pen Fun with JointJs - Filtering by soentio (@soentio) on CodePen.

There was some part of the styles generated in javascript and added to the head on runtime. Owners of the processes are set dynamically and their amount may vary, so keeping CSS rules for every of them wasn’t a way to go.  

Support for SVG

In general, support for inline SVG across the browsers is pretty good. Let’s remember that we are creating an application with custom UI and actions that are hardly the part of what you usually see in the web. This all makes support on IE>8 acceptable.
JointJS - browsersupport JointJS - browsersupport I have tested the application in various browsers both in browserstack.com  as well  as  on real devices. The results were surprisingly good.  it didn't need any hacks  to work in Internet Explorer (as it usually does).  

Weak spots

If I was to name weak spots of JointJS I would say that the biggest one is the size, its dependencies - jQuery: 2.2.4, Lodash: 3.10.1, Backbone: 1.3.3 weight together almost 70 kB, full version of JointJS itself - 61kB and its CSS file - 34kB. Together it’s more than 170kB. That’s much, and your app has not even started loading at this point. But this is definitely not a script to be included on every page, and your users probably have the dependencies cached already. Another issue may be it’s performance with large amount of nodes. If you want to display few thousands of them, then JointJS is probably not the right tool for the job. Not because it’s own performance, but because of every cell is actual DOM node. That makes it really heavy for the browser to render and handle.  

Conclusion

In my humble opinion the best thing about JointJS is flexibility when it comes to markup of nodes. It will consume almost everything, and converting SVG designs to actual elements is almost as easy as just copying it’s SVG markup. You can load the markup from external files as well. Ports, nesting components, snapping to the grid, functions to get nodes from current cursor position or inside an area of given coordinates just make the work pleasure. JointJS is surely a cool library. It makes working with graphs really easy and allows customising its elements. You can create new models or extend existing ones. You can animate them and code really sophisticated interactions. All of that with the aid of CSS and in really friendly way. There’s a paid extension (if you prefer to paying over coding) called Rapid Framework which is based on JointJS and contains much more features, however, it is not a cheap one. We couldn’t use it as one of the requirements was not to use any paid libraries. Luckily, JointJS allows implementing new functionalities without big effort and it’s architecture helps with designing your app neatly. I hope you’re eager to try your luck with JointJS after this reading. If this is the case, good luck and feel free to share your thoughts, ideas or just results of your work with me at i[email protected].