Explained: Real-time heatmap example
Introduction
Update: I wrote a new article about an approach on multi-user real-time heatmaps
Due to some really nice feedback on my recently released real-time heatmap example with Javascript and HTMLCanvas ( Visit the example page ) I thought this article might be useful for gaining a better understanding of it.
This article contains a full explanation of the key hooks on which the heatmap example relies, I definitely will not write about the basic application structure, I think this part should be clear.
If not, there are many good books about Javascript out there ;-)
This article should also provide the possibility of discussing the example, I appreciate all forms of feedback.
So what are the basic requirements of a real-time heatmap?
– The heatmap should collect data dynamically
– The heatmap should show the dynamically collected data in (nearly) real-time
In my case, the user’s mouse-movement represents the heatmap data.
Mouse-movement tracking
… or how to NOT capture all MouseMove Events
At this point we should keep in mind that we are using Javascript. Since my heatmap example is using a live coloring algorithm it is not realistic to expect great application performance and processing of all the fired mousemove events at the same time. My approach on this problem was really simple: I defined a status variable (named invoke) which contains the information of whether an event should be captured or not (default value false). The check is in the mouse move handler, placed on the top of the function content so we can discard the event as early as possible.
mouseMoveHandler = function(ev){ // if invoke is false -> quit further processing if(!invoke) return; //... }
The initialize function sets a timer on the activate function which inverts the current state, so every 50ms the invoke state will get inverted.
initialize: function(/*...*/){ //... // call the activate function in an interval of 50ms (function(fn){ setInterval(fn, 50); })(activate); }
If the invoke variable is true, the mouseover event gets processed and the heatmap gets manipulated. After this manipulation, the invoke state gets inverted again.
Now we only process mouse events every 0.05 seconds which still results in enough data for a nice real-time heatmap effect.
Simulating MouseMove Events – the rough way for iPhone/iPad support.
I have to say I do not own an iPhone/iPad, so implementing/testing this feature was not easy for me, the first version of the heatmap example didn’t contain iPhone event support. The iPhone/iPad does not support the usual mouse events, there are seperate events for touches such as ontouchstart, ontouchmove, ontouchend..therefore I wrote an event handler for the ontouchmove event which simulates a mousemove event.
// iPhone / iPad support canvas["ontouchmove"] = function(ev){ var touch = ev.touches[0], // simulating a mousemove event simulatedEvent = document.createEvent("MouseEvent"); simulatedEvent.initMouseEvent("mousemove", true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); // dispatching the simulated event touch.target.dispatchEvent(simulatedEvent); // we don't want to have the default iphone scrolling behaviour ontouchmove ev.preventDefault(); };
It’s important to call the preventDefault function because otherwise the iphone default behaviour will consist of scrolling the page. This was probably not the best way to add iPhone support for the heatmap but it worked. If you have a better (working) solution for this, please let me know.
Alpha maps – now it’s getting interesting
And now the “magic”. The real-time heatmap generating process is based on alpha maps. What does this mean? This means that the heatmap colors were calculated from a grayscale (transparent) image which contains the frequency of the user’s mouse coordinates. For example a frequently visited spot at the map is darker and less transparent than a spot which has not been visited before.
But let’s have a closer look at the alpha map drawing code
//... var rgr = ctx.createRadialGradient(x,y,r1,x,y,r2); // the center of the radial gradient has .1 alpha value rgr.addColorStop(0, 'rgba(0,0,0,0.1)'); // and it fades out to 0 rgr.addColorStop(1, 'rgba(0,0,0,0)'); // drawing the gradient ctx.fillStyle = rgr; ctx.fillRect(x-r2,y-r2,2*r2,2*r2); //...
The code sequence above creates a radial gradient with .1 alpha as central (maximum) value and fades out to alpha=0. This code gets executed when the mouse move handler captures a mouse event thus the app creates an alpha radial at each captured mouse (/touch) coordinate. By now the heatmap only contains alpha values, the current heatmap would now probably look like this:
But a grayscale heatmap doesn’t look very spectacular – yea, it’s actually possible to extract exact the same information from a grayscale alpha heatmap, but colored heatmaps rock.
The heatmap coloring algorithm – the tricky part
For this part we need to know that the alpha map provides us a value range from 0 to 255, where 255 is 100% visibility and 0 is not visible. In our case the alpha value 255 equals 100% black, the value 0 is not visible. The next step is the transformation from the alpha values system to the rgb system.
The illustration above describes the problematic pretty well, we need to extract three values (red green and blue each 0..255) from only one alpha value. The alpha minimum is 0, the alpha maximum is 255, the hottest color in the heatmap should be red, the coldest color should be blue. Those facts allow us to build a customized color shift from alpha 255 to 0 aka (r=255,g=0,b=0) to (r=0,g=0,b=255). I chose my own coloring ranges, the illustration above explains the general concept. e.g. you want a transition from red to green: starting from 255,0,0 reduce the red tone and add more green tone up to 0,255,0. I set up my own alpha ranges for providing a more heatmap-like transition between the colors as you can see in the following snippet:
// coloring depending on the current alpha value if(alpha <= 255 && alpha >= 235){ tmp=255-alpha; r=255-tmp; g=tmp*12; }else if(alpha <= 234 && alpha >= 200){ tmp=234-alpha; r=255-(tmp*8); g=255; }else if(alpha <= 199 && alpha >= 150){ tmp=199-alpha; g=255; b=tmp*5; }else if(alpha <= 149 && alpha >= 100){ tmp=149-alpha; g=255-(tmp*5); b=255; }else b=255;
Further thoughts
How to get the heatmap to work in IE?
I gave this a try, but it turned out to be not as easy as I thought. My first approach was to include excanvas.js and look what happens, but the heatmap example requires canvas pixel manipulation, which is not possible with excanvas. After some research I found flashcanvas, which enables canvas support for IE by a flash layer. Flashcanvas supports pretty much everything I needed to run my real-time heatmap BUT I didn’t manage to get enough mouse move events in IE, because the events only fire upon entering the canvas element (?wtf!). I would really love to see someone’s approach :)
How can I save my heatmap?
In this example it’s possible to save the image data of the heatmap. You just have to use the getData function, which returns a (long) data-URL of your heatmap image, to get it. This is especially useful for subsequent serverside processing or saving the image on the client. e.g. you can probably save all heatmaps to a server which processes alpha values from the maps and creates an overall heatmap from this data – the colored heatmaps still contain the alpha values from their alpha map.
Final words
I hope you enjoyed this article about the key concepts of my real-time heatmap example. Did I miss something? What would you do better in this article or example? Are there any questions open? Please let me know!