// // Open Web Analytics - An Open Source Web Analytics Framework // // Copyright 2010 Peter Adams. All rights reserved. // // Licensed under GPL v2.0 http://www.gnu.org/copyleft/gpl.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // $Id$ // /** * Javascript Heatmap Library * * @author Peter Adams * @web Open Web Analytics * @copyright Copyright © 2006-2010 Peter Adams * @license http://www.gnu.org/copyleft/gpl.html GPL v2.0 * @category owa * @package owa * @version $Revision$ * @since owa 1.2.1 */ OWA.heatmap = function(w, h) { this.docDimensions = this.getDim(document); w = w || this.docDimensions.w; h = h || this.docDimensions.h; OWA.debug("Canvas size: %s by %s", w, h); this.createCanvas(w,h); this.canvas = document.getElementById('owa_heatmap'); this.context = this.canvas.getContext('2d'); this.calcRegions(); }; OWA.heatmap.prototype = { options: { dotSize: 12, numRegions: 40, alphaIncrement:50, demoMode: false, liveMode: false, mapInterval: 1000, randomDataCount: 200, rowsPerFetch: 100, strokeRegions: false, svgUrl: OWA.getSetting('baseUrl')+'/modules/base/i/test.svg#f1', baseUrl: '', apiUrl: '' }, canvas: null, context: null, docDimensions: null, regions: new Array(), regionsMap: new Array(), regionWidth: null, regionHeight: null, dirtyRegions: new Object(), timer: '', clicks: '', nextPage: 1, more: true, lock: false, /** * Marks a region as dirty so that it can be re-rendered */ markRegionDirty: function(region_num) { if (region_num >= 0) { this.dirtyRegions[region_num] = true; OWA.debug("marking region dirty: %s", region_num); } else { OWA.debug("no region to mark dirty!"); } }, showControlPanel: function() { var that = this; jQuery('body').append('
'); jQuery('#owa_overlay').append(''); jQuery('#owa_overlay').append('
Start
'); jQuery('#owa_overlay_start').toggleClass('active'); jQuery('#owa_overlay').append('
Stop
'); jQuery('#owa_overlay').append('
X
'); jQuery('#owa_overlay_start').click(function(){that.startTimer()}); jQuery('#owa_overlay_stop').click(function(){that.stopTimer()}); jQuery('.owa_overlay_control').bind('click', function(){ jQuery(".owa_overlay_control").removeClass('active'); jQuery(this).addClass('active'); }); jQuery('#owa_overlay_end').click(function(){that.endSession()}); //eliminate session cookie when window closes. jQuery(window).unload(function() {OWA.endOverlaySession()}); }, /** * Main generation method. kicks off the timer if in liveMode */ generate: function() { this.showControlPanel(); this.applyBlur(); if (this.options.liveMode === true) { this.startTimer(); } else { this.map(); } }, endSession: function() { OWA.util.eraseCookie('owa_overlay', document.domain); window.close(); }, startTimer: function() { var that = this; this.timer = setInterval(function(){that.map()}, this.options.mapInterval); }, stopTimer: function() { if (!this.timer) return false; clearInterval(this.timer); }, /** * Gets data and plots it */ map: function() { if (this.lock == true) { OWA.debug("skipping data fetch due to lock."); return; } else { this.lock = true; } if (this.options.liveMode === true) { var more = this.checkForMoreClicks(); if (more === true) { OWA.debug('there are more clicks to fetch.'); var data = this.getData(); } else { OWA.debug('there are no more clicks to fetch.'); this.stopTimer(); } } else { var data = this.getData(); } }, /** * Gets data, random if in demoMode */ getData: function() { // get data if (this.options.demoMode === true) { return this.getRandomData(this.options.randomDataCount); } else { var data = this.fetchData(this.getNextPage()); return; } }, checkForMoreClicks: function() { return this.more; }, getNextPage: function() { return this.nextPage; }, setNextPage: function(page) { OWA.debug("setNextpage received page as %d", page); this.nextPage++; OWA.debug("setNextpage is setting page as %d", this.nextPage); }, setMore: function(bool) { this.more = bool; }, /** * Fetches data via ajax request */ fetchData: function(page) { var p = OWA.util.readCookie('owa_overlay'); //alert(unescape(p)); var params = OWA.util.parseCookieStringToJson(p); //params.action = 'base.reportOverlay'; //params.document_url = OWA.util.urlEncode(document.location); params.action = 'getDomClicks'; params.pageUrl = OWA.util.urlEncode(document.location); //params.document_url = document.location; //OWA.debug('encoded url: '+OWA.util.urlEncode(document.location)); params.resultsPerPage = this.options.rowsPerFetch; params.format = 'jsonp'; // add page number if one was passed in if (page) { OWA.debug("fetchData will fetch page %s", page); params.page = page; } //closure var that = this; jQuery.ajax({ url: OWA.getApiEndpoint(), data: OWA.util.nsParams(params), dataType: 'jsonp', jsonp: 'owa_jsonpCallback', success: function(data) { that.plotClickData(data); } }); }, plotClickData: function(data) { if (data) { //OWA.debug('setClicks says data is defined'); this.clicks = data; //set more flag if (data.more === true && data.more != null) { OWA.debug("plotClickData says more flag was set to true"); this.setMore(true); //set next page this.setNextPage(data.page); } else { OWA.debug("plotClickData says more flag was set to false"); this.setMore(false); } //plot dots //this.plotDots(this.getClicks()); this.plotDotsRound(this.getClicks()); this.lock = false; return true; } else { return false; } }, getClicks: function() { //OWA.debug("getClicks is logging %s", this.clicks['page']); return this.clicks.rows; }, /** * Looks up the a region's top lower right corner plot points */ getRegion: function(num) { //OWA.debug("Getting dims for region %s", num); return this.regions[num]; }, /** * Sets the color of a pixels a region based on their alpha values */ setColor: function(num) { OWA.debug("About to set color for region %s", num); var dims = this.getRegion(num); OWA.debug("set color coords %s %s", dims.x, dims.y); // get the actual pixel data from the region var canvasData = this.context.getImageData(dims.x, dims.y, this.regionWidth, this.regionHeight); var pix = canvasData.data; // Loop over each pixel and invert the color. for (var i = 0, n = pix.length; i < n; i += 4) { var rgb = this.getRgbFromAlpha(pix[i+3]); pix[i ] = Math.round(parseInt(rgb.r)); // red pix[i+1] = Math.round(parseInt(rgb.g)); // green pix[i+2] = Math.round(parseInt(rgb.b)); // blue } // Draw the ImageData object at the given (x,y) coordinates. this.context.putImageData(canvasData,dims.x,dims.y); }, getRgbFromAlpha : function(alpha) { var rgb = {'r': null, 'g': null, 'b': null}; // set colors based on current alpha value if( alpha <= 255 && alpha >= 235 ){ tmp = 255 - alpha; rgb.r = 255 - tmp; rgb.g = tmp * 12; } else if ( alpha <= 234 && alpha >= 200 ){ tmp = 234 - alpha; rgb.r = 255 - ( tmp * 8 ); rgb.g = 255; } else if ( alpha <= 199 && alpha >= 150 ){ tmp = 199 - alpha; rgb.g = 255; rgb.b = tmp * 5; } else if ( alpha <= 149 && alpha >= 100 ){ tmp = 149 - alpha; rgb.g = 255 - ( tmp * 5 ); rgb.b = 255; } else { rgb.b = 255; } return rgb; }, /** * Fills a region with grey * DEPRICATED */ fillRegion: function(num) { this.fillRectangle(this.regions[num].x, this.regions[num].y, this.regionWidth, this.regionHeight, "rgba(0,0,0, 0.5)"); }, strokeRegion: function(num) { this.context.strokeRect(this.regions[num].x, this.regions[num].y, this.regionWidth, this.regionHeight); }, /** * Fills a rectangle with an rgba value */ fillRectangle: function(x,y,w,h,rgba) { this.context.fillStyle = rgba; this.context.fillRect(x, y, w, h); }, /** * Fils all regions * DEPRICATED */ fillAllRegions: function() { for (var i=0, n = this.regions.length; i < n; i++) { //OWA.debug("region %s", i); this.fillRegion(i); } }, /** * Find the region that a set of coordinates falls into */ findRegion: function(x, y) { x = parseFloat(x); y = parseFloat(y); // walk the outer x map in ascending order OWA.debug("finding region for %s", x,y); for (i in this.regionsMap) { // look for the first value that is greater that or equals to the x coordinate if (this.regionsMap.hasOwnProperty(i)) { OWA.debug("regionmap i: %s", i); if (x <= i) { // For that x coordinate walk the inner map in ascending order OWA.debug("regionmap x chosen: %s. x was: %s", i, x); for ( n in this.regionsMap[i]) { // find the first value that is greater than or equals to the y coordinate if (this.regionsMap[i].hasOwnProperty(n)) { //OWA.debug("what is this %s", n); if (y <= n) { // Return the region number OWA.debug("stopping on regionmap y: %s", n); OWA.debug("regionmap y: %s", n); OWA.debug("region chosen: %s (i = %s, n = %s)", this.regionsMap[i][n], i , n); return this.regionsMap[i][n]; } } } } } } // Something went wrong as the coordinate does not fit into any region //OWA.debug("can't find region for %s %s", x, y); }, /** * Chop the document up into a set of regions */ calcRegions: function() { // Calculate the region dimensions. This is controlled by the option numRegion. // More regions will increase the speed of rendering. this.regionWidth = Math.round((this.docDimensions.w / this.options.numRegions) * 100)/100; this.regionHeight = Math.round((this.docDimensions.h / this.options.numRegions) * 100)/100; OWA.debug("Region dims: %s %s", this.regionWidth, this.regionHeight); var count = 0; // y loop for (var y = this.regionHeight, n = this.docDimensions.h; y <= n; y+=this.regionHeight) { y = Math.round(y * 100)/100 -.00; OWA.debug("calcregions y value", y); // x loop for (var x = this.regionWidth, nn = this.docDimensions.w; x <= nn; x+=this.regionWidth) { x = Math.round(x * 100)/100 -.00; // add region this.regions[count] = {'x': x - this.regionWidth, 'y': y - this.regionHeight}; //create inner y map if (!this.regionsMap[x]) { this.regionsMap[x] = Array(); } //add region to inner map this.regionsMap[x][y] = count; //OWA.debug("adding to map: %s %s %s",x,y,count); if (this.options.strokeRegions === true) { this.strokeRegion(count); } count++; } //OWA.debug("x Count: %s", this.regions.length); } }, /** * Generates random data * Takes an int */ getRandomData: function(count) { var data = Array(); for (var li=0; li < count; li++) { var x = Math.round(Math.floor(Math.random()*(this.docDimensions.w-this.options.dotSize))); var y = Math.round(Math.floor(Math.random()*(this.docDimensions.h-this.options.dotSize))); data.push({'x':x,'y':y}); } return data; }, /** * Plots dots on a the canvas * */ plotDotsRound: function(data) { for( var i = 0; i < data.length; i++) { if ((data[i].x + this.options.dotSize) > this.docDimensions.w) { data[i].x = data[i].x - this.options.dotSize; } if ((data[i].y + this.options.dotSize) > this.docDimensions.h) { data[i].y = data[i].y - this.options.dotSize; } if ((data[i].x <= this.docDimensions.w) && (data[i].y <= this.docDimensions.h)) { OWA.debug("plotting %s %s", data[i].x, data[i].y); } else { OWA.debug("not getting image data. coordinates %s %s are outside the canvas", data[i].x, data[i].y); continue; } if ((data[i].x >= 0) && (data[i].y >= 0)) { OWA.debug("plotting %s %s", data[i].x, data[i].y); } else { OWA.debug("not getting image data. coordinates %s %s less than zero.", data[i].x, data[i].y); continue; } // create a radial gradient with the defined parameters. we want to draw an alphamap var rgr = this.context.createRadialGradient(data[i].x,data[i].y,7,data[i].x,data[i].y,this.options.dotSize); // the center of the radial gradient has .1 alpha value rgr.addColorStop(0, 'rgba(0,0,0,0.1)'); // and it fades out to 0 rgr.addColorStop(1, 'rgba(0,0,0,0)'); // drawing the gradient this.context.fillStyle = rgr; this.context.fillRect(data[i].x-this.options.dotSize,data[i].y-this.options.dotSize,2*this.options.dotSize,2*this.options.dotSize); // mark region dirty this.markRegionDirty(this.findRegion(data[i].x,data[i].y)); } // color dirty Regions this.processDirtyRegions(); }, processDirtyRegions: function() { for (i in this.dirtyRegions) { if (this.dirtyRegions.hasOwnProperty(i)) { this.setColor(i); } } this.dirtyRegions = new Array(); }, applyBlur: function() { // apply gausian blur this.canvas.className = 'owa_blur'; }, getDocHeight : function() { var D = document; return Math.max( Math.max(D.body.scrollHeight, D.documentElement.scrollHeight), Math.max(D.body.offsetHeight, D.documentElement.offsetHeight), Math.max(D.body.clientHeight, D.documentElement.clientHeight) ); }, getDim: function(d) { var w=200, h=200, scr_h, off_h; //OWA.setSetting('debug', true); if( d.height ) { //OWA.debug("doc dims %s %s", d.width, d.height); //return {'w':d.width,'h':d.height}; } if( d.body ) { if( d.body.scrollHeight ) { h=scr_h=d.body.scrollHeight; w=d.body.scrollWidth; } if( d.body.offsetHeight ) { h=off_h=d.body.offsetHeight; w=d.body.offsetWidth; } if( scr_h && off_h ) h=Math.max(scr_h, off_h); } h = this.getDocHeight(); OWA.debug("doc dims %s %s", w, h); return {'w': w,'h':h}; }, createCanvas: function(w, h) { var that = this; jQuery("body").append(''); }, getDataPoints: function() { } }