|
// |
|
// Canvas Heatmap |
|
// Version - 0.1 |
|
// |
|
// This tiny lib implement a generic heatmap feature for canvas as described in |
|
// this Gist : https://gist.github.com/madsgraphics/4676852. |
|
// |
|
// The purposes of this lib is to bind DOM and custom events on canvas shapes. |
|
// |
|
// by |
|
// MAD <[email protected]> |
|
// |
|
// Tri-license - WTFPL | MIT | BSD |
|
// |
|
|
|
(function(undefined) { |
|
|
|
// EVENT |
|
// ----- |
|
// |
|
// Define an Event Object to handle th custom events stacks on shapes |
|
function Event(heatmap) { |
|
// the events stacks |
|
this.stack = {}; |
|
// a hover flag to check the mouseover/mouseout |
|
this.isOver = false; |
|
// reference to the parent heatmap |
|
this.heatmap = heatmap; |
|
} |
|
|
|
// Bind event with `on` method |
|
Event.prototype.on = function (name, handler) { |
|
var _this = this; |
|
|
|
// Create the stack for the event if it doesn't exists |
|
if (this.stack[name] === undefined) { |
|
this.stack[name] = []; |
|
} |
|
|
|
// Add handler to stack |
|
this.stack[name].push(handler); |
|
|
|
// If heatmap already registered an event listener for this event, exit. |
|
if (this.heatmap.listeners.indexOf(name) !== -1) { return this; } |
|
|
|
// Add event listener to DOM canvas element for this event |
|
this.heatmap.canvas.addEventListener(name, function(e) { |
|
_this.heatmap._trigger(e); |
|
}, false); |
|
this.heatmap.listeners.push(name); |
|
|
|
// Return `this` event object to permit chaining "a la jQuery" |
|
return this; |
|
}; |
|
|
|
// Unbind event |
|
Event.prototype.off = function (name, handler) { |
|
// Unregister all events for the event if no handler is specified |
|
if (handler === undefined) { |
|
this.stack[name] = undefined; |
|
} |
|
|
|
// or unregister only given handler |
|
else { |
|
for(var _i = 0, _len = this.stack[name].length; _i < _len; _i++) { |
|
if (handler === this.stack[name][_i]) { |
|
this.stack[name][_i] = undefined; |
|
} |
|
} |
|
} |
|
}; |
|
|
|
// Launch event stack |
|
Event.prototype.fire = function(name, evt) { |
|
// Exit if there's no stack for this event |
|
if (this.stack[name] === undefined) { return; } |
|
|
|
// Loop on stack event's handlers and call them |
|
for (var _i = 0, _len = this.stack[name].length; _i < _len; _i++) { |
|
this.stack[name][_i].call(this, evt); |
|
} |
|
}; |
|
|
|
// HEATMAP |
|
// ------- |
|
// |
|
// The heatmap object. Take the DOM canvas element as argument |
|
function Heatmap(el) { |
|
this.canvas = el; |
|
|
|
this._init(); |
|
} |
|
|
|
// _initializer |
|
Heatmap.prototype._init = function () { |
|
// Create a non-DOM map canvas |
|
this.map = document.createElement('canvas'); |
|
// set it the same size as the DOM reference canvas |
|
this.map.width = this.canvas.width; |
|
this.map.height = this.canvas.height; |
|
|
|
// store contexts for convenience |
|
this.contexts = { |
|
canvas : this.canvas.getContext('2d'), |
|
map : this.map.getContext('2d') |
|
}; |
|
|
|
// Prepare the shape (for bind events) and listeners references |
|
this.shapes = {}; |
|
this.listeners = []; |
|
}; |
|
|
|
// Add a custom shape the the canvas |
|
// |
|
// It will add the shape both on canvas and on map. It take the drawing |
|
// function as argument. |
|
Heatmap.prototype.addShape = function (drawFunc) { |
|
// Create a random rgb value to fill the shape on the heatmap |
|
var r = Math.floor(Math.random()*256) |
|
, g = Math.floor(Math.random()*256) |
|
, b = Math.floor(Math.random()*256) |
|
// use it as a UID |
|
, uid = r + ':' + g + ':' + b |
|
// extract arguments for later use |
|
, args = Array.prototype.slice.call(arguments, 1); |
|
|
|
// Call drawfunc on canvas with potential arguments |
|
drawFunc.apply(this.contexts.canvas, args); |
|
|
|
// Then call it again on map after defined the fill color with the |
|
// randomized RGB value. |
|
this.contexts.map.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'; |
|
drawFunc.apply(this.contexts.map, args); |
|
|
|
// Store the shape for later use as an Event instance |
|
// add an empty mousemove handler to detect 'mouseover' and 'mouseout' evt |
|
// and return it |
|
this.shapes[uid] = new Event(this); |
|
this.shapes[uid].on('mousemove', function() { return; }); |
|
return this.shapes[uid]; |
|
}; |
|
|
|
// The trigger called to fire events |
|
Heatmap.prototype._trigger = function (e) { |
|
// Detect RGB-UID value under the pointer |
|
var pix = this._getRGB(e.pageX - e.target.offsetLeft, e.pageY - e.target.offsetTop) |
|
, uid; |
|
|
|
if (e.type === 'mousemove') { |
|
// on mousemove, first loop on shapes to detect mouseout actions |
|
for (uid in this.shapes) { |
|
if (pix !== uid && this.shapes[uid].isOver) { |
|
this.shapes[uid].isOver = false; |
|
this.shapes[uid].fire('mouseout', e); |
|
} |
|
} |
|
// then call mouseover event on the current hovered shape (if exists) |
|
if (this.shapes[pix] !== undefined && !this.shapes[pix].isOver) { |
|
this.shapes[pix].isOver = true; |
|
this.shapes[pix].fire('mouseover', e); |
|
} |
|
} |
|
|
|
// Exit if there's no shape |
|
if (this.shapes[pix] === undefined) { return; } |
|
|
|
// call the correponding stack event for the correct shape |
|
this.shapes[pix].fire(e.type, e); |
|
}; |
|
|
|
// return the current RGB color under the pointer |
|
Heatmap.prototype._getRGB = function (x, y) { |
|
var pixelData = this.contexts.map.getImageData(x, y, 1, 1).data; |
|
return pixelData[0] + ':' + pixelData[1] + ':' + pixelData[2]; |
|
}; |
|
|
|
// Export Heatmap to the global scope |
|
window.Heatmap = Heatmap; |
|
|
|
})(); |