Scale up your D3 graph visualisation – WebGL & Canvas with PIXI.js

· 3 min read

Do you use D3 for data visualisation and either you are considering, or already using it also for graph visualisation? Keep in mind that D3 uses SVG for rendering. While it is the easiest to work with API for drawing 2D graphics on the Web, its downside is that the browser keeps the entire DOM tree of vector elements in memory, even for elements that are effectively invisible. You might hit a performance drop with complex graphics, specifically for graph visualisation when you try drawing graphs larger than ~1000 nodes, or even less with complex SVG effects.

At this time you should reconsider why you run into performance issues in the first place. Why do you have such complex graph to draw, what is the purpose of the visualisation, and how is user going to interact with it? Sometimes too much information can result into meaningless graphics, if user doesn’t know what to do with it.

Since you’re still reading, you probably have reasons? Ok, I warned you :-)

In this article we are going to explore together how you can scale up your existing D3 graph visualisation. Another solution could be resorting to a commercial library, but we are not going to cover it here, because it could mean significant changes to your existing application. However if you are starting a new project, all of these libraries are great to work with and we suggest you evaluate them (listed in alphabetical order): Keylines by Cambridge Intelligence, Ogma by Linkurious or yFiles by yWorks.

Canvas HTML element has a few available drawing APIs, with different performance and browser support. The most advanced, WebGL, uses GPU for hardware-accelerated drawing. However this means that for the best coverage of drawing size, browser and hardware compatibility you would need to implement drawing code multiple times, for each chosen API separately.

Another complexity arises with mouse interaction, because with Canvas you can only detect the mouse position and color at the position. Detecting which element is at the position can be implemented for example by rendering a separate hidden canvas, where color designates the element.

Visible canvas
Visible canvas
Separate hidden canvas for detecting clicked element
Separate hidden canvas for detecting clicked element

PIXI.js

Enter PIXI.js, a 2D drawing library. It allows you to express a declarative render tree, similarly to SVG. However the render tree is processed in the JavaScript engine only, instead of in the DOM as in SVG. The browser only receives drawing instructions, as if you would write Canvas drawing code yourself.

PIXI.js uses WebGL by default if available, and supports fallback to Canvas otherwise. Mouse interaction complexities are also abstracted away from the developer. No wonder that this library has a heavily active community related to development of browser-based games.

Implementation

We are going to start by forking the original D3 graph visualisation example, which uses d3-force for a force-directed layout.

Replacing SVG rendering with PIXI.js involves creating an instance of PIXI.Application and adding children to it, according to the desired style and interaction. Follow API docs for details, their Performance Tips are also helpful. We can add richer features such as labels, font icons, hover effect, zoom & drag viewport and a simple toolbar. Anything is possible!

A live demo of this exercise shows there is almost nothing left from the original code which was related to SVG rendering, it was replaced with code for a different rendering target. PIXI.js rendering runs on top of D3, which stays only for computing graph layout.

Live demo
Live demo

Summary

We have uncovered a hidden strength of D3. A few years ago, D3 was refactored with modularity in mind. A single monolithic library was split into many composable single-purpose libraries, which can be used also standalone. If you need to increase performance of your existing D3 data visualisation, you can replace rendering by another drawing library such as PIXI.js, while still using D3 for underlying layout computation.

Further performance improvement can involve moving layout computation to a separate WebWorker thread, so that it doesn’t block UI from other actions. I can see Part 2 of this article coming soon ;-)

Get in touch with GraphAware to see how we can help you with performant graph visualisations!

Jan Zak