Introducing MooTools HeatMap

By  on  

MooTools HeatMap

It's often interesting to think about where on a given element, whether it be the page, an image, or a static DIV, your users are clicking.  With that curiosity in mind, I've created HeatMap: a MooTools class that allows you to detect, load, save, and display spots on a given area where a user has clicked.

The CSS

There's really only one CSS declaration you'll need to make with HeatMap, and that's the CSS class that represents how a spot should look.  A sample spot CSS class could look like:

.heatmap-spot	{ 
	width:6px; 
	height:6px; 
	margin-top:-3px; 
	margin-left:-3px;
	-webkit-border-radius:4px; 
	-moz-border-radius:4px; 
	background:#fff; 
	position:absolute; /* important! */
	z-index:200; 
}

HeatMap was created to allow spot styling to look however you'd like. Note that you'll want to add negative margins to the spot depending on how large you create the spot.

The MooTools JavaScript

The class is relatively compact but grew larger than I had expected.  HeatMap allows for easy loading and saving of spots with minimal code.  Here's the complete class:

var HeatMap = new Class({
	options: {
		event: 'click',
		load: {
			// request settings here
		},
		method: 'get',
		save: {
			// request settings here
		},
		spotClass: 'heatmap-spot',
		zone: ''/*,
		onClick: $empty,
		onSpot: $empty
		*/
	},
	Implements: [Options,Events],
	initialize: function(element,options) {
		this.element = document.id(element).setStyle('position','relative');
		this.setOptions(options);
		this.newClicks = [];
		this.oldClicks = [];
		this.attachEvents();
	},
	attachEvents: function() {
		var self = this;
		this.clickEvent = function(e) {
			var obj = self.getRelativePosition(e.page.x,e.page.y);
			obj.spot = self.createSpot(obj.x,obj.y);
			self.newClicks.push(obj);
		};
		this.element.addEvent(this.options.event,this.clickEvent);
	},
	detachEvents: function() {
		this.element.removeEvent(this.options.event,this.clickEvent);
	},
	getRelativePosition: function(x,y) {
		var position = this.element.getPosition();
		return { x: x - position.x, y: y - position.y };
	},
	load: function() {
		if(!this.loadRequest) this.loadRequest = new Request.JSON(this.options.load);
		if(!this.options.load.onSuccess && !this.loadSuccess) {
			this.loadSuccess = function(json) {
				json.each(function(click,i) {
					json[i].spot = this.createSpot(click.x,click.y);
					this.oldClicks.push(json[i]);
				},this);
			}.bind(this);
			this.loadRequest.addEvent('success',this.loadSuccess);
		}
		this.loadRequest[this.options.method]({
			load: 1,
			zone: this.options.zone
		});
		return this;
	},
	save: function(data) {
		if(!this.sendRequest) this.sendRequest = new Request.JSON(this.options.save);
		if(this.newClicks.length) {
			this.sendRequest.addEvent('success',function() {
				this.newClicks.each(function(click) {
					this.oldClicks.push(this.createSpot({ x: click.x, y:click.y }));
				},this);
				this.newClicks = [];
			}.bind(this));
			this.sendRequest[this.options.method]({
				save: 1,
				zone: this.options.zone,
				data: this.newClicks
			});
		}
		return this;
	},
	createSpot: function(x,y) {
		var spot = new Element('div',{
			'class': this.options.spotClass,
			styles: {
				top: y.toInt(),
				left: x.toInt()
			}
		}).inject(this.element);
		this.fireEvent('spot',[spot,x,y]);
		return spot;
	}
});

Arguments for HeatMap include:

  • element: the element with which to listen for clicks on
  • options: options for the class instance

Options for HeatMap include:

  • event: (string, defaults to event) the event to listen for -- defaults to click
  • load: (object, defaults to {}) the Request.JSON options object for loading spots
  • method: (string, defaults to "get") the Request.JSON request type
  • save: (object, defaults to {}) the Request.JSON options object for saving spots
  • spotClass: (string, defaults to 'heatmap-spot') the CSS class for styling a spot
  • zone: (string, defaults to '') the "zone" by which the click will be saved under; especially important if more than one spot is one the page.

Events for HeatMap include:

  • onSpot: fires when a spot is created.

A relatively simple class.  The class could have more complexity but I've chosen to keep it simple for iteration one.

HeatMap Usage

