As part of a current pet project (more on that some other time) I stumbled upon a fantastic canvas-based library for creating heat maps- the aptly named heatmap.js. It only has one flaw- when you start adding a lot of data, it’s slow. That’s a problem for me, because I want to animate my heat maps. With no modifications, the map was rendering at around 3fps on my 2010 Macbook Air.
How does this thing work anyway?
The first step was to find out how the library worked in the first place. It’s pretty ingenious- drawing monochromatic blurred circles for each of the data points, at different alpha levels depending on value. Then it redraws pixel by pixel, colorising according to the alpha value of the pixel. This means that overlapping points combine alphas, dictating the overall colour.
Problem #1: this involves using <canvas>’s getImageData() function- which has some serious performance problems. Surprise #1: while different browsers have huge performance differences, all benefitted by splitting the map into multiple canvases and reading them in turn. As it happens, that suits my needs perfectly- I am going to be displaying the heat map on top of tiled map images.
Still not good enough
It was an improvement, but I was still looking at around 12fps in Chrome- not bad, but not ideal. With the getImageData() parts optimised as best I could manage, I looked at the next big drain- the initial drawing of blurred data points. Problem #2: what can I do that is any simpler than drawing a circle? Surprise #2: I can just copy and paste the same circle over and over.
The existing code was drawing it’s circles like so:
ctx.shadowColor = ('rgba(0,0,0,'+((count)?(count/me.store.max):'0.1')+')');
ctx.shadowOffsetX = 1000;
ctx.shadowOffsetY = 1000;
ctx.shadowBlur = 15;
ctx.arc(x - 1000, y - 1000, radius, 0, Math.PI * 2, true);
I extracted that code and drew it (with 1.0 opacity) on another separate <canvas>. Then all I needed to do for each point was:
var pointTemplate = me.get("pointTemplate"); // the canvas tag
ctx.globalAlpha = ((count)?(count/me.store.max):0.1)
It didn’t seem like it ought to be faster- switching between two different tags and running drawImage() seemed like it would be far more complex than drawing a simple circle. But I was wrong- the performance increase was massive- take a look at the before and after test pages. There are undoubtedly further improvements to be made, but my heat maps are now flowing along at over 30fps, which gives me a good amount of leeway as I continue to develop my project. I would love to be able to transfer these calculations to a web worker, but alas they have no DOM, and so there’s no way for me to get a 2D drawing context.
Did I miss something fundamental? Any ideas for improvements? Let me know!