
Heat maps are an effective method for showing geographical or temporal data in a very visual and easily-grasped manner – and now there’s an easy-to-use JavaScript implementation.
Heatmap.js is the brainchild of Patrick Wied (patrick-wied.at), which began life as a JS1K competition entry and is now open source and available from GitHub (github.com/pa7/heatmap.js). It has since been used by Rockstar Games (of Grand Theft Auto fame) and a number of visualisation projects featured at bit.ly/171zlVal as well as gaining the interest of over 1,100 GitHub users.
But heat maps don’t have to be limited to traditional maps. In this tutorial we’ll use heatmap.js in two ways. First, to create a user experience tool which will track where the user interacts with the page with their mouse. When they navigate to a different page we’ll save an image to a server that could then layer together all heat maps of a single page and see which elements are interacted with the most.
We’ll also create a heat map visualisation with Leaflet (leafletjs.com), which will overlay a dataset over the UK to demonstrate exactly how versatile heatmap.js is.
Heat map Factory
Heatmap.js exposes itself as h337 (as in 1337, but heat) to the window. To make a new heat map out of the entire page we’ll attach it to the body element and make it invisible by default (although for debugging it’s useful to set this to true). The radius is how big a single point is in pixels.
001 var heatmap = h337.create({
002 element: document.body,
003 radius: 25,
004 visible: false
005 });
Set up variables
Next we’ll set up a few variables that will help us keep track of the user’s movements. lastCoords will contain the X and Y co-ordinates when the mouse moves and when it’s still. mouseMove and Over are flags to make sure we don’t store results when the mouse is idle (e.g. if the user is hovering over something).
001 var active = false,
002 lastCoords = [], timer = null,
003 mouseMove = false, mouseOver = false,
004 activate = function() {
005 active = true;
006 };
Click event
We’ll start by adding a new point when the user clicks. This is probably the most common thing that you’ll do with Heatmap.js and so it makes it very easy to accomplish. Simply use its utility method for getting the mouse’s position from the event and then use its addDataPoint method to add the X and Y co-ordinates.
001 var el = document.body;
002 el.addEventListener(‘click’, function(event) {
003 var pos = h337.util.mousePosition(event);
004 heatmap.store.addDataPoint(pos[0], pos[1]);
005 }, false);
Simulating click
Next we’ll write a method that will simulate the click event and be used when the user hovers over something. Again, it takes up to three parameters: X, Y, and a value, but for this we’re only interested in the X and Y co-ordinates but we’ll read the last known co-ordinates of the mouse from the lastCoords array.
001 var simulateEv = function() {
002 heatmap.store.addDataPoint(lastCoords[0], lastCoords[1]);
003 };
Being anti idle
To call this simulateEv method we’ll check for when a user hovers over a single point, if the mouse triggers the mouseover event and it’s not moving then we push a new data point to the heat map every second that this occurs – this builds a stronger point, turning it from blue to red.
001 var antiIdle = function() {
002 if (mouseOver && !mouseMove && lastCoords && !timer) {
003 timer = setInterval(simulateEv, 1000);
004 }
005 };
006 (function(fn) {
007 setInterval(fn, 1000);
008 }(antiIdle));
Mouse-out event
When the user’s mouse no longer hovers on that element we clear the timer, if there is one, and set the mouseOver flag to false so that the antiIdle function will stop adding data points to the last co-ordinate every second. By using addEventListener we don’t conflict with other functions listening to the same event.
001 el.addEventListener(‘mouseout’, function() 002 {
003 mouseOver = false;
004 if (timer) {
005 clearInterval(timer);
006 timer = null;
007 }
008 }, false);
Mouse-move event
We now have heat map data points being added every time the user clicks, as well as when they hover. Next we’ll add an event for when the user moves their mouse. This adds a data point with the mousePosition and addDataPoints methods we used earlier and sets the lastCoords array which antiIdle uses.
001 el.addEventListener(‘mousemove’, function(ev) {
002 mouseMove = true; mouseOver = true;
003 if (active) {
004 if (timer) {
005 clearInterval(timer);
006 timer = null;
007 }
008 var pos = h337.util.mousePosition(ev);
009 heatmap.store.addDataPoint(pos[0], pos[1]);
010 lastCoords = [pos[0], pos[1]];
011 active = false;
012 }
013 mouseMove = false;
014 }, false);
Save function
That’s all it takes to make a functional UX heat map and see where users are interacting with the page. To increase its usefulness we can then export the heat map to an image and POST that to a server to be saved. We’ll pass the site’s title so that it can be identified later on.
001 var save = function() {
002 var url = heatmap.getImageData();
003 $.post(‘ux-heatmaps.php’, {
004 img: url,
005 site: document.title
006 });
007 };
Adding buttons
When user testing you could include some administrator buttons on the page to see the heat map being produced and manually save it if the user does something noteworthy. It is probably better to do this remotely though, to avoid disrupting the user’s flow. Still, for the purposes of this tutorial let’s include some.
001 <div class=”admin-buttons”> 002 <h3>Admin only</h3> 003 <button class=”btn” id=”save”>Save</ button> 004 <button class=”btn” id=”toggle- visibility”>Toggle</button> 005 </div>
Layout CSS
We’ll fix the admin buttons to the bottom-right of the page so that they don’t interrupt the page’s layout and make them semi-transparent, only appearing fully when they’re hovered over. As we have our CSS open we’ll also set the height of the map that we’ll make shortly.
001 .admin-buttons {
002 position: fixed;
003 bottom: 1em; right: 1em;
004 opacity: 0.5;
005 transition: opacity 0.15s linear;
006 }
007 .admin-buttons:hover {
008 opacity: 1;
009 }
010 #map {
011 height: 500px;
012 } 013
Adding events
Now that we’ve written the save function we can trigger it when the page unloads, ie when the user goes to another page. Also we’ll add a manual save button as well as a toggle button for the heat map’s visibility. This is done using another helper method in Heatmap.js, toggleDisplay.
001 var toggleVisibility = function() {
002 heatmap.toggleDisplay();
003 };
004 $(‘#save’).on(‘click’, save, false);
005 $(‘#toggle-visibility’).on(‘click’, 006 toggleVisibility, false);
007 $(window).on(‘unload’, save, false);
Decode image data
We’re POSTing to the server but not actually handling it, so the following short snippet of PHP gets the POST data, decodes the Base64 encoded string and then converts it to a PNG file. You could then use another tool (for example, something like FFmpeg) to layer images from the same page to see which elements users are interacting with most often on the page.
001 <?php 002 define(‘UPLOAD_DIR’, ‘heatmaps/’); 003 $img = $_POST[‘img’]; 004 $img = str_replace(‘data:image/ png;base64,’, ‘’, $img); 005 $img = str_replace(‘ ‘, ‘+’, $img); 006 $data = base64_decode($img);
Save image file
With our image file ready to save, we’ll give it a name. We POSTed the site’s title so we’ll use that to identify it and a timestamp (time()) so that we can them proceed to list them by name and still see them chronologically. file_put_contents is an easy way to open a file and write its contents – in this case, the Base64 string.
001 $site = $_POST[‘site’]; 002 $filename = UPLOAD_DIR . $site . ‘-’ . time() . ‘.png’; 003 $success = file_put_contents($filename, $data); 004 print $success ? $filename : ‘Unable to save the file.’;
Map HTML
We’ve learned how we can use Heatmap.js to track user’s movements in real-time and save that data as an image. Next, we’ll visualise static data on a map, this could be anything from how many stores you have in separate locations, to dry statistical data. The ID on the map <div> will help Leaflet initialise it.
001 <div class=”hero-unit container”> 002 <h1>Heading</h1> 003 <p>Tagline</p> 004 <div id=”map”></div> 005 <p> 006 <a class=”btn btn-primary btn- large”>Learn more</a> 007 </p> 008 </div>
Script includes
Heatmap.js has built–in support for Leaflet, it’s a great alternative to Google Maps and its implementation seems to work better than the Google Maps integration that comes with Heatmap.js. We also include QuadTree.js, which is a way of subdividing datasets to make it efficient at each zoom level.
001 <link rel=”stylesheet” href=”http://cdn. leafletjs.com/leaflet-0.6.4/leaflet.css”> 002 <script src=”http://cdn.leafletjs.com/ leaflet-0.6.4/leaflet.js”></script> 003 <script src=”src/heatmap-leaflet.js”></ script> 004 <script src=”src/QuadTree.js”></script> 005
New tile layer
Leaflet’s layers are fully customisable (like Google Maps satellite and road in hybrid mode) so we define which we’ll use as the default base layer and build on top of that. It gets its map data from the OpenStreetMap project and we’ll specify a maximum zoom level as it can be unresponsive with lots of overlapping points in some cases.
001 var baseLayer = L.tileLayer(‘http://{s}. tile. cloudmade.com/997/256/{z}/{x}/{y}.png’, {
002 attribution: ‘Map data ©
003 <a href=”http://openstreetmap. org”>OpenStreetMap</ a> contributors, <a href=”http:// creativecommons.org/ licenses/by-sa/2.0/”>CC-BY- SA</a>, Imagery © <a href=”http://cloudmade. com”>CloudMade</ a>’,
004 maxZoom: 18
005 });
Heat map layer
We then add our second layer: the heat map. This time we’ll specify a much larger radius and set the absolute property to true. This means that the data points stay relative to the maximum value in the dataset while opacity determines how much of the map beneath is visible.
001 var heatmapLayer = L.TileLayer.heatMap({
002 radius: { value: 15000, absolute: true },
003 opacity: 0.8
004 //gradient step
005 });
Gradient object
The gradient object contains key/value pairs that relate to how much weight an area has (from 0 to 1) and a CSS colour value. We’ll customise this to be greyscale (also known as the blackbody spectrum) as colours can lead to perception of gradients that aren’t actually present to avoid confusion with the UX tool.
001 gradient: {
002 0.45: “rgb(230,230,230)”,
003 0.55: “rgb(172,172,172)”,
004 0.65: “rgb(114,114,114)”,
005 0.95: “rgb(56,56,56)”,
006 1.0: “rgb(0,0,0)”
007 }
008
Fetch and set
We’re going to visualise a traffic dataset from data.gov.uk – you can find the JSON file (data.json) on the resource disc included with this issue. The heat map layer that we created makes it extremely simple to set the map’s dataset simply by calling the setData method. This computes where the data relates to on the map.
001 $.get(‘data.json’, function(dataset) {
002 heatmapLayer.setData(dataset.data);
003 });
Expected JSON
The dataset is simple, our example one has an array of objects that have a longitude, latitude, and value. If your dataset doesn’t have a value, don’t worry as it defaults to 1. The maximum value in our dataset is nine so we’ll set that. Thanks to QuadTree, heatmap.js is able to deftly handle large datasets – ours has 7,981 points.
001 {
002 “max”: 9,
003 “data”: [{
004 “lon”: “-0.088176”,
005 “lat”: “51.509763”,
006 “value”: “2”
007 }]
008 }
Init Leaflet map
Putting it all together we initialise a new Leaflet map and centre it on the middle of the UK – we’ll also set a zoom level that fits mainland UK in view and then apply the two layers (base and then the heat map). ‘Map’ is the ID of the element that it’ll render itself inside.
001 var map = new L.Map(‘map’, {
002 center: new L.LatLng(54.559323, -4.174805),
003 zoom: 6,
004 layers: [baseLayer, heatmapLayer]
005 });
Conclusion
We’ve shown how you can use heatmap.js as a useful UX tool and in order to visualise a dataset. It does all of the heavy lifting for you while being versatile enough to be customised and applied to a variety of scenarios.