Using HeatMap is as simple as this:

/* usage */
window.addEvent('domready',function() {
	map = new HeatMap('ricci-map',{
		zone: 'cricci',
		save: { url: 'heat-map.php' },
		load: { url: 'heat-map.php' },
		onSpot: function(spot) {
			spot.setStyle('opacity',0).fade(1);
		}
	});
	document.id('loader').addEvent('click',function() {
		map.load();
	});
	document.id('saver').addEvent('click',function() {
		map.save();
	});
});

Much simpler than you had probably imagined!  I'd recommend using click as the event -- using other types of events could be confusing to users and could result in massive amounts of data for mouseenter events.

The MySQL Table

My MySQL table looks as follows:

CREATE TABLE `example_heatmap` (
  `click_id` mediumint(6) NOT NULL auto_increment,
  `zone` varchar(60) NOT NULL default '',
  `x` smallint(5) NOT NULL default '0',
  `y` smallint(5) NOT NULL default '0',
  `date_clicked` datetime NOT NULL,
  PRIMARY KEY  (`click_id`)
) ENGINE=MyISAM AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;

How you choose to set up the SQL side of this is entirely up to you.

The PHP Script

A few thing I'd like to point out about the server-side handling of HeatMap:

  1. You can use any server-side language to facilitate the loading of saving of spots -- I simply used PHP because it's what I'm most familiar with.
  2. Save your complaints about my usage of PHP's native mysql functions and the lack of validation -- my focus with this post is the JavaScript class.

Without further adieu, here's a PHP solution for saving and loading spots:

/* load  */
if(isset($_GET['load'])) {
	
	/* vars */
	$spots = array();
	
	/* connect to the db */
	$connection = mysql_connect('localhost','dbuser','dbpass');
	mysql_select_db('dbname',$connection);
	
	/* get spots */
	$query = 'SELECT * FROM example_heatmap WHERE zone = \''.mysql_escape_string($_GET['zone']).'\' LIMIT 2000';
	$result = mysql_query($query,$connection);
	while($record = mysql_fetch_assoc($result)) {
		$spots[] = $record;
	}
	
	/* close db connection */
	mysql_close($connection);
	
	/* return result */
	$json = json_encode($spots);
	echo $json;
	die();
}
/* save */
elseif(isset($_GET['save']) && isset($_GET['data']) && count($_GET['data'])) {
	
	/* vars */
	$query = 'INSERT INTO example_heatmap (zone,x,y,date_clicked) VALUES ';
	$queryRecords = array();
	$records = 0;
	
	/* connect to the db */
	$connection = mysql_connect('localhost','dbuser','dbpass');
	mysql_select_db('dbname',$connection);
	
	/* save! */
	foreach($_GET['data'] as $data) {
		$queryRecords[] =  '(\''.mysql_escape_string($_GET['zone']).'\','.mysql_escape_string($data['x']).','.mysql_escape_string($data['y']).',NOW())';
		$records++;
	}
	
	/* execute query, close */
	$query.= implode(',',$queryRecords);
	mysql_query($query,$connection);
	mysql_close($connection);
	
	/* return result */
	die(count($records));
}

I prefer to use one script for both the saving and loading of spots -- using one script cuts down on the number of files you need and the logic to handle multiple functionality isn't difficult to organize within that one file.

Bring the Heat!

MooTools HeatMap is something I find incredibly fun.  You could use HeatMap on an image, a static DIV, or the entire body.  If you don't want the user to see spots and simply want to track their clicks, you could hide spots and periodically save clicks.  Have fun with this class and let me know if you have suggestions!

Recent Features

  • By
    Welcome to My New Office

    My first professional web development was at a small print shop where I sat in a windowless cubical all day. I suffered that boxed in environment for almost five years before I was able to find a remote job where I worked from home. The first...

  • By
    Vibration API

    Many of the new APIs provided to us by browser vendors are more targeted toward the mobile user than the desktop user.  One of those simple APIs the Vibration API.  The Vibration API allows developers to direct the device, using JavaScript, to vibrate in...

Incredible Demos

  • By
    MooTools HTML Police: dwMarkupMarine

    We've all inherited rubbish websites from webmasters that couldn't master valid HTML. You know the horrid markup: paragraph tags with align attributes and body tags with background attributes. It's almost a sin what they do. That's where dwMarkupMarine comes in.

  • By
    9 Incredible CodePen Demos

    CodePen is a treasure trove of incredible demos harnessing the power of client side languages.   The client side is always limited by what browsers provide us but the creativity and cleverness of developers always pushes the boundaries of what we think the front end can do.  Thanks to CSS...

Discussion

  1. OMG don’t load clicks, just trust me it works…. what have people done. Sweet work thou David, would be really useful on lots of sites – people pay big bucks for this stuff.

  2. Really really really really bad idea to allow loading of others’ clicks on that pic.
    –NSFW–

    I would really like to see more heat-map like results combined and color-coded

  3. I saw that coming. (snare). Cleared out again.

  4. Screw the post, who is this girl? :)

  5. Excellent job … again :P

  6. Great Job once again David!! any chance that this is available for jQuery?

    • I believe Chris Coyier has something similar at CSS-Tricks, but it’s a bit old now. Probably still works great though.

  7. kward

    Pretty cool, but it should NOT be called heatmap. Clickmap would be more accurate, as this doesn’t actually produced weighted results.

    • Possibly, I struggled with naming. In the end, however, it does show the heat of where users are clicking, mouseover-ing, etc. I couldn’t call it ClickMap because the user may try another event.

    • If you made the dots have like 10% opacity or something, then you could see each click, and where there was a lot of clicks close together, the group of spots would be more opaque…

  8. I didnt look @ the code or the doc yet, but would this be applicable on a web page (per say) or just on images?

    • Anywhere Avi. You can set the element argument to any element on the page. :)

    • WOW… Very impressive!! Great Job David… Great Job!

  9. Fabian

    I wouldn’t call that “HeatMap” either, thats absolutely not the right word. While we’re at it, check this out: http://www.patrick-wied.at/static/heatmap/

    • Hi Fabian, that also is pretty cool, but doesn’t it only works with browsers that support HTML5?

    • Fabian

      Has nothing todo with HTML5, you need a browser that knows about . :)

    • Hi Fabian, please excuse my ignorance here, but the example uses the tag to show the “hot spots”, when u try that same example on a non HTML5 supported browser, the example doest work. Hence the comment “works only with browsers that support HTML5”. So how does that have nothing to do with HTML5? and what do u mean when u say “you need a browser that knows about”?

  10. dd

    on v faire comme ci cela marchait !

    • oui, dac!….. alors aparament que faire semblant c’est toujours la solution desirable!! ;))

  11. This class will be renamed to ClickMap once the MooTools Forge allows for project change abilities.

  12. Cool! Curious, but in what ways would this be applied to website? BUt great piece of work!

  13. How about putting a light opacity on the dots. Then each time a dot is placed over another the opacity thickens and that way you get better visual information. Also you could do it with different colors or shades, i.e. each time a dot is clicked the color is replaced with a darker one #EEE -> #DDD.

  14. Petah

    It doesn’t appear to be saving clicks

  15. Lenny

    Can you do this so it saves automatically???

  16. Dan

    Hey David, thanks for the great class!

    I’ve found what looks like a bug – after a successful ‘save’ request, we push each old click into oldClicks using this.oldClicks.push(this.createSpot({ x: click.x, y:click.y })); . The signature for createSpot is (x,y) so this fails; I presume you intended to just pass the json object straight into .push()? With that modification it works beautifully.

    Anyway, I’m building something pretty awesome with this as inspiration – I’ll let you know when it’s ready for general consumption. Thanks again!

    Dan

  17. Andy

    I am amazed that no one asked this question. Let’s say i want to track clicks for my entire website , what will happen between different screen resolutions , so let’s say a user is on 1024×768 screen resolution , the other one from desktop again is on 1024×640 and one more from a mobile phone.

    At last i am looking at my website report using a totally different screen resolution let’s say 1280×720. Isn’t it going to create a mess.

    Any thoughts on this ( David you have life saver all the time, i want you to be my hero again ? )

  18. Andy

    I am amazed that no one asked this question. Let’s say i want to track clicks for my entire website , what will happen between different screen resolutions , so let’s say a user is on 1024×768 screen resolution , the other one from desktop again is on 1024×640 and one more from a mobile phone.

    At last i am looking at my website report using a totally different screen resolution let’s say 1280×720. Isn’t it going to create a mess.

    Any thoughts on this ( David you have been a life saver all the time, i want you to be my hero again ? )

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!