
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.
Example of how a piece of map meeting all the principles of metro map looks like
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.
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 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.
See 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
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).
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.
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
- jQuery plugin for subway map visualisation non interactive, data read from data-attributes
- Metro Map Creator which is more of a drawing widget than a serious framework
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 websiteJointJS 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.



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 waysvgPanZoom('#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: 
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.