Official website for Web Designer - defining the internet through beautiful design
FOLLOW US ON:
Author: Steve Jenkins
17th October 2013

Add visual flair with HTML5 heatmaps

Create both real-time and static heat maps for your website – you’ll be amazed by its power and simplicity

Add visual flair with HTML5 heatmaps


DOWNLOAD TUTORIAL FILES

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 &copy; 
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.

  • Tell a Friend
  • Follow our Twitter to find out about all the latest web development, news, reviews, previews, interviews, features and a whole more.
    • Scott Krivacek

      Included files don’t work. map.html doesn’t show the heat layer.