/* * heatmap.js 1.0 - JavaScript Heatmap Library * * Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at) * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license. */ (function(w){ // the heatmapFactory creates heatmap instances var heatmapFactory = (function(){ // store object constructor // a heatmap contains a store // the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added function store(hmap){ var _ = { // data is a two dimensional array // a datapoint gets saved as data[point-x-value][point-y-value] // the value at [point-x-value][point-y-value] is the occurrence of the datapoint data: [], // tight coupling of the heatmap object heatmap: hmap }; // the max occurrence - the heatmaps radial gradient alpha transition is based on it this.max = 0; this.get = function(key){ return _[key]; }, this.set = function(key, value){ _[key] = value; }; }; store.prototype = { // function for adding datapoints to the store // datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence addDataPoint: function(x, y){ if(x < 0 || y < 0) return; var heatmap = this.get("heatmap"), data = this.get("data"); if(!data[x]) data[x] = []; if(!data[x][y]) data[x][y] = 1; // if count parameter is set increment by count otherwise by 1 data[x][y]+=(arguments.length<3)?1:arguments[2]; // do we have a new maximum? if(this.max < data[x][y]){ this.max = data[x][y]; // max changed, we need to redraw all existing(lower) datapoints heatmap.get("actx").clearRect(0,0,heatmap.get("width"),heatmap.get("height")); for(var one in data) for(var two in data[one]) heatmap.drawAlpha(one, two, data[one][two]); // @TODO // implement feature // heatmap.drawLegend(); ? return; } heatmap.drawAlpha(x, y, data[x][y]); }, setDataSet: function(obj){ this.max = obj.max; var heatmap = this.get("heatmap"), data = this.get("data"), d = obj.data, dlen = d.length; // clear the heatmap before the data set gets drawn heatmap.clear(); while(dlen--){ var point = d[dlen]; heatmap.drawAlpha(point.x, point.y, point.count); if(!data[point.x]) data[point.x] = []; if(!data[point.x][point.y]) data[point.x][point.y] = 1; data[point.x][point.y]+=point.count; } // Add event after all done // 10-22-2011 by Jeffri Hong if ( typeof(obj.callback) == 'function' ) obj.callback(); }, exportDataSet: function(){ var data = this.get("data"); var exportData = []; for(var one in data){ // jump over undefined indexes if(one === undefined) continue; for(var two in data[one]){ if(two === undefined) continue; // if both indexes are defined, push the values into the array exportData.push({x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two]}); } } return exportData; }, generateRandomDataSet: function(points){ var heatmap = this.get("heatmap"), w = heatmap.get("width"), h = heatmap.get("height"); var randomset = {}, max = Math.floor(Math.random()*1000+1); randomset.max = max; var data = []; while(points--){ data.push({x: Math.floor(Math.random()*w+1), y: Math.floor(Math.random()*h+1), count: Math.floor(Math.random()*max+1)}); } randomset.data = data; this.setDataSet(randomset); } }; // heatmap object constructor function heatmap(config){ // private variables var _ = { radiusIn : 20, radiusOut : 40, element : {}, canvas : {}, acanvas: {}, ctx : {}, actx : {}, visible : true, width : 0, height : 0, max : false, gradient : false, opacity: 180 }; // heatmap store containing the datapoints and information about the maximum // accessible via instance.store this.store = new store(this); this.get = function(key){ return _[key]; }, this.set = function(key, value){ _[key] = value; }; // configure the heatmap when an instance gets created this.configure(config); // and initialize it this.init(); }; // public functions heatmap.prototype = { configure: function(config){ if(config.radius){ var rout = config.radius, rin = parseInt(rout/2); } this.set("radiusIn", rin || 15), this.set("radiusOut", rout || 40), this.set("element", (config.element instanceof Object)?config.element:document.getElementById(config.element)); this.set("visible", config.visible); this.set("max", config.max || false); this.set("gradient", config.gradient || { 0.45: "rgb(0,0,255)", 0.55: "rgb(0,255,255)", 0.65: "rgb(0,255,0)", 0.95: "yellow", 1.0: "rgb(255,0,0)"}); // default is the common blue to red gradient this.set("opacity", parseInt(255/(100/config.opacity), 10) || 180); this.set("width", config.width || 0); this.set("height", config.height || 0); }, init: function(){ this.initColorPalette(); var canvas = document.createElement("canvas"), acanvas = document.createElement("canvas"), element = this.get("element"); this.set("canvas", canvas); this.set("acanvas", acanvas); canvas.width = acanvas.width = element.style.width.replace(/px/,"") || this.getWidth(element); this.set("width", canvas.width); canvas.height = acanvas.height = element.style.height.replace(/px/,"") || this.getHeight(element); this.set("height", canvas.height); canvas.style.position = acanvas.style.position = "absolute"; canvas.style.top = acanvas.style.top = "0"; canvas.style.left = acanvas.style.left = "0"; canvas.style.zIndex = 1000000; if(!this.get("visible")) canvas.style.display = "none"; this.get("element").appendChild(canvas); this.set("ctx", canvas.getContext("2d")); this.set("actx", acanvas.getContext("2d")); }, initColorPalette: function(){ var canvas = document.createElement("canvas"); canvas.width = "1"; canvas.height = "256"; var ctx = canvas.getContext("2d"); var grad = ctx.createLinearGradient(0,0,1,256), gradient = this.get("gradient"); for(var x in gradient){ grad.addColorStop(x, gradient[x]); } ctx.fillStyle = grad; ctx.fillRect(0,0,1,256); this.set("gradient", ctx.getImageData(0,0,1,256).data); delete canvas; delete grad; delete ctx; }, getWidth: function(element){ var width = element.offsetWidth; if(element.style.paddingLeft) width+=element.style.paddingLeft; if(element.style.paddingRight) width+=element.style.paddingRight; return width; }, getHeight: function(element){ var height = element.offsetHeight; if(element.style.paddingTop) height+=element.style.paddingTop; if(element.style.paddingBottom) height+=element.style.paddingBottom; return height; }, colorize: function(x, y){ // get the private variables var width = this.get("width"), radiusOut = this.get("radiusOut"), height = this.get("height"), actx = this.get("actx"), ctx = this.get("ctx"); var x2 = radiusOut*2; if(x+x2>width) x=width-x2; if(x<0) x=0; if(y<0) y=0; if(y+x2>height) y=height-x2; // get the image data for the mouse movement area var image = actx.getImageData(x,y,x2,x2), // some performance tweaks imageData = image.data, length = imageData.length, palette = this.get("gradient"), opacity = this.get("opacity"); // loop thru the area for(var i=3; i < length; i+=4){ // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha var alpha = imageData[i], offset = alpha*4; if(!offset) continue; // we ve started with i=3 // set the new r, g and b values imageData[i-3]=palette[offset]; imageData[i-2]=palette[offset+1]; imageData[i-1]=palette[offset+2]; // we want the heatmap to have a gradient from transparent to the colors // as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value imageData[i] = (alpha < opacity)?alpha:opacity; } // the rgb data manipulation didn't affect the ImageData object(defined on the top) // after the manipulation process we have to set the manipulated data to the ImageData object image.data = imageData; ctx.putImageData(image,x,y); }, drawAlpha: function(x, y, count){ // storing the variables because they will be often used var r1 = this.get("radiusIn"), r2 = this.get("radiusOut"), ctx = this.get("actx"), max = this.get("max"), // create a radial gradient with the defined parameters. we want to draw an alphamap rgr = ctx.createRadialGradient(x,y,r1,x,y,r2), xb = x-r2, yb = y-r2, mul = 2*r2; // the center of the radial gradient has .1 alpha value rgr.addColorStop(0, 'rgba(0,0,0,'+((count)?(count/this.store.max):'0.1')+')'); // and it fades out to 0 rgr.addColorStop(1, 'rgba(0,0,0,0)'); // drawing the gradient ctx.fillStyle = rgr; ctx.fillRect(xb,yb,mul,mul); // finally colorize the area this.colorize(xb,yb); }, toggleDisplay: function(){ var visible = this.get("visible"), canvas = this.get("canvas"); if(!visible) canvas.style.display = "block"; else canvas.style.display = "none"; this.set("visible", !visible); }, // dataURL export getImageData: function(){ return this.get("canvas").toDataURL(); }, clear: function(){ var w = this.get("width"), h = this.get("height"); this.store.set("data",[]); // @TODO: reset stores max to 1 //this.store.max = 1; this.get("ctx").clearRect(0,0,w,h); this.get("actx").clearRect(0,0,w,h); } }; return { create: function(config){ return new heatmap(config); }, util: { mousePosition: function(ev){ // this doesn't work right // rather use /* // this = element to observe var x = ev.pageX - this.offsetLeft; var y = ev.pageY - this.offsetTop; */ var x, y; if (ev.layerX) { // Firefox x = ev.layerX; y = ev.layerY; } else if (ev.offsetX) { // Opera x = ev.offsetX; y = ev.offsetY; } if(typeof(x)=='undefined') return; return [x,y]; } } }; })(); w.h337 = w.heatmapFactory = heatmapFactory; })(window);