diff --git a/heatmap.js b/heatmap.js index 0b9018a4..8355417f 100644 --- a/heatmap.js +++ b/heatmap.js @@ -1,22 +1,22 @@ -/* +/* * 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 + // 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: [], @@ -25,49 +25,49 @@ }; // the max occurrence - the heatmaps radial gradient alpha transition is based on it this.max = 1; - + 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 me = this, heatmap = me.get("heatmap"), data = me.get("data"); - - if(!data[x]) + + if(!data[x]) data[x] = []; - - if(!data[x][y]) + + if(!data[x][y]) data[x][y] = 0; - + // if count parameter is set increment by count otherwise by 1 data[x][y]+=(arguments.length<3)?1:arguments[2]; - + me.set("data", data); // do we have a new maximum? if(me.max < data[x][y]){ - + me.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 one in data) for(var two in data[one]) heatmap.drawAlpha(one, two, data[one][two]); - + // @TODO // implement feature - // heatmap.drawLegend(); ? + // heatmap.drawLegend(); ? return; } heatmap.drawAlpha(x, y, data[x][y]); @@ -86,13 +86,13 @@ while(dlen--){ var point = d[dlen]; heatmap.drawAlpha(point.x, point.y, point.count); - if(!data[point.x]) + if(!data[point.x]) data[point.x] = []; - - if(!data[point.x][point.y]) + + if(!data[point.x][point.y]) data[point.x][point.y] = 0; - - data[point.x][point.y]=point.count + + data[point.x][point.y]=point.count; } this.set("data", data); }, @@ -100,7 +100,7 @@ var me = this, data = me.get("data"), exportData = []; - + for(var one in data){ // jump over undefined indexes if(one === undefined) @@ -112,7 +112,7 @@ exportData.push({x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two]}); } } - + return { max: me.max, data: exportData }; }, generateRandomDataSet: function(points){ @@ -130,8 +130,8 @@ this.setDataSet(randomset); } }; - - + + // heatmap object constructor function heatmap(config){ // private variables @@ -149,15 +149,16 @@ max : false, gradient : false, opacity: 180, + premultiplyAlpha: false, debug: false }; // 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; }; @@ -165,18 +166,20 @@ this.configure(config); // and initialize it this.init(); - }; - + } + // public functions heatmap.prototype = { configure: function(config){ - var me = this; + var me = this, + rout, rin; + if(config.radius){ - var rout = config.radius, - rin = parseInt(rout/2, 10); + rout = config.radius; + rin = parseInt(rout/2, 10); } - me.set("radiusIn", rin || 15), - me.set("radiusOut", rout || 40), + me.set("radiusIn", rin || 15); + me.set("radiusOut", rout || 40); me.set("element", (config.element instanceof Object)?config.element:document.getElementById(config.element)); me.set("visible", config.visible); me.set("max", config.max || false); @@ -201,7 +204,7 @@ canvas = document.createElement("canvas"), acanvas = document.createElement("canvas"), element = me.get("element"); - + me.initColorPalette(); me.set("canvas", canvas); @@ -211,54 +214,68 @@ canvas.style.top = acanvas.style.top = "0"; canvas.style.left = acanvas.style.left = "0"; canvas.style.zIndex = 1000000; - + if(!me.get("visible")) canvas.style.display = "none"; me.get("element").appendChild(canvas); - // debugging purposes only + // debugging purposes only if(me.get("debug")) document.body.appendChild(acanvas); me.set("ctx", canvas.getContext("2d")); me.set("actx", acanvas.getContext("2d")); }, initColorPalette: function(){ - + var me = this, - canvas = document.createElement("canvas"); + canvas = document.createElement("canvas"), + gradient = me.get("gradient"), + ctx, grad, testData; + canvas.width = "1"; canvas.height = "256"; - var ctx = canvas.getContext("2d"), - grad = ctx.createLinearGradient(0,0,1,256), - gradient = me.get("gradient"); + ctx = canvas.getContext("2d"); + grad = ctx.createLinearGradient(0,0,1,256); + + // Test how the browser renders alpha by setting a partially transparent pixel + // and reading the result. A good browser will return a value reasonably close + // to what was set. Some browsers (e.g. on Android) will return a ridiculously wrong value. + testData = ctx.getImageData(0,0,1,1); + testData.data[0] = testData.data[3] = 64; // 25% red & alpha + testData.data[1] = testData.data[2] = 0; // 0% blue & green + ctx.putImageData(testData, 0, 0); + testData = ctx.getImageData(0,0,1,1); + me.set("premultiplyAlpha", (testData.data[0] < 60 || testData.data[0] > 70)); + for(var x in gradient){ grad.addColorStop(x, gradient[x]); } - + ctx.fillStyle = grad; ctx.fillRect(0,0,1,256); - + me.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) + if(element.style.paddingLeft){ width+=element.style.paddingLeft; - if(element.style.paddingRight) + } + if(element.style.paddingRight){ width+=element.style.paddingRight; - + } + return width; }, getHeight: function(element){ var height = element.offsetHeight; - if(element.style.paddingTop) + if(element.style.paddingTop){ height+=element.style.paddingTop; - if(element.style.paddingBottom) + } + if(element.style.paddingBottom){ height+=element.style.paddingBottom; - + } + return height; }, colorize: function(x, y){ @@ -268,56 +285,50 @@ radiusOut = me.get("radiusOut"), height = me.get("height"), actx = me.get("actx"), - ctx = me.get("ctx"); - - if (me.premultiplyAlpha == undefined) { - // Test how the browser renders alpha by setting a partially transparent pixel - // and reading the result. A good browser will return a value reasonably close - // to what was set. A bad browser (like android) will return a ridiculously wrong value. - var testData = ctx.getImageData(0,0,1,1); - testData.data[0] = testData.data[3] = 64; // 25% red & alpha - testData.data[1] = testData.data[2] = 0; // 0% blue & green - ctx.putImageData(testData, 0, 0); - testData = ctx.getImageData(0,0,1,1); - me.premultiplyAlpha = (testData.data[0] < 60 || testData.data[0] > 70); - } - - var x2 = radiusOut*2; - - if(x+x2>width) + ctx = me.get("ctx"), + x2 = radiusOut * 2, + premultiplyAlpha = me.get("premultiplyAlpha"), + palette = me.get("gradient"), + opacity = me.get("opacity"), + image, imageData, length, alpha, offset, finalAlpha; + + if(x+x2>width){ x=width-x2; - if(x<0) + } + if(x<0){ x=0; - if(y<0) + } + if(y<0){ y=0; - if(y+x2>height) + } + if(y+x2>height){ y=height-x2; + } // get the image data for the mouse movement area - var image = actx.getImageData(x,y,x2,x2), + image = actx.getImageData(x,y,x2,x2); // some performance tweaks - imageData = image.data, - length = imageData.length, - palette = me.get("gradient"), - opacity = me.get("opacity"); + imageData = image.data; + length = imageData.length; + // loop thru the area for(var i=3; i < length; i+=4){ - + // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha - var alpha = imageData[i], + alpha = imageData[i], offset = alpha*4; - + if(!offset) continue; - + // we ve started with i=3 // set the new r, g and b values - var finalAlpha = (alpha < opacity)?alpha:opacity; + finalAlpha = (alpha < opacity)?alpha:opacity; imageData[i-3]=palette[offset]; imageData[i-2]=palette[offset+1]; imageData[i-1]=palette[offset+2]; - if (me.premultiplyAlpha) { - // To fix bad browsers that premultiply incorrectly, we'll pass in a value scaled + if (premultiplyAlpha) { + // To fix browsers that premultiply incorrectly, we'll pass in a value scaled // appropriately so when the multiplication happens the correct value will result. imageData[i-3] /= 255/finalAlpha; imageData[i-2] /= 255/finalAlpha; @@ -331,7 +342,7 @@ // 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); + ctx.putImageData(image,x,y); }, drawAlpha: function(x, y, count){ // storing the variables because they will be often used @@ -344,11 +355,11 @@ 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/me.store.max):'0.1')+')'); + rgr.addColorStop(0, 'rgba(0,0,0,'+((count)?(count/me.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.fillStyle = rgr; ctx.fillRect(xb,yb,mul,mul); // finally colorize the area me.colorize(xb,yb); @@ -357,12 +368,12 @@ var me = this, visible = me.get("visible"), canvas = me.get("canvas"); - + if(!visible) canvas.style.display = "block"; else canvas.style.display = "none"; - + me.set("visible", !visible); }, // dataURL export @@ -373,9 +384,9 @@ var me = this, w = me.get("width"), h = me.get("height"); - + me.store.set("data",[]); - // @TODO: reset stores max to 1 + // @TODO: reset stores max to 1 //me.store.max = 1; me.get("ctx").clearRect(0,0,w,h); me.get("actx").clearRect(0,0,w,h); @@ -383,10 +394,9 @@ cleanup: function(){ var me = this; me.get("element").removeChild(me.get("canvas")); - delete me; } }; - + return { create: function(config){ return new heatmap(config); @@ -394,15 +404,15 @@ util: { mousePosition: function(ev){ // this doesn't work right - // rather use + // 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; @@ -412,11 +422,12 @@ } if(typeof(x)=='undefined') return; - + return [x,y]; } } }; })(); w.h337 = w.heatmapFactory = heatmapFactory; -})(window); \ No newline at end of file +})(window); + diff --git a/src/heatmap.js b/src/heatmap.js index 590cb771..fc0a88da 100644 --- a/src/heatmap.js +++ b/src/heatmap.js @@ -171,8 +171,9 @@ // public functions heatmap.prototype = { configure: function(config){ - var me = this; - var rout, rin; + var me = this, + rout, rin; + if(config.radius){ rout = config.radius; rin = parseInt(rout/2, 10); @@ -227,13 +228,14 @@ initColorPalette: function(){ var me = this, - canvas = document.createElement("canvas"); + canvas = document.createElement("canvas"), + gradient = me.get("gradient"), + ctx, grad, testData; + canvas.width = "1"; canvas.height = "256"; - var ctx = canvas.getContext("2d"), - grad = ctx.createLinearGradient(0,0,1,256), - gradient = me.get("gradient"), - testData; + ctx = canvas.getContext("2d"); + grad = ctx.createLinearGradient(0,0,1,256); // Test how the browser renders alpha by setting a partially transparent pixel // and reading the result. A good browser will return a value reasonably close @@ -256,19 +258,23 @@ }, getWidth: function(element){ var width = element.offsetWidth; - if(element.style.paddingLeft) + if(element.style.paddingLeft){ width+=element.style.paddingLeft; - if(element.style.paddingRight) + } + if(element.style.paddingRight){ width+=element.style.paddingRight; + } return width; }, getHeight: function(element){ var height = element.offsetHeight; - if(element.style.paddingTop) + if(element.style.paddingTop){ height+=element.style.paddingTop; - if(element.style.paddingBottom) + } + if(element.style.paddingBottom){ height+=element.style.paddingBottom; + } return height; }, @@ -280,30 +286,35 @@ height = me.get("height"), actx = me.get("actx"), ctx = me.get("ctx"), - premultiplyAlpha = me.get("premultiplyAlpha"); - - var x2 = radiusOut*2; + x2 = radiusOut * 2, + premultiplyAlpha = me.get("premultiplyAlpha"), + palette = me.get("gradient"), + opacity = me.get("opacity"), + image, imageData, length, alpha, offset, finalAlpha; - if(x+x2>width) + if(x+x2>width){ x=width-x2; - if(x<0) + } + if(x<0){ x=0; - if(y<0) + } + if(y<0){ y=0; - if(y+x2>height) + } + if(y+x2>height){ y=height-x2; + } // get the image data for the mouse movement area - var image = actx.getImageData(x,y,x2,x2), + image = actx.getImageData(x,y,x2,x2); // some performance tweaks - imageData = image.data, - length = imageData.length, - palette = me.get("gradient"), - opacity = me.get("opacity"); + imageData = image.data; + length = imageData.length; + // loop thru the area for(var i=3; i < length; i+=4){ // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha - var alpha = imageData[i], + alpha = imageData[i], offset = alpha*4; if(!offset) @@ -311,7 +322,7 @@ // we ve started with i=3 // set the new r, g and b values - var finalAlpha = (alpha < opacity)?alpha:opacity; + finalAlpha = (alpha < opacity)?alpha:opacity; imageData[i-3]=palette[offset]; imageData[i-2]=palette[offset+1]; imageData[i-1]=palette[offset+2];