среда, 13 апреля 2016 г.

JavaScript Browser Detection

github.com/ded/bowser/blob/master/bowser.js
useragentstring.com/pages/useragentstring.php

Here's how to detect browsers in 2016, including Microsoft Edge and detection of Blink:

// Opera 8.0+ (UA detection to detect Blink/v8-powered Opera)
isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;

// Firefox 1.0+
isFirefox = typeof InstallTrigger !== 'undefined';

// At least Safari 3+: "[object HTMLElementConstructor]"
isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;

// At least IE6
isIE = /*@cc_on!@*/false || !!document.documentMode;

// Edge 20+
isEdge = !isIE && !!window.StyleMedia;

// Chrome 1+
isChrome = !!window.chrome && !!window.chrome.webstore;

// Blink engine detection
isBlink = (isChrome || isOpera) && !!window.CSS;

The beauty of this approach is that it relies on browser engine properties, so it covers even derivative browsers, such as Yandex or Vivaldi, which are practically compatible with the major browsers whose engines they use. The exception is Opera, which is detected via user agent sniffing, but today (i.e. ver. 15 and up) even Opera is itself only a shell for Blink.

But it is better and universal solution!

// Returns an object with name and version of browser.
// Compatable across roughly 99.8% browsers.
function getBrowserInfo (userAgent) {
    var matchedUserAgent = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [] // ["MSIE 7", "MSIE", "7"]
        , temp
        , browser = {
              name: undefined
            , version: undefined
            , platform: navigator.platform
        };
    // Trident check (IE 11 and below)
    if (/trident/i.test(matchedUserAgent[1])) { // ["Trident/7", "Trident", "7"]
        temp = /\brv[ :]+(\d+)/g.exec(userAgent) || []; // ["rv:11", "11"]
        browser.name = 'MSIE'; // mark IE browser as MSIE
        browser.version = (parseInt(temp[1], 10) || 0);
    } else if (matchedUserAgent[1] === 'Chrome') { // ["Chrome/48", "Chrome", "48"]
        temp = userAgent.match(/\bOPR\/(\d+)/); // ["OPR/35", "OPR", "35"]
        if (temp !== null) {
            browser.name = 'Opera';
            browser.version = parseInt(temp[1], 10);
        } else {
            browser.name = 'MSIE' // matchedUserAgent[1]; // mark Edge browser as MSIE
            browser.version = parseInt(matchedUserAgent[2], 10);
        }
    } else {
        if (matchedUserAgent[2]) { // ["Chrome/48", "Chrome", "48"]
            matchedUserAgent = [matchedUserAgent[1], matchedUserAgent[2]]; // ["Chrome", "48"]
        } else { // ["Firefox", undefined]
            matchedUserAgent = [navigator.appName, navigator.appVersion, '-?']; // ["Netscape", "5.0 (Windows)", "-?"]
        }
        temp = userAgent.match(/version\/(\d+)/i);
        if (temp !== null) {
            matchedUserAgent.splice(1, 1, temp[1]);
        }
        browser.name = matchedUserAgent[0];
        browser.version = parseInt(matchedUserAgent[1], 10);
    }
    return browser;
}

var ua1 = 'Mozilla/5.0 (iPad; CPU OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D201'
    , ua2 = 'Mozilla/5.0 (iPad; CPU OS_7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D201 Safari/9537.53'
    , ua3 = navigator.userAgent
    , b1 = getBrowserInfo(ua1)
    , b2 = getBrowserInfo(ua2)
    , b3 = getBrowserInfo(ua3);

console.log('1', b1);
console.log('2', b2);
console.log('3', b3);

Original code

navigator.browser = (function(){
    var ua = navigator.userAgent,
          tem,
          M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
    if (/trident/i.test(M[1])) {
        tem =  /\brv[ :]+(\d+)/g.exec(ua) || [];
        return 'IE '+(tem[1] || '');
    }
    if (M[1] === 'Chrome') {
        tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
        if(tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
    }
    M= M[2] ? [M[1], M[2]]: [navigator.appName, navigator.appVersion, '-?'];
    if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
    return M.join(' ');
})();

JavaScript Heatmap, Clickmap and MouseMove Recorder

To create clickmap just listen to all the event in JQuery for document:
- blur
- focus
- focusin
- focusout
- load
- resize
- scroll
- unload
- click
- dblclick
- mousedown
- mouseup
- mousemove
- mouseover
- mouseout
- mouseenter
- mouseleave
- change
- select
- submit
- keydown
- keypress
- keyup
- error

Heatmap.js

patrick-wied.at/static/heatmapjs/
raw.githubusercontent.com/pa7/heatmap.js/master/build/heatmap.js



Source code:

/*
 * heatmap.js v2.0.2 | JavaScript Heatmap Library
 *
 * Copyright 2008-2016 Patrick Wied <heatmapjs@patrick-wied.at> - All rights reserved.
 * Dual licensed under MIT and Beerware license
 *
 * :: 2016-02-04 21:25
 */
;(function (name, context, factory) {

  // Supports UMD. AMD, CommonJS/Node.js and browser context
  if (typeof module !== "undefined" && module.exports) {
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    define(factory);
  } else {
    context[name] = factory();
  }

})("h337", this, function () {

// Heatmap Config stores default values and will be merged with instance config
var HeatmapConfig = {
  defaultRadius: 40,
  defaultRenderer: 'canvas2d',
  defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"},
  defaultMaxOpacity: 1,
  defaultMinOpacity: 0,
  defaultBlur: .85,
  defaultXField: 'x',
  defaultYField: 'y',
  defaultValueField: 'value',
  plugins: {}
};
var Store = (function StoreClosure() {

  var Store = function Store(config) {
    this._coordinator = {};
    this._data = [];
    this._radi = [];
    this._min = 0;
    this._max = 1;
    this._xField = config['xField'] || config.defaultXField;
    this._yField = config['yField'] || config.defaultYField;
    this._valueField = config['valueField'] || config.defaultValueField;

    if (config["radius"]) {
      this._cfgRadius = config["radius"];
    }
  };

  var defaultRadius = HeatmapConfig.defaultRadius;

  Store.prototype = {
    // when forceRender = false -> called from setData, omits renderall event
    _organiseData: function(dataPoint, forceRender) {
        var x = dataPoint[this._xField];
        var y = dataPoint[this._yField];
        var radi = this._radi;
        var store = this._data;
        var max = this._max;
        var min = this._min;
        var value = dataPoint[this._valueField] || 1;
        var radius = dataPoint.radius || this._cfgRadius || defaultRadius;

        if (!store[x]) {
          store[x] = [];
          radi[x] = [];
        }

        if (!store[x][y]) {
          store[x][y] = value;
          radi[x][y] = radius;
        } else {
          store[x][y] += value;
        }

        if (store[x][y] > max) {
          if (!forceRender) {
            this._max = store[x][y];
          } else {
            this.setDataMax(store[x][y]);
          }
          return false;
        } else{
          return {
            x: x,
            y: y,
            value: value,
            radius: radius,
            min: min,
            max: max
          };
        }
    },
    _unOrganizeData: function() {
      var unorganizedData = [];
      var data = this._data;
      var radi = this._radi;

      for (var x in data) {
        for (var y in data[x]) {

          unorganizedData.push({
            x: x,
            y: y,
            radius: radi[x][y],
            value: data[x][y]
          });

        }
      }
      return {
        min: this._min,
        max: this._max,
        data: unorganizedData
      };
    },
    _onExtremaChange: function() {
      this._coordinator.emit('extremachange', {
        min: this._min,
        max: this._max
      });
    },
    addData: function() {
      if (arguments[0].length > 0) {
        var dataArr = arguments[0];
        var dataLen = dataArr.length;
        while (dataLen--) {
          this.addData.call(this, dataArr[dataLen]);
        }
      } else {
        // add to store
        var organisedEntry = this._organiseData(arguments[0], true);
        if (organisedEntry) {
          this._coordinator.emit('renderpartial', {
            min: this._min,
            max: this._max,
            data: [organisedEntry]
          });
        }
      }
      return this;
    },
    setData: function(data) {
      var dataPoints = data.data;
      var pointsLen = dataPoints.length;


      // reset data arrays
      this._data = [];
      this._radi = [];

      for(var i = 0; i < pointsLen; i++) {
        this._organiseData(dataPoints[i], false);
      }
      this._max = data.max;
      this._min = data.min || 0;
   
      this._onExtremaChange();
      this._coordinator.emit('renderall', this._getInternalData());
      return this;
    },
    removeData: function() {
      // TODO: implement
    },
    setDataMax: function(max) {
      this._max = max;
      this._onExtremaChange();
      this._coordinator.emit('renderall', this._getInternalData());
      return this;
    },
    setDataMin: function(min) {
      this._min = min;
      this._onExtremaChange();
      this._coordinator.emit('renderall', this._getInternalData());
      return this;
    },
    setCoordinator: function(coordinator) {
      this._coordinator = coordinator;
    },
    _getInternalData: function() {
      return {
        max: this._max,
        min: this._min,
        data: this._data,
        radi: this._radi
      };
    },
    getData: function() {
      return this._unOrganizeData();
    }/*,

      TODO: rethink.

    getValueAt: function(point) {
      var value;
      var radius = 100;
      var x = point.x;
      var y = point.y;
      var data = this._data;

      if (data[x] && data[x][y]) {
        return data[x][y];
      } else {
        var values = [];
        // radial search for datapoints based on default radius
        for(var distance = 1; distance < radius; distance++) {
          var neighbors = distance * 2 +1;
          var startX = x - distance;
          var startY = y - distance;

          for(var i = 0; i < neighbors; i++) {
            for (var o = 0; o < neighbors; o++) {
              if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
                if (data[startY+i] && data[startY+i][startX+o]) {
                  values.push(data[startY+i][startX+o]);
                }
              } else {
                continue;
              }
            }
          }
        }
        if (values.length > 0) {
          return Math.max.apply(Math, values);
        }
      }
      return false;
    }*/
  };


  return Store;
})();

var Canvas2dRenderer = (function Canvas2dRendererClosure() {

  var _getColorPalette = function(config) {
    var gradientConfig = config.gradient || config.defaultGradient;
    var paletteCanvas = document.createElement('canvas');
    var paletteCtx = paletteCanvas.getContext('2d');

    paletteCanvas.width = 256;
    paletteCanvas.height = 1;

    var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
    for (var key in gradientConfig) {
      gradient.addColorStop(key, gradientConfig[key]);
    }

    paletteCtx.fillStyle = gradient;
    paletteCtx.fillRect(0, 0, 256, 1);

    return paletteCtx.getImageData(0, 0, 256, 1).data;
  };

  var _getPointTemplate = function(radius, blurFactor) {
    var tplCanvas = document.createElement('canvas');
    var tplCtx = tplCanvas.getContext('2d');
    var x = radius;
    var y = radius;
    tplCanvas.width = tplCanvas.height = radius*2;

    if (blurFactor == 1) {
      tplCtx.beginPath();
      tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
      tplCtx.fillStyle = 'rgba(0,0,0,1)';
      tplCtx.fill();
    } else {
      var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
      gradient.addColorStop(0, 'rgba(0,0,0,1)');
      gradient.addColorStop(1, 'rgba(0,0,0,0)');
      tplCtx.fillStyle = gradient;
      tplCtx.fillRect(0, 0, 2*radius, 2*radius);
    }



    return tplCanvas;
  };

  var _prepareData = function(data) {
    var renderData = [];
    var min = data.min;
    var max = data.max;
    var radi = data.radi;
    var data = data.data;

    var xValues = Object.keys(data);
    var xValuesLen = xValues.length;

    while(xValuesLen--) {
      var xValue = xValues[xValuesLen];
      var yValues = Object.keys(data[xValue]);
      var yValuesLen = yValues.length;
      while(yValuesLen--) {
        var yValue = yValues[yValuesLen];
        var value = data[xValue][yValue];
        var radius = radi[xValue][yValue];
        renderData.push({
          x: xValue,
          y: yValue,
          value: value,
          radius: radius
        });
      }
    }

    return {
      min: min,
      max: max,
      data: renderData
    };
  };


  function Canvas2dRenderer(config) {
    var container = config.container;
    var shadowCanvas = this.shadowCanvas = document.createElement('canvas');
    var canvas = this.canvas = config.canvas || document.createElement('canvas');
    var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0];

    var computed = getComputedStyle(config.container) || {};

    canvas.className = 'heatmap-canvas';

    this._width = canvas.width = shadowCanvas.width = config.width || +(computed.width.replace(/px/,''));
    this._height = canvas.height = shadowCanvas.height = config.height || +(computed.height.replace(/px/,''));

    this.shadowCtx = shadowCanvas.getContext('2d');
    this.ctx = canvas.getContext('2d');

    // @TODO:
    // conditional wrapper

    canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';

    container.style.position = 'relative';
    container.appendChild(canvas);

    this._palette = _getColorPalette(config);
    this._templates = {};

    this._setStyles(config);
  };

  Canvas2dRenderer.prototype = {
    renderPartial: function(data) {
      if (data.data.length > 0) {
        this._drawAlpha(data);
        this._colorize();
      }
    },
    renderAll: function(data) {
      // reset render boundaries
      this._clear();
      if (data.data.length > 0) {
        this._drawAlpha(_prepareData(data));
        this._colorize();
      }
    },
    _updateGradient: function(config) {
      this._palette = _getColorPalette(config);
    },
    updateConfig: function(config) {
      if (config['gradient']) {
        this._updateGradient(config);
      }
      this._setStyles(config);
    },
    setDimensions: function(width, height) {
      this._width = width;
      this._height = height;
      this.canvas.width = this.shadowCanvas.width = width;
      this.canvas.height = this.shadowCanvas.height = height;
    },
    _clear: function() {
      this.shadowCtx.clearRect(0, 0, this._width, this._height);
      this.ctx.clearRect(0, 0, this._width, this._height);
    },
    _setStyles: function(config) {
      this._blur = (config.blur == 0)?0:(config.blur || config.defaultBlur);

      if (config.backgroundColor) {
        this.canvas.style.backgroundColor = config.backgroundColor;
      }

      this._width = this.canvas.width = this.shadowCanvas.width = config.width || this._width;
      this._height = this.canvas.height = this.shadowCanvas.height = config.height || this._height;


      this._opacity = (config.opacity || 0) * 255;
      this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255;
      this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255;
      this._useGradientOpacity = !!config.useGradientOpacity;
    },
    _drawAlpha: function(data) {
      var min = this._min = data.min;
      var max = this._max = data.max;
      var data = data.data || [];
      var dataLen = data.length;
      // on a point basis?
      var blur = 1 - this._blur;

      while(dataLen--) {

        var point = data[dataLen];

        var x = point.x;
        var y = point.y;
        var radius = point.radius;
        // if value is bigger than max
        // use max as value
        var value = Math.min(point.value, max);
        var rectX = x - radius;
        var rectY = y - radius;
        var shadowCtx = this.shadowCtx;




        var tpl;
        if (!this._templates[radius]) {
          this._templates[radius] = tpl = _getPointTemplate(radius, blur);
        } else {
          tpl = this._templates[radius];
        }
        // value from minimum / value range
        // => [0, 1]
        var templateAlpha = (value-min)/(max-min);
        // this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
        shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;

        shadowCtx.drawImage(tpl, rectX, rectY);

        // update renderBoundaries
        if (rectX < this._renderBoundaries[0]) {
            this._renderBoundaries[0] = rectX;
          }
          if (rectY < this._renderBoundaries[1]) {
            this._renderBoundaries[1] = rectY;
          }
          if (rectX + 2*radius > this._renderBoundaries[2]) {
            this._renderBoundaries[2] = rectX + 2*radius;
          }
          if (rectY + 2*radius > this._renderBoundaries[3]) {
            this._renderBoundaries[3] = rectY + 2*radius;
          }

      }
    },
    _colorize: function() {
      var x = this._renderBoundaries[0];
      var y = this._renderBoundaries[1];
      var width = this._renderBoundaries[2] - x;
      var height = this._renderBoundaries[3] - y;
      var maxWidth = this._width;
      var maxHeight = this._height;
      var opacity = this._opacity;
      var maxOpacity = this._maxOpacity;
      var minOpacity = this._minOpacity;
      var useGradientOpacity = this._useGradientOpacity;

      if (x < 0) {
        x = 0;
      }
      if (y < 0) {
        y = 0;
      }
      if (x + width > maxWidth) {
        width = maxWidth - x;
      }
      if (y + height > maxHeight) {
        height = maxHeight - y;
      }

      var img = this.shadowCtx.getImageData(x, y, width, height);
      var imgData = img.data;
      var len = imgData.length;
      var palette = this._palette;


      for (var i = 3; i < len; i+= 4) {
        var alpha = imgData[i];
        var offset = alpha * 4;


        if (!offset) {
          continue;
        }

        var finalAlpha;
        if (opacity > 0) {
          finalAlpha = opacity;
        } else {
          if (alpha < maxOpacity) {
            if (alpha < minOpacity) {
              finalAlpha = minOpacity;
            } else {
              finalAlpha = alpha;
            }
          } else {
            finalAlpha = maxOpacity;
          }
        }

        imgData[i-3] = palette[offset];
        imgData[i-2] = palette[offset + 1];
        imgData[i-1] = palette[offset + 2];
        imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;

      }

      img.data = imgData;
      this.ctx.putImageData(img, x, y);

      this._renderBoundaries = [1000, 1000, 0, 0];

    },
    getValueAt: function(point) {
      var value;
      var shadowCtx = this.shadowCtx;
      var img = shadowCtx.getImageData(point.x, point.y, 1, 1);
      var data = img.data[3];
      var max = this._max;
      var min = this._min;

      value = (Math.abs(max-min) * (data/255)) >> 0;

      return value;
    },
    getDataURL: function() {
      return this.canvas.toDataURL();
    }
  };


  return Canvas2dRenderer;
})();


var Renderer = (function RendererClosure() {

  var rendererFn = false;

  if (HeatmapConfig['defaultRenderer'] === 'canvas2d') {
    rendererFn = Canvas2dRenderer;
  }

  return rendererFn;
})();


var Util = {
  merge: function() {
    var merged = {};
    var argsLen = arguments.length;
    for (var i = 0; i < argsLen; i++) {
      var obj = arguments[i]
      for (var key in obj) {
        merged[key] = obj[key];
      }
    }
    return merged;
  }
};
// Heatmap Constructor
var Heatmap = (function HeatmapClosure() {

  var Coordinator = (function CoordinatorClosure() {

    function Coordinator() {
      this.cStore = {};
    };

    Coordinator.prototype = {
      on: function(evtName, callback, scope) {
        var cStore = this.cStore;

        if (!cStore[evtName]) {
          cStore[evtName] = [];
        }
        cStore[evtName].push((function(data) {
            return callback.call(scope, data);
        }));
      },
      emit: function(evtName, data) {
        var cStore = this.cStore;
        if (cStore[evtName]) {
          var len = cStore[evtName].length;
          for (var i=0; i<len; i++) {
            var callback = cStore[evtName][i];
            callback(data);
          }
        }
      }
    };

    return Coordinator;
  })();


  var _connect = function(scope) {
    var renderer = scope._renderer;
    var coordinator = scope._coordinator;
    var store = scope._store;

    coordinator.on('renderpartial', renderer.renderPartial, renderer);
    coordinator.on('renderall', renderer.renderAll, renderer);
    coordinator.on('extremachange', function(data) {
      scope._config.onExtremaChange &&
      scope._config.onExtremaChange({
        min: data.min,
        max: data.max,
        gradient: scope._config['gradient'] || scope._config['defaultGradient']
      });
    });
    store.setCoordinator(coordinator);
  };


  function Heatmap() {
    var config = this._config = Util.merge(HeatmapConfig, arguments[0] || {});
    this._coordinator = new Coordinator();
    if (config['plugin']) {
      var pluginToLoad = config['plugin'];
      if (!HeatmapConfig.plugins[pluginToLoad]) {
        throw new Error('Plugin \''+ pluginToLoad + '\' not found. Maybe it was not registered.');
      } else {
        var plugin = HeatmapConfig.plugins[pluginToLoad];
        // set plugin renderer and store
        this._renderer = new plugin.renderer(config);
        this._store = new plugin.store(config);
      }
    } else {
      this._renderer = new Renderer(config);
      this._store = new Store(config);
    }
    _connect(this);
  };

  // @TODO:
  // add API documentation
  Heatmap.prototype = {
    addData: function() {
      this._store.addData.apply(this._store, arguments);
      return this;
    },
    removeData: function() {
      this._store.removeData && this._store.removeData.apply(this._store, arguments);
      return this;
    },
    setData: function() {
      this._store.setData.apply(this._store, arguments);
      return this;
    },
    setDataMax: function() {
      this._store.setDataMax.apply(this._store, arguments);
      return this;
    },
    setDataMin: function() {
      this._store.setDataMin.apply(this._store, arguments);
      return this;
    },
    configure: function(config) {
      this._config = Util.merge(this._config, config);
      this._renderer.updateConfig(this._config);
      this._coordinator.emit('renderall', this._store._getInternalData());
      return this;
    },
    repaint: function() {
      this._coordinator.emit('renderall', this._store._getInternalData());
      return this;
    },
    getData: function() {
      return this._store.getData();
    },
    getDataURL: function() {
      return this._renderer.getDataURL();
    },
    getValueAt: function(point) {

      if (this._store.getValueAt) {
        return this._store.getValueAt(point);
      } else  if (this._renderer.getValueAt) {
        return this._renderer.getValueAt(point);
      } else {
        return null;
      }
    }
  };

  return Heatmap;

})();


// core
var heatmapFactory = {
  create: function(config) {
    return new Heatmap(config);
  },
  register: function(pluginKey, plugin) {
    HeatmapConfig.plugins[pluginKey] = plugin;
  }
};

return heatmapFactory;


});

Documentation

Functions:

- h337
  - create(configObject) - (most commonly used)
  - register(pluginKey, plugin) - (used for customized implementations)

- heatmapInstance
  - addData(object|array) - (most commonly used)
  - setData(object) - (most commonly used)
  - setDataMax(number) - (rarely used functionality)
  - setDataMin(number) - (rarely used functionality)
  - configure(configObject) - (used for customized implementations)
  - getValueAt(object) - (used for customized implementations)
  - getData() - (most commonly used)
  - getDataURL() - (used for customized implementations)
  - repaint() - (rarely used functionality)

h337

"h337" is the name of the global object registered by heatmap.js. You can use it to create heatmap instances
h337.create(configObject)

Returns a heatmapInstance.

Use h337.create to create heatmap instances. A Heatmap can be customized with the configObject.
The configObject parameter is required.

Possible configuration properties:
container (DOMNode) *required*
A DOM node where the heatmap canvas should be appended (heatmap will adapt to the node's size)
backgroundColor (string) *optional*
A background color string in form of hexcode, color name, or rgb(a)
gradient (object) *optional*
An object that represents the gradient (syntax: number string [0,1] : color string), check out the example
radius (number) *optional*
The radius each datapoint will have (if not specified on the datapoint itself)
opacity (number) [0,1] *optional* default = .6
A global opacity for the whole heatmap. This overrides maxOpacity and minOpacity if set!
maxOpacity (number) [0,1] *optional*
The maximal opacity the highest value in the heatmap will have. (will be overridden if opacity set)
minOpacity(number) [0,1] *optional*
The minimum opacity the lowest value in the heatmap will have (will be overridden if opacity set)
onExtremaChange function callback
Pass a callback to receive extrema change updates. Useful for DOM legends.
blur (number) [0,1] *optional* default = 0.85
The blur factor that will be applied to all datapoints. The higher the blur factor is, the smoother the gradients will be
xField (string) *optional* default = "x"
The property name of your x coordinate in a datapoint
yField (string) *optional* default = "y"
The property name of your y coordinate in a datapoint
valueField (string) *optional* default = "value"
The property name of your y coordinate in a datapoint
Example configurations

Simple configuration with standard gradient
// create configuration object
var config = {
  container: document.getElementById('heatmapContainer'),
  radius: 10,
  maxOpacity: .5,
  minOpacity: 0,
  blur: .75
};
// create heatmap with configuration
var heatmapInstance = h337.create(config);
Custom gradient configuration
// create configuration object
var config = {
  container: document.getElementById('heatmapContainer'),
  radius: 10,
  maxOpacity: .5,
  minOpacity: 0,
  blur: .75,
  gradient: {
    // enter n keys between 0 and 1 here
    // for gradient color customization
    '.5': 'blue',
    '.8': 'red',
    '.95': 'white'
  }
};
var heatmapInstance = h337.create(config);
heatmapInstance

Heatmap instances are returned by h337.create. A heatmap instance has its own internal datastore and renderer where you can manipulate data. As a result the heatmap gets updated (either partially or completely, depending on whether it's necessary).
heatmapInstance.addData(object|array)

Returns heatmapInstance

Use this functionality only for adding datapoints on the fly, not for data initialization! heatmapInstance.addData adds a single or multiple datapoints to the heatmaps' datastore.
// a single datapoint
var dataPoint = {
  x: 5, // x coordinate of the datapoint, a number
  y: 5, // y coordinate of the datapoint, a number
  value: 100 // the value at datapoint(x, y)
};
heatmapInstance.addData(dataPoint);

// multiple datapoints (for data initialization use setData!!)
var dataPoints = [dataPoint, dataPoint, dataPoint, dataPoint];
heatmapInstance.addData(dataPoints);
heatmapInstance.setData(object)

Returns heatmapInstance

Initializes a heatmap instance with a dataset. "min", "max", and "data" properties are required.
setData removes all previously existing points from the heatmap instance and re-initializes the datastore.
var data = {
  max: 100,
  min: 0,
  data: [
    dataPoint, dataPoint, dataPoint, dataPoint
  ]
};
heatmapInstance.setData(data);
heatmapInstance.setDataMax(number)

Returns heatmapInstance

Changes the upper bound of your dataset and triggers a complete rerendering.
heatmapInstance.setDataMax(200);
// setting the maximum value triggers a complete rerendering of the heatmap
heatmapInstance.setDataMax(100);
heatmapInstance.setDataMin(number)

Returns heatmapInstance

Changes the lower bound of your dataset and triggers a complete rerendering.
heatmapInstance.setDataMin(10);
// setting the minimum value triggers a complete rerendering of the heatmap
heatmapInstance.setDataMin(0);
heatmapInstance.configure(configObject)

Returns heatmapInstance

Reconfigures a heatmap instance after it has been initialized. Triggers a complete rerendering.
var nuConfig = {
  radius: 10,
  maxOpacity: .5,
  minOpacity: 0,
  blur: .75
};
heatmapInstance.configure(nuConfig);
heatmapInstance.getValueAt(object)

Returns value at datapoint position

Note: The returned value is an interpolated value based on the gradient blending if point is not in store
heatmapInstance.addData({ x: 10, y: 10, value: 100});
// get the value at x=10, y=10
heatmapInstance.getValueAt({ x: 10, y: 10 }); // returns 100
heatmapInstance.getData()

Returns a persistable and reimportable (with setData) JSON object

var currentData = heatmapInstance.getData();
// now let's create a new instance and set the data
var heatmap2 = h337.create(config);
heatmap2.setData(currentData); // now both heatmap instances have the same content
heatmapInstance.getDataURL()

Returns dataURL string

The returned value is the base64 encoded dataURL of the heatmap instance.
heatmapInstance.getDataURL(); // data:image/png;base64...
// ready for saving locally or on the server
heatmapInstance.repaint()

Returns heatmapInstance

Repaints the whole heatmap canvas

A Quick Example

var heatmap = h337.create({
  container: domElement
});

heatmap.setData({
  max: 5,
  data: [{ x: 10, y: 15, value: 5}, ...]
});

Minimal Configuration Example

This example demonstrates the minimal required configuration properties you need to configure a heatmap instance. Make sure the container you pass has a width and a height because heatmap.js will use the container's dimensions.

Code

// minimal heatmap instance configuration
var heatmapInstance = h337.create({
  // only container is required, the rest will be defaults
  container: document.querySelector('.heatmap')
});

// now generate some random data
var points = [];
var max = 0;
var width = 840;
var height = 400;
var len = 200;

while (len--) {
  var val = Math.floor(Math.random()*100);
  max = Math.max(max, val);
  var point = {
    x: Math.floor(Math.random()*width),
    y: Math.floor(Math.random()*height),
    value: val
  };
  points.push(point);
}
// heatmap data format
var data = {
  max: max,
  data: points
};
// if you have a set of datapoints always use setData instead of addData
// for data initialization
heatmapInstance.setData(data);

Point Configuration Example

This example demonstrates configuration on a point basis with minimal configuration. The difference to the minimal configuration example is that there is now a radius property set on every datapoint. This gives you the freedom of having datapoints with different radi of influence, yay.

Code

// minimal heatmap instance configuration
var heatmapInstance = h337.create({
  // only container is required, the rest will be defaults
  container: document.querySelector('.heatmap')
});

// now generate some random data
var points = [];
var max = 0;
var width = 840;
var height = 400;
var len = 300;

while (len--) {
  var val = Math.floor(Math.random()*100);
  // now also with custom radius
  var radius = Math.floor(Math.random()*70);

  max = Math.max(max, val);
  var point = {
    x: Math.floor(Math.random()*width),
    y: Math.floor(Math.random()*height),
    value: val,
    // radius configuration on point basis
    radius: radius
  };
  points.push(point);
}
// heatmap data format
var data = {
  max: max,
  data: points
};
// if you have a set of datapoints always use setData instead of addData
// for data initialization
heatmapInstance.setData(data);

Heatmap Customization Example

This example demonstrates a completely customized heatmap. Any minOpacity > 0 will result in visible point impact boundaries (no gradient fading into transparency!)

Code

// customized heatmap configuration
var heatmapInstance = h337.create({
  // required container
  container: document.querySelector('.heatmap'),
  // backgroundColor to cover transparent areas
  backgroundColor: 'rgba(0,0,0,.95)',
  // custom gradient colors
  gradient: {
    // enter n keys between 0 and 1 here
    // for gradient color customization
    '.5': 'blue',
    '.8': 'red',
    '.95': 'white'
  },
  // the maximum opacity (the value with the highest intensity will have it)
  maxOpacity: .9,
  // minimum opacity. any value > 0 will produce
  // no transparent gradient transition
  minOpacity: .3
});

// now generate some random data
var points = [];
var max = 0;
var width = 840;
var height = 400;
var len = 300;

while (len--) {
  var val = Math.floor(Math.random()*100);
  var radius = Math.floor(Math.random()*70);

  max = Math.max(max, val);
  var point = {
    x: Math.floor(Math.random()*width),
    y: Math.floor(Math.random()*height),
    value: val,
    radius: radius
  };
  points.push(point);
}
// heatmap data format
var data = {
  max: max,
  data: points
};
// if you have a set of datapoints always use setData instead of addData
// for data initialization
heatmapInstance.setData(data);

Simpleheat.js

A super-tiny JavaScript library for drawing heatmaps with Canvas. Inspired by heatmap.js, but with focus on simplicity and performance.

github.com/mourner/simpleheat

Source code

'use strict';

if (typeof module !== 'undefined') module.exports = simpleheat;

function simpleheat(canvas) {
    if (!(this instanceof simpleheat)) return new simpleheat(canvas);

    this._canvas = canvas = typeof canvas === 'string' ? document.getElementById(canvas) : canvas;

    this._ctx = canvas.getContext('2d');
    this._width = canvas.width;
    this._height = canvas.height;

    this._max = 1;
    this._data = [];
}

simpleheat.prototype = {

    defaultRadius: 25,

    defaultGradient: {
        0.4: 'blue',
        0.6: 'cyan',
        0.7: 'lime',
        0.8: 'yellow',
        1.0: 'red'
    },

    data: function (data) {
        this._data = data;
        return this;
    },

    max: function (max) {
        this._max = max;
        return this;
    },

    add: function (point) {
        this._data.push(point);
        return this;
    },

    clear: function () {
        this._data = [];
        return this;
    },

    radius: function (r, blur) {
        blur = blur === undefined ? 15 : blur;

        // create a grayscale blurred circle image that we'll use for drawing points
        var circle = this._circle = this._createCanvas(),
            ctx = circle.getContext('2d'),
            r2 = this._r = r + blur;

        circle.width = circle.height = r2 * 2;

        ctx.shadowOffsetX = ctx.shadowOffsetY = r2 * 2;
        ctx.shadowBlur = blur;
        ctx.shadowColor = 'black';

        ctx.beginPath();
        ctx.arc(-r2, -r2, r, 0, Math.PI * 2, true);
        ctx.closePath();
        ctx.fill();

        return this;
    },

    resize: function () {
        this._width = this._canvas.width;
        this._height = this._canvas.height;
    },

    gradient: function (grad) {
        // create a 256x1 gradient that we'll use to turn a grayscale heatmap into a colored one
        var canvas = this._createCanvas(),
            ctx = canvas.getContext('2d'),
            gradient = ctx.createLinearGradient(0, 0, 0, 256);

        canvas.width = 1;
        canvas.height = 256;

        for (var i in grad) {
            gradient.addColorStop(+i, grad[i]);
        }

        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, 1, 256);

        this._grad = ctx.getImageData(0, 0, 1, 256).data;

        return this;
    },

    draw: function (minOpacity) {
        if (!this._circle) this.radius(this.defaultRadius);
        if (!this._grad) this.gradient(this.defaultGradient);

        var ctx = this._ctx;

        ctx.clearRect(0, 0, this._width, this._height);

        // draw a grayscale heatmap by putting a blurred circle at each data point
        for (var i = 0, len = this._data.length, p; i < len; i++) {
            p = this._data[i];
            ctx.globalAlpha = Math.max(p[2] / this._max, minOpacity === undefined ? 0.05 : minOpacity);
            ctx.drawImage(this._circle, p[0] - this._r, p[1] - this._r);
        }

        // colorize the heatmap, using opacity value of each pixel to get the right color from our gradient
        var colored = ctx.getImageData(0, 0, this._width, this._height);
        this._colorize(colored.data, this._grad);
        ctx.putImageData(colored, 0, 0);

        return this;
    },

    _colorize: function (pixels, gradient) {
        for (var i = 0, len = pixels.length, j; i < len; i += 4) {
            j = pixels[i + 3] * 4; // get gradient color from opacity value

            if (j) {
                pixels[i] = gradient[j];
                pixels[i + 1] = gradient[j + 1];
                pixels[i + 2] = gradient[j + 2];
            }
        }
    },

    _createCanvas:function() {
        if (typeof document !== 'undefined') {
            return document.createElement('canvas');
        } else {
            // create a new canvas instance in node.js
            // the canvas class needs to have a default constructor without any parameter
            return new this._canvas.constructor();
        }
    }
};

Documentation

simpleheat('canvas').data(data).draw();

Constructor

// create a simpleheat object given an id or canvas reference
var heat = simpleheat(canvas);
Data

// set data of [[x, y, value], ...] format
heat.data(data);

// set max data value (1 by default)
heat.max(max);

// add a data point
heat.add(point);

// clear data
heat.clear();
Appearance

// set point radius and blur radius (25 and 15 by default)
heat.radius(r, r2);

// set gradient colors as {<stop>: '<color>'}, e.g. {0.4: 'blue', 0.65: 'lime', 1: 'red'}
heat.gradient(grad);

// call in case Canvas size changed
heat.resize();
Rendering

// draw the heatmap with optional minimum point opacity (0.05 by default)
heat.draw(minOpacity);

Demo data

<!DOCTYPE html>
<html>
<head>
    <title>simpleheat demo</title>
    <style>
        body { text-align: center; font: 16px/1.4 "Helvetica Neue", Arial, sans-serif; }
        a { color: #0077ff; }
        .container { width: 1000px; height: 600px; margin: 0 auto; position: relative; border: 1px solid #ccc; }
        .options { position: absolute; top: 0; right: 0; padding: 10px; background: rgba(255,255,255,0.6);
            border-bottom: 1px solid #ccc; border-left: 1px solid #ccc; line-height: 1; }
        .options input { width: 200px; }
        .options label { width: 60px; float: left; text-align: right; margin-right: 10px; color: #555; }
        .ghbtns { position: relative; top: 4px; margin-left: 5px; }
    </style>
</head>
<body>
<p>
    <strong>simpleheat</strong> is a tiny and fast JS heatmap library.
    More on <a href="https://github.com/mourner/simpleheat">mourner / simpleheat</a>
    <iframe class="ghbtns" src="http://ghbtns.com/github-btn.html?user=mourner&amp;repo=simpleheat&amp;type=watch&amp;count=true"
  allowtransparency="true" frameborder="0" scrolling="0" width="90" height="20"></iframe>
</p>
<div class="container">
    <div class="options">
        <label>Radius </label><input type="range" id="radius" value="25" min="10" max="50" /><br />
        <label>Blur </label><input type="range" id="blur" value="15" min="10" max="50" />
    </div>
    <canvas id="canvas" width="1000" height="600"></canvas>
</div>

<script src="../simpleheat.js"></script>
<script src="data.js"></script>
<script>

window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
                               window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

function get(id) {
    return document.getElementById(id);
}

var heat = simpleheat('canvas').data(data).max(18),
    frame;

function draw() {
    console.time('draw');
    heat.draw();
    console.timeEnd('draw');
    frame = null;
}

draw();

get('canvas').onmousemove = function (e) {
    heat.add([e.layerX, e.layerY, 1]);
    frame = frame || window.requestAnimationFrame(draw);
};

var radius = get('radius'),
    blur = get('blur'),
    changeType = 'oninput' in radius ? 'oninput' : 'onchange';

radius[changeType] = blur[changeType] = function (e) {
    heat.radius(+radius.value, +blur.value);
    frame = frame || window.requestAnimationFrame(draw);
};

</script>
</body>
</html>

var data = [[38,20,2],[38,690,3],[48,30,1],[48,40,1],[48,670,1],[58,640,1],[58,680,1],[67,630,1],[86,10,1],[86,660,1],[96,0,1],[96,80,1],[96,530,1],[96,540,2],[96,560,1],[96,620,1],[96,640,1],[105,530,1],[105,560,3],[105,590,1],[105,610,1],[115,300,1],[115,310,4],[125,260,1],[125,280,1],[125,300,1],[125,500,1],[125,530,1],[134,250,1],[134,260,1],[134,280,1],[144,40,1],[144,260,1],[144,270,4],[144,320,1],[144,330,1],[153,220,1],[163,280,1],[173,120,2],[182,80,1],[182,120,2],[192,10,1],[192,120,1],[192,130,2],[192,190,1],[192,530,1],[201,120,2],[201,130,1],[201,150,1],[201,190,1],[201,240,1],[201,280,1],[201,290,1],[201,340,1],[201,390,3],[201,400,2],[201,420,1],[201,670,1],[201,710,1],[201,750,1],[211,160,2],[211,280,1],[211,320,1],[211,340,1],[211,800,2],[211,810,2],[221,80,1],[221,140,2],[221,170,1],[221,180,1],[221,230,1],[221,420,1],[221,490,2],[221,730,1],[230,150,1],[230,550,4],[230,670,1],[230,790,2],[240,100,1],[240,120,1],[240,150,1],[240,160,1],[240,220,1],[240,240,1],[240,300,1],[240,330,1],[240,460,1],[240,480,2],[240,550,1],[240,570,1],[240,840,2],[249,70,1],[249,120,1],[249,200,1],[249,210,1],[249,290,3],[249,340,1],[249,860,2],[249,870,2],[259,0,1],[259,90,1],[259,160,1],[259,180,1],[259,190,1],[259,270,1],[259,280,1],[259,290,2],[259,320,1],[259,360,1],[259,430,1],[259,480,1],[259,490,1],[259,860,1],[269,60,2],[269,150,1],[269,220,1],[269,260,1],[269,280,1],[269,290,1],[269,300,1],[269,320,1],[269,350,1],[269,450,3],[269,470,2],[269,480,3],[269,490,1],[278,120,1],[278,140,1],[278,150,2],[278,190,1],[278,220,1],[278,260,1],[278,290,2],[278,500,2],[278,680,2],[278,740,2],[288,0,1],[288,50,1],[288,150,2],[288,230,1],[288,260,1],[288,280,1],[288,290,2],[288,320,1],[288,330,1],[288,340,1],[288,460,1],[288,630,2],[288,720,2],[288,730,2],[288,750,2],[288,790,2],[288,840,1],[297,20,1],[297,120,2],[297,140,2],[297,150,1],[297,180,1],[297,250,4],[297,290,8],[297,300,4],[297,310,1],[297,340,2],[297,350,2],[297,360,1],[297,380,2],[297,410,1],[297,430,2],[297,440,5],[297,450,1],[297,460,8],[297,470,2],[297,480,4],[297,490,2],[297,500,3],[297,520,2],[297,530,1],[297,540,1],[297,550,1],[297,610,1],[297,620,2],[297,630,4],[297,640,1],[297,650,2],[297,660,3],[297,670,11],[297,690,1],[297,700,1],[297,710,2],[297,730,2],[297,770,3],[297,780,2],[297,790,2],[297,830,2],[307,0,1],[307,10,1],[307,70,1],[307,100,1],[307,120,3],[307,140,2],[307,150,2],[307,170,2],[307,180,1],[307,230,1],[307,250,1],[307,270,1],[307,290,1],[307,300,1],[307,320,1],[307,350,1],[307,680,2],[307,690,2],[307,700,2],[307,710,1],[307,730,1],[307,840,1],[307,850,2],[316,0,1],[316,140,1],[316,150,1],[316,270,1],[316,410,1],[316,420,1],[316,430,4],[316,440,1],[316,460,1],[316,490,1],[316,510,1],[316,530,2],[316,550,1],[316,690,1],[316,700,2],[316,730,1],[316,850,1],[316,880,1],[326,20,1],[326,90,1],[326,110,1],[326,130,1],[326,170,2],[326,190,1],[326,230,1],[326,260,1],[326,280,1],[326,290,1],[326,300,2],[326,310,1],[326,320,1],[326,330,1],[326,410,1],[326,460,1],[326,480,1],[326,530,1],[326,580,1],[326,680,1],[326,690,3],[326,750,2],[326,840,1],[326,870,1],[326,1010,2],[336,140,1],[336,170,1],[336,180,1],[336,190,1],[336,230,1],[336,240,1],[336,290,2],[336,310,1],[336,480,1],[336,510,1],[336,690,1],[336,730,1],[336,750,3],[336,810,1],[336,870,3],[336,880,1],[336,960,1],[336,990,1],[336,1000,1],[345,0,1],[345,150,3],[345,160,1],[345,190,2],[345,240,1],[345,260,1],[345,290,4],[345,400,1],[345,420,1],[345,440,1],[345,460,1],[345,500,1],[345,510,1],[345,530,1],[345,630,1],[345,650,1],[345,690,1],[345,710,1],[345,750,2],[345,820,1],[345,850,2],[345,900,1],[345,960,1],[355,20,1],[355,140,1],[355,150,1],[355,160,1],[355,180,2],[355,220,1],[355,250,1],[355,280,1],[355,290,3],[355,300,1],[355,310,2],[355,320,2],[355,330,4],[355,460,1],[355,470,1],[355,510,1],[355,680,1],[355,750,1],[355,800,2],[355,810,1],[355,850,1],[364,150,1],[364,160,1],[364,170,1],[364,200,1],[364,230,1],[364,250,1],[364,290,1],[364,310,1],[364,430,1],[364,520,1],[364,700,1],[364,720,1],[364,760,1],[364,780,4],[364,900,1],[364,980,1],[374,90,1],[374,140,1],[374,150,2],[374,180,2],[374,190,2],[374,250,1],[374,260,2],[374,340,1],[374,450,1],[374,480,1],[374,490,1],[374,690,1],[374,870,1],[384,30,3],[384,40,1],[384,50,1],[384,80,1],[384,120,1],[384,140,1],[384,150,1],[384,180,1],[384,210,1],[384,250,4],[384,270,1],[384,300,1],[384,310,1],[384,350,1],[384,390,1],[384,400,2],[384,550,1],[384,560,1],[384,730,1],[384,780,1],[393,50,1],[393,70,1],[393,100,1],[393,140,1],[393,150,2],[393,160,1],[393,180,2],[393,210,1],[393,290,1],[393,310,1],[393,400,2],[393,450,1],[393,480,1],[393,510,1],[393,520,1],[393,600,1],[393,610,1],[393,620,1],[393,630,1],[393,640,1],[393,660,1],[393,680,1],[393,710,1],[393,720,1],[393,850,1],[403,160,1],[403,230,2],[403,250,1],[403,280,1],[403,390,1],[403,400,2],[403,450,1],[403,470,1],[403,500,2],[403,570,1],[403,600,1],[403,610,1],[403,640,4],[403,690,3],[403,720,1],[403,750,1],[412,150,1],[412,160,1],[412,210,1],[412,220,1],[412,250,1],[412,270,1],[412,280,2],[412,330,1],[412,380,2],[412,400,4],[412,450,1],[412,470,1],[412,480,1],[412,490,1],[412,520,1],[412,530,1],[412,560,1],[412,620,2],[412,650,1],[412,680,1],[412,700,1],[412,750,1],[412,840,1],[412,870,1],[422,30,1],[422,40,1],[422,60,1],[422,160,1],[422,170,2],[422,180,1],[422,200,1],[422,220,1],[422,400,1],[422,420,1],[422,450,1],[422,460,1],[422,480,1],[422,490,2],[422,510,1],[422,560,1],[422,600,1],[422,610,1],[422,620,1],[422,630,1],[422,640,1],[422,700,1],[422,710,2],[422,780,1],[432,110,1],[432,150,1],[432,170,1],[432,180,1],[432,240,1],[432,250,1],[432,260,1],[432,310,1],[432,330,1],[432,380,1],[432,430,1],[432,460,1],[432,480,1],[432,510,1],[432,520,1],[432,530,1],[432,620,3],[432,630,5],[432,660,1],[432,670,2],[432,680,1],[432,690,1],[432,730,3],[432,740,1],[432,750,2],[441,50,1],[441,120,1],[441,140,1],[441,150,1],[441,190,2],[441,220,1],[441,290,1],[441,330,1],[441,400,1],[441,410,1],[441,450,1],[441,480,2],[441,500,2],[441,510,1],[441,540,1],[441,570,1],[441,600,2],[441,610,3],[441,620,1],[441,680,3],[441,690,2],[441,730,1],[441,850,1],[441,870,1],[451,140,1],[451,150,3],[451,160,1],[451,220,1],[451,400,1],[451,410,1],[451,450,1],[451,460,1],[451,480,2],[451,560,1],[451,570,1],[451,590,1],[451,600,1],[451,610,4],[451,620,2],[451,870,1],[451,950,2],[460,140,1],[460,150,1],[460,160,1],[460,230,1],[460,300,1],[460,310,3],[460,320,3],[460,330,5],[460,340,8],[460,350,2],[460,400,1],[460,410,1],[460,440,2],[460,450,1],[460,470,1],[460,520,1],[460,630,1],[460,670,1],[460,720,1],[460,850,1],[470,10,1],[470,130,1],[470,140,2],[470,150,1],[470,180,1],[470,220,1],[470,260,1],[470,300,1],[470,330,1],[470,340,1],[470,360,3],[470,400,1],[470,450,1],[470,520,1],[470,600,1],[470,670,1],[470,700,1],[470,850,3],[480,200,1],[480,220,1],[480,290,2],[480,320,2],[480,330,7],[480,410,1],[480,450,2],[480,510,1],[480,540,1],[480,620,2],[480,650,1],[480,670,2],[480,700,1],[480,730,1],[489,20,1],[489,130,1],[489,140,1],[489,150,2],[489,160,1],[489,170,1],[489,190,1],[489,220,2],[489,230,1],[489,240,2],[489,250,2],[489,260,1],[489,270,3],[489,280,1],[489,290,1],[489,430,1],[489,450,1],[489,510,1],[489,640,2],[489,770,1],[489,780,1],[489,840,2],[489,880,1],[499,100,1],[499,110,1],[499,120,1],[499,130,1],[499,140,2],[499,160,2],[499,170,1],[499,180,1],[499,210,1],[499,220,1],[499,240,1],[499,310,2],[499,380,1],[499,410,1],[499,450,1],[499,530,1],[499,540,1],[499,650,2],[499,720,1],[499,790,6],[499,810,1],[508,20,1],[508,120,1],[508,130,2],[508,140,2],[508,150,1],[508,190,1],[508,280,2],[508,360,1],[508,410,1],[508,450,2],[508,490,1],[508,510,2],[508,760,2],[508,860,1],[518,20,1],[518,60,1],[518,100,1],[518,120,1],[518,140,1],[518,150,1],[518,160,1],[518,180,1],[518,200,1],[518,210,1],[518,400,1],[518,410,1],[518,500,1],[527,120,1],[527,140,1],[527,150,1],[527,220,18],[527,230,3],[527,240,1],[527,270,1],[527,300,3],[527,380,1],[527,450,1],[527,470,1],[527,480,1],[527,490,2],[527,500,1],[527,510,2],[527,570,1],[527,580,1],[527,650,7],[527,830,1],[537,140,1],[537,150,1],[537,160,1],[537,170,1],[537,270,1],[537,410,1],[537,450,1],[537,470,2],[537,490,1],[537,630,1],[537,670,2],[537,760,1],[537,880,2],[547,80,2],[547,160,1],[547,180,4],[547,260,2],[547,270,1],[547,280,1],[547,380,1],[547,390,1],[547,410,1],[547,420,1],[547,520,1],[547,630,1],[547,750,3],[547,770,2],[547,860,4],[556,50,1],[556,70,1],[556,130,2],[556,140,1],[556,160,1],[556,190,1],[556,230,5],[556,290,1],[556,300,1],[556,330,1],[556,390,11],[556,450,2],[556,460,13],[556,500,13],[556,520,6],[556,530,14],[556,600,3],[556,660,4],[566,140,1],[566,170,1],[566,180,1],[566,230,1],[566,260,1],[566,320,3],[566,360,1],[566,490,1],[566,830,3],[575,20,1],[575,140,2],[575,150,2],[575,160,1],[575,180,1],[575,260,11],[575,330,1],[575,410,2],[575,450,1],[585,90,1],[585,100,1],[585,140,1],[585,160,1],[585,180,1],[585,190,1],[585,200,1],[585,250,1],[585,310,1],[585,330,1],[585,830,1],[595,30,1],[595,70,2],[595,80,1],[595,90,1],[595,140,1],[595,160,1],[595,180,1],[595,200,1],[595,250,1],[595,280,1],[595,450,1],[595,530,1],[595,540,1],[604,20,1],[604,150,2],[604,180,1],[604,200,1],[604,290,1],[604,410,1],[604,830,1],[614,0,3],[614,20,1],[614,40,1],[614,140,1],[614,160,1],[614,180,1],[614,230,1],[614,430,1],[614,510,1],[623,200,1],[623,230,2],[623,300,1],[623,330,1],[623,410,1],[623,420,1],[623,520,1],[623,530,2],[623,570,5],[633,60,1],[633,150,1],[633,170,1],[633,190,1],[633,260,2],[633,400,1],[633,570,4],[633,830,1],[643,0,1],[643,70,1],[643,150,1],[643,330,2],[643,570,10],[643,590,1],[652,0,2],[652,140,1],[652,160,1],[652,180,2],[652,400,1],[652,470,1],[652,570,4],[652,830,1],[662,170,1],[662,180,1],[662,290,2],[662,400,1],[662,570,2],[671,0,1],[671,30,1],[671,160,1],[671,170,1],[671,310,1],[671,400,1],[671,720,1],[681,0,1],[681,160,1],[681,280,1],[681,320,1],[700,80,1],[700,170,1],[700,850,1],[710,240,1],[710,450,1],[719,80,1],[719,140,1],[719,150,1],[719,710,1],[729,240,1],[729,840,1],[738,10,1],[738,40,1],[738,450,1],[748,0,1],[758,300,1],[758,660,1],[767,270,2],[796,160,1],[806,970,1],[815,280,1],[815,300,1],[815,360,1],[825,790,1],[844,470,1],[901,270,1],[921,220,1],[921,340,1],[921,720,1],[930,490,1],[930,500,1],[940,180,2],[940,430,1],[940,510,1],[940,580,1],[949,120,5],[949,150,1],[949,180,1],[949,370,1],[949,390,1],[949,570,2],[949,720,1],[949,770,2],[949,780,1],[949,860,1]];

Clickmap.js

Source code

Tracking Clicks, Building a Clickmap

Record the X and Y coordinates of the mouse cursor when it is clicked on a web page.
Save those coordinates to a database
When called, display a "clickmap" on top of the web page visually displaying the locations of those clicks.


<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Click Map Demo</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <script src="//www.google.com/jsapi" type="text/javascript"></script>
    <script type="text/javascript" src="js/jquery.js">
    <script type="text/javascript" src="js/clickmap.js"></script>
    <script type="text/javascript">
        $(function() {
            // do stuff
        });
    </script>
</head>
<body>
    <img src="images/baywatch.jpg" alt="baywatch" />
    <p class="button displayClicks"><a href="#demo">Display Click Map</a></p>
</body>
</html>

Not much content there, just a picture of the Baywatch crew and a simple button.

The JavaScript is going to do two major things for us: saving clicks and displaying clicks.

#Saving Clicks

(function($) { 

$.fn.saveClicks = function() { 
    $(this).bind('mousedown.clickmap', function(evt) { 
        $.post('/examples/ClickMap/clickmap.php', {  
            x:evt.pageX,  
            y:evt.pageY,  
            l:escape(document.location.pathname) 
        }); 
    }); 
}; 
 
$.fn.stopSaveClicks = function() { 
     $(this).unbind('mousedown.clickmap'); 
};

})(jQuery); 

We are binding the mousedown event to the element it gets called on (it will be the whole document) and then using jQuery's post function to send some data (the coordinates) to a special file (clickmap.php).

#Displaying Clicks

Again, two functions. One is in charge of creating the overlay and displaying the click graphics (the PHP sends all the data but the jQuery does the appending).
The other removes everything. We make use of the jQuery get function.

$.displayClicks = function(settings) { 
    $('<div id="clickmap-overlay"></div>').appendTo('body'); 
    $('<div id="clickmap-loading"></div>').appendTo('body'); 
    $.get('/examples/ClickMap/clickmap.php', { l:escape( document.location.pathname) },  
        function(htmlContentFromServer) { 
            $(htmlContentFromServer).appendTo('body');     
            $('#clickmap-loading').remove(); 
        } 
    ); 
}; 
 
$.removeClicks = function() { 
    $('#clickmap-overlay').remove(); 
    $('#clickmap-container').remove(); 
}; 

#Firing it all off

We'll include some JavaScript right on the page to fire everything off.

<script type="text/javascript">
    $(function() {
        $(document).saveClicks(); 
    
        $('.displayClicks').click(function() {
            $.displayClicks();
            $('#clickmap-overlay').click(function() {
                 $.removeClicks();
                 $(document).saveClicks();
            });
            $(document).stopSaveClicks();
            return false;
        });
    });
</script>
#The PHP

#The CSS

The actual clickmap stuff doesn't need a heck of a lot in terms of styling. 
Just the overlay itself, a loading area (in case it takes a while to get all the click data), and the little graphics for the clicks themselves.

#clickmap-overlay { 
    position:fixed; 
    top:0; left:0; 
    width:100%; height:100%;  
    background-color:#000; 
    filter:alpha(opacity=70); opacity: 0.7; 
} 

#clickmap-loading { 
    position:fixed; 
    top:0; left:0; 
    width:100%; height:100%;  
    background:transparent url(images/loading.gif) no-repeat center center; 
} 

#clickmap-container div { 
    position:absolute; 
    width:20px; height:20px; 
    background:transparent url(images/click.png) no-repeat center center; 
}

Trackclicks.js

<script type="text/javascript">
if(document.addEventListener) {
  document.body.addEventListener("click", _EventLogging, false);
} else {
  document.body.attachEvent("onclick", _EventLogging);
}

function _EventLogging($e) {
  $obj = ($e.target) ? $e.target : $e.srcElement;
  if(window.scrollX) {
    $x = $e.clientX + window.scrollX;
    $y = $e.clientY + window.scrollY;
  } else {
    $x = window.event.clientX + document.documentElement.scrollLeft + document.body.scrollLeft;
    $y = window.event.clientY + document.documentElement.scrollTop + document.body.scrollTop;
  }

  // More code here...
}
</script>

MouseMoveRecorder.js

var pageCoords = []; 
$(document).onmousemove = function(){
  pageCoords.push("( " + e.pageX + ", " + e.pageY + " )");//get page coordinates and storing in array
}
$(window ).unload(function() {
  //make ajax call to save coordinates array to database
  console.log(pageCoords);
});

(function() {
    var mousePos;

    document.onmousemove = handleMouseMove;
    setInterval(getMousePosition, 100); // setInterval repeats every X ms

    function handleMouseMove(event) {
        var dot, eventDoc, doc, body, pageX, pageY;

        event = event || window.event; // IE-ism

        // If pageX/Y aren't available and clientX/Y are,
        // calculate pageX/Y - logic taken from jQuery.
        // (This is to support old IE)
        if (event.pageX == null && event.clientX != null) {
            eventDoc = (event.target && event.target.ownerDocument) || document;
            doc = eventDoc.documentElement;
            body = eventDoc.body;

            event.pageX = event.clientX +
              (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
              (doc && doc.clientLeft || body && body.clientLeft || 0);
            event.pageY = event.clientY +
              (doc && doc.scrollTop  || body && body.scrollTop  || 0) -
              (doc && doc.clientTop  || body && body.clientTop  || 0 );
        }

        mousePos = {
            x: event.pageX,
            y: event.pageY
        };
    }
    function getMousePosition() {
        var pos = mousePos;
        if (!pos) {
            // We haven't seen any movement yet
        }
        else {
            // Use pos.x and pos.y
            // Add a dot to follow the cursor
            dot = document.createElement('div');
            dot.className = "dot";
            dot.style.left = mousePos.x + "px";
            dot.style.top = mousePos.y + "px";
            document.body.appendChild(dot);
        }
    }
})();

User actions tracker

usertrack.net/demo.php

Tracker.js

'use strict';
var UST = {
    DEBUG: false,
    settings: {
        isStatic: true,
        recordClick: true,
        recordMove: true,
        recordKeyboard: true,
        delay: 200,
        maxMoves: 800,
        serverPath: "//www.usertrack.net/dashboard",
        percentangeRecorded: 100,
        ignoreGET: ['utm_source', 'utm_ccc_01', 'gclid', 'utm_campaign', 'utm_medium'],
    }
};
UST.randomToken = function() {
    return Math.random().toString(36).substr(2) + Math.random().toString(36).substr(2);
};
UST.enableRecord = function() {
    localStorage.noRecord = 'false';
};
UST.disableRecord = function() {
    localStorage.noRecord = 'true';
};
UST.canRecord = function() {
    if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) {
        return false;
    }
    if (top !== self) {
        return false;
    }
    if (localStorage.noRecord === 'true') {
        return false;
    }
    if (localStorage.getItem('token') === null) {
        if (Math.random() * 100 >= UST.settings.percentangeRecorded) {
            UST.disableRecord();
            return false;
        }
    }
    return true;
};
UST.testRequirements = function() {
    if (typeof jQuery === 'undefined')
        return "Did you include jQuery before tracker.js?";
    var versions = jQuery.fn.jquery.split('.');
    var oldEnough = versions[0] > 1 || (versions[1] >= 8 && versions[2] >= 1);
    if (!oldEnough)
        console.log("Your jQuery version seems to be old. userTrack requires at least jQuery 1.8.1");
    return 'ok';
};
UST.getContentDiv = function() {
    var mostProbable = jQuery('body');
    var maxP = 0;
    var documentWidth = jQuery(document).width();
    var documentHeight = jQuery(document).height();
    jQuery('div').each(function() {
        var probability = 0;
        var t = jQuery(this);
        if (t.css('position') == 'static' || t.css('position') == 'relative')
            probability += 2;
        if (t.height() > documentHeight / 2)
            probability += 3;
        if (t.parent().is('body'))
            probability++;
        if (t.css('marginLeft') == t.css('marginRight'))
            probability++;
        if (t.attr('id') == 'content')
            probability += 2;
        if (t.attr('id') == 'container')
            probability++;
        if (t.width() != documentWidth)
            probability += 2;
        if (probability > maxP) {
            maxP = probability;
            mostProbable = t;
        }
    });
    return mostProbable;
};
UST.getContextPath = function() {
    if (UST.settings.serverPath !== '') {
        return UST.settings.serverPath + '/';
    }
    return '/';
};
UST.getDomain = function() {
    if (document.domain.indexOf('www.') === 0) {
        return document.domain.substr(4);
    }
    return document.domain;
};
UST.removeURLParam = function(key, url) {
    var rtn = url.split("?")[0],
        param, params_arr = [],
        queryString = (url.indexOf("?") !== -1) ? url.split("?")[1] : "";
    if (queryString !== "") {
        params_arr = queryString.split("&");
        for (var i = params_arr.length - 1; i >= 0; i -= 1) {
            param = params_arr[i].split("=")[0];
            if (param === key) {
                params_arr.splice(i, 1);
            }
        }
        rtn = rtn + "?" + params_arr.join("&");
    }
    return rtn;
};
UST.getCleanPageURL = function() {
    var currentURL = window.location.pathname + window.location.search;
    if (UST.lastURL != currentURL) {
        UST.lastURL = currentURL;
        UST.cleanPageURL = currentURL;
        for (var key in UST.settings.ignoreGET) {
            var param = UST.settings.ignoreGET[key];
            UST.cleanPageURL = UST.removeURLParam(param, UST.cleanPageURL);
            if (UST.cleanPageURL[UST.cleanPageURL.length - 1] == '?') {
                UST.cleanPageURL = UST.cleanPageURL.slice(0, -1);
            }
        }
    }
    return UST.cleanPageURL;
};
UST.coord4 = {
    fillZeros: function(x) {
        x = x.toString();
        while (x.length < 4) {
            x = '0' + x;
        }
        return x;
    },
    get2DPoint: function(x) {
        x = x.toString();
        var p = {
            x: x.substring(0, 4),
            y: x.substring(4)
        };
        while (p.x[0] === '0') {
            p.x = p.x.substring(1);
        }
        while (p.y[0] === '0') {
            p.y = p.y.substring(1);
        }
        return p;
    }
};
UST.init = function() {
    UST.DEBUG && console.log(localStorage);
    var errorStarting = UST.testRequirements();
    if (errorStarting !== 'ok') {
        console.log('userTrack tracker could not be started.', errorStarting)
        return;
    }
    if (!UST.canRecord()) {
        return;
    }
    var getContextPath = UST.getContextPath;
    var getDomain = UST.getDomain;
    UST.sendData = function(clientPageID) {
        localStorage.setItem('lastTokenDate', new Date());
        var data = {
            movements: '',
            clicks: '',
            partial: ''
        };
        var toSend = [];
        for (var v in movements) {
            var obj = UST.coord4.get2DPoint(v);
            obj.count = movements[v];
            toSend.push(obj);
        }
        if (toSend.length > 3) {
            data.movements = JSON.stringify(toSend);
            movements = {};
        }
        toSend = [];
        for (v in clicks) {
            var obj = UST.coord4.get2DPoint(v);
            obj.count = clicks[v];
            toSend.push(obj);
        }
        if (toSend.length > 0) {
            data.clicks = JSON.stringify(toSend);
            clicks = {};
        }
        var cachedRecords = localStorage.getItem('record');
        if (cachedRecords !== null && cachedRecords !== undefined) {
            if (cachedRecords.length > 30) {
                data.partial = cachedRecords;
            }
        }
        jQuery.ajax({
            type: "POST",
            crossDomain: UST.settings.serverPath !== '',
            data: {
                movements: data.movements,
                clicks: data.clicks,
                partial: data.partial,
                what: 'data',
                clientPageID: clientPageID
            },
            url: getContextPath() + 'addData.php',
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {},
            error: function(data) {
                console.log(data.responseText);
            }
        });
        activityCount = 0;
    };
    var lastTokenDate = localStorage.getItem('lastTokenDate');
    if (localStorage.getItem('token') === null || (new Date() - Date.parse(lastTokenDate) > 40000)) {
        localStorage.setItem('token', UST.randomToken());
        localStorage.removeItem('clientID');
    }
    var token = localStorage.getItem('token');
    localStorage.setItem('lastTokenDate', new Date());
    var focused = true;
    jQuery(document).hover(function() {
        focused = true;
    }, function() {
        focused = false;
    });
    var lastDate = new Date();
    var scrollTimeout = null;
    var maxTimeout = 3000;
    var movements = {};
    var clicks = {};
    var record = [];
    var activityCount = 0;
    var lastX, lastY, relX = 0;
    var cachedClicks = localStorage.getItem('clicks');
    if (cachedClicks !== null && cachedClicks !== undefined) {
        clicks = JSON.parse(cachedClicks);
        UST.sendData(localStorage.getItem('clientPageID'));
    }
    var cachedRecords = localStorage.getItem('record');
    localStorage.removeItem('record');
    if (cachedRecords !== null && cachedRecords !== undefined) {
        if (cachedRecords.length > 2) {
            jQuery.ajax({
                type: "POST",
                data: {
                    record: cachedRecords,
                    what: 'record',
                    clientPageID: localStorage.getItem('clientPageID')
                },
                url: getContextPath() + 'addData.php',
                beforeSend: function(x) {
                    if (x && x.overrideMimeType) {
                        x.overrideMimeType("application/j-son;charset=UTF-8");
                    }
                },
                success: function() {},
                error: function(data) {
                    console.log(data.responseText);
                }
            });
        } else {
            localStorage.removeItem('record');
        }
    }
    var clientPageID;
    var clientID = localStorage.getItem('clientID');
    jQuery.ajax({
        type: "POST",
        crossDomain: UST.settings.serverPath !== '',
        dataType: "JSON",
        data: {
            resolution: ((window.innerWidth || (document.documentElement.clientWidth + 17)) + ' ' + (window.innerHeight || (document.documentElement.clientHeight))),
            token: token,
            url: UST.getCleanPageURL(),
            domain: getDomain(),
            clientID: clientID,
        },
        url: getContextPath() + 'tracker/createClient.php',
        beforeSend: function(x) {
            if (x && x.overrideMimeType) {
                x.overrideMimeType("application/j-son;charset=UTF-8");
            }
        },
        success: function(data) {
            UST.DEBUG && console.log(data);
            clientPageID = data.clientPageID;
            localStorage.setItem('clientPageID', clientPageID);
            localStorage.setItem('clientID', data.clientID);
            startSendingData();
        },
        error: function(data) {
            console.log(data.responseText);
        }
    });
    jQuery.ajax({
        type: "POST",
        data: {
            clientPageID: localStorage.getItem('clientPageID')
        },
        crossDomain: UST.settings.serverPath !== '',
        url: getContextPath() + 'helpers/clearPartial.php',
        success: function() {
            UST.DEBUG && console.log('partials cleared');
        },
        error: function(data) {
            console.log("Could not clear partial!" + data.responseText);
        }
    });
    if (UST.settings.isStatic) {
        relX = parseInt(UST.getContentDiv().offset().left);
    }
    jQuery(document).click(function(e) {
        if (!focused) {
            return;
        }
        if (typeof e.pageX === 'undefined') {
            return;
        }
        if (UST.settings.recordClick) {
            var p = UST.coord4.fillZeros(e.pageX - relX).toString() + UST.coord4.fillZeros(e.pageY);
            if (clicks[p] === undefined) {
                clicks[p] = 0;
            }
            clicks[p]++;
        }
        record.push({
            t: 'c',
            x: e.pageX,
            y: e.pageY
        });
        localStorage.setItem('record', JSON.stringify(record));
        localStorage.setItem('url', UST.getCleanPageURL());
        activityCount += 10;
        if (jQuery(e.target).closest('a').length) {
            localStorage.setItem('clicks', JSON.stringify(clicks));
            localStorage.setItem('url', UST.getCleanPageURL());
        }
    });
    var lastScrollDate = undefined;
    jQuery(window).scroll(function() {
        var now = new Date();
        if (lastScrollDate == undefined || now - lastScrollDate >= 100) {
            UST.DEBUG && console.log('Scroll event recorded!');
            lastScrollDate = now;
            record.push({
                t: 's',
                x: jQuery(window).scrollLeft(),
                y: jQuery(window).scrollTop()
            });
            localStorage.setItem('record', JSON.stringify(record));
            activityCount++;
        }
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(function() {
            UST.DEBUG && console.log('Scroll event recorded!');
            record.push({
                t: 's',
                x: jQuery(window).scrollLeft(),
                y: jQuery(window).scrollTop()
            });
            localStorage.setItem('record', JSON.stringify(record));
            lastScrollDate = new Date();
            activityCount++;
        }, 100);
    });
    jQuery(document).mousemove(function(e) {
        if (!focused)
            return;
        var curDate = new Date();
        var passed = curDate - lastDate;
        if (passed < UST.settings.delay)
            return;
        if (--UST.settings.maxMoves > 0 && passed < maxTimeout) {
            if (lastX !== undefined && UST.settings.recordMove) {
                var p = UST.coord4.fillZeros(lastX).toString() + UST.coord4.fillZeros(lastY);
                if (!(lastX === 0 || lastY === 0)) {
                    if (movements[p] === undefined)
                        movements[p] = 0;
                    movements[p]++;
                }
            }
            if (!(lastX === 0 || lastY === 0)) {
                record.push({
                    t: 'm',
                    x: e.pageX,
                    y: e.pageY
                });
                localStorage.setItem('record', JSON.stringify(record));
                activityCount++;
            }
        }
        lastDate = curDate;
        lastX = e.pageX;
        lastY = e.pageY;
        if (UST.settings.isStatic) {
            lastX -= relX;
        }
    });
    if (UST.settings.recordKeyboard) {
        jQuery(document).on('blur', 'input:not([type="submit"]):not([type="button"]), textarea', function() {
            if (jQuery(this).hasClass('noRecord') || jQuery(this).attr('type') == 'password')
                return;
            var uniquePath = jQuery(this).getPath();
            record.push({
                t: 'b',
                p: uniquePath,
                v: jQuery(this).val()
            });
            localStorage.setItem('record', JSON.stringify(record));
        });
    }

    function startSendingData() {
        recurseSend(300);
    }

    function recurseSend(t) {
        UST.DEBUG && console.log("Sending data for clientPageID: ", clientPageID);
        if (t < 4000)
            t += 400;
        if (t > 2000 && localStorage.getItem('record') && activityCount > 10) {
            t = 800;
        }
        UST.sendData(clientPageID);
        setTimeout(function() {
            recurseSend(t);
        }, t);
    }
    jQuery.fn.getPath = function() {
        if (this.length != 1) throw 'Requires one element.';
        var path, node = this;
        if (node[0].id) return "#" + node[0].id;
        while (node.length) {
            var realNode = node[0],
                name = realNode.localName;
            if (!name) break;
            name = name.toLowerCase();
            var parent = node.parent();
            var siblings = parent.children(name);
            if (siblings.length > 1) {
                name += ':eq(' + siblings.index(realNode) + ')';
            }
            path = name + (path ? '>' + path : '');
            node = parent;
        }
        return path;
    };
};
var errorMessage = UST.testRequirements();
if (errorMessage !== 'ok') {
    console.log(errorMessage);
}
jQuery(function() {
    UST.init();
});
if (top !== self) {
    var elementUnder = null,
        lastElement = null;
    var lastEvent = null;
    var receiver = function(event) {
        if (event.origin == UST.settings.serverPath || true) {
            if (event.data[0] == '!' || event.data[0] > 'A' && event.data[0] < 'z')
                return;
            var data = JSON.parse(event.data);
            if (data.task !== undefined)
                lastEvent = event;
            switch (data.task) {
                case 'CSS':
                    for (var i = 0;; ++i) {
                        var classes = document.styleSheets[i];
                        if (classes === undefined || classes === null)
                            break;
                        classes = classes.rules;
                        if (classes === undefined || classes === null)
                            continue;
                        for (var x = 0; x < classes.length; x++) {
                            var ss = "";
                            if (classes[x].selectorText !== undefined) {
                                classes[x].selectorText = classes[x].selectorText.replace(':hover', '.hover');
                            }
                        }
                    }
                    break;
                case 'EL':
                    elementUnder = document.elementFromPoint(data.x, data.y);
                    break;
                case 'HOV':
                    iframeHover();
                    break;
                case 'CLK':
                    iframeRealClick();
                    break;
                case 'VAL':
                    jQuery(data.sel).trigger('focus').val(data.val);
                    break;
                case 'SZ':
                    event.source.postMessage(JSON.stringify({
                        task: 'SZ',
                        w: Math.max(jQuery(document).width(), jQuery('html').width(), window.innerWidth),
                        h: Math.max(jQuery(document).height(), jQuery('html').height(), window.innerHeight)
                    }), event.origin);
                    break;
                case 'PTH':
                    event.source.postMessage(JSON.stringify({
                        task: 'PTH',
                        p: location.pathname
                    }), event.origin);
                    break;
                case 'SCR':
                    jQuery(document).scrollTop(data.top);
                    jQuery(document).scrollLeft(data.left);
                    break;
                case 'STATIC':
                    event.source.postMessage(JSON.stringify({
                        task: 'STATIC',
                        X: UST.getContentDiv().offset().left
                    }), event.origin);
                    break;
                case 'addHtml2canvas':
                    if (typeof window.html2canvasAdded === "undefined") {
                        window.html2canvasAdded = true;
                        var s = document.createElement("script");
                        s.type = "text/javascript";
                        document.body.appendChild(s);
                        s.onload = function() {
                            event.source.postMessage(JSON.stringify({
                                task: 'html2canvasAdded'
                            }), event.origin);
                        };
                        s.src = UST.settings.serverPath + '/lib/html2canvas/html2canvas.js';
                    } else {
                        event.source.postMessage(JSON.stringify({
                            task: 'html2canvasAdded'
                        }), event.origin);
                    }
                    break;
                case 'screenshot':
                    jQuery(document).scrollTop(0);
                    jQuery(document).scrollLeft(0);
                    html2canvas(document.body, {
                        logging: false,
                        useCORS: false,
                        proxy: UST.settings.serverPath + '/lib/html2canvas/proxy.php',
                    }).then(function(canvas) {
                        var img = new Image();
                        img.onload = function() {
                            img.onload = null;
                            event.source.postMessage(JSON.stringify({
                                task: 'screenshot',
                                img: img.src
                            }), event.origin);
                        };
                        img.onerror = function() {
                            img.onerror = null;
                            window.console.log("Not loaded image from canvas.toDataURL");
                        };
                        img.src = canvas.toDataURL("image/png");
                    });
                    break;
            }
        }
    };
    jQuery(document).scroll(function() {
        var t = jQuery(this).scrollTop();
        var l = jQuery(this).scrollLeft();
        if (lastEvent !== null) {
            lastEvent.source.postMessage(JSON.stringify({
                task: 'SCROLL',
                top: t,
                left: l
            }), lastEvent.origin);
        } else {
            console.log("Scroll event happened before parent call to iframe");
        }
    });
    var iframeRealClick = function() {
        if (elementUnder !== null) {
            if (elementUnder.nodeName == 'SELECT') {
                jQuery(elementUnder).get(0).setAttribute('size', elementUnder.options.length);
            } else {
                var link = jQuery(elementUnder).parents('a').eq(0);
                if (link !== undefined) {
                    link = link.attr('href');
                    if (link !== undefined && (link.indexOf('//') != -1 || link.indexOf('www.') != -1) && link.indexOf(window.location.host) == -1)
                        link = 'external';
                }
                if (link !== 'external') {
                    if (!jQuery(elementUnder).closest('.UST_noClick').length) {
                        fireEvent(elementUnder, 'click');
                    } else {
                        UST.DEBUG && console.log("Didn't trigger the click. Had class UST_noClick");
                    }
                } else {
                    alertify.alert('User has left the website');
                }
            }
        }
        if (lastElement !== null && lastElement.nodeName == 'SELECT')
            jQuery(lastElement).get(0).setAttribute('size', 1);
        lastElement = elementUnder;
    };
    var lastHover = null;
    var lastParents = null;
    var iframeHover = function() {
        if (lastHover != elementUnder) {
            var parents = jQuery(elementUnder).parents().addBack();
            if (lastParents !== null) {
                lastParents.removeClass("hover");
                lastParents.trigger("mouseout");
            }
            parents.addClass("hover");
            parents.trigger("mouseover");
            lastParents = parents;
        } else {
            return 1;
        }
        lastHover = elementUnder;
        return 0;
    };
    var fireEvent = function(element, event) {
        var evt;
        if (document.createEvent) {
            evt = document.createEvent("HTMLEvents");
            evt.initEvent(event, true, true);
            return !element.dispatchEvent(evt);
        } else {
            evt = document.createEventObject();
            return element.fireEvent('on' + event, evt);
        }
    };
    window.addEventListener('message', receiver, false);
}

TrackPlayer

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="lib/heatmap.js"></script>
<script src="lib/html2canvas/html2canvas.js"></script>
<script src="lib/jquery.qtip.min.js"></script>
<script src="lib/jquery.uniform.min.js"></script>
<script src="lib/alertify.js"></script>
 
<script src="js/userTrackHeatmap.js"></script>
<script src="js/userTrackHeatmapDownload.js"></script>
<script src="js/userTrackRecords.js"></script>
<script src="js/userTrackScrollmap.js"></script>
<script src="js/userTrackAjax.js"></script>
<script src="js/userTrackAjaxSettings.js"></script>
<script src="js/userTrack.js"></script>
<script src="js/userTrackUI.js"></script>
<script src="js/clientList.js"></script>

userTrackHeatmap.js

var userTrackHeatmap = (function() {
    var heatmap = 0,
        minimap;
    var emptySet = {
        max: 0,
        data: []
    };

    function drawHeatmap() {
        DEBUG && console.log('createHeatmap');
        jQuery('#loading').stop(1, 0).fadeIn(200).text("Loading data...");
        userTrackAjax.loadHeatmapData();
        jQuery('#heatmapWrap').stop(1, 0).animate({
            opacity: 1
        }, 100);
        jQuery('#heatmapIframe').stop(1, 0).animate({
            opacity: 1
        }, 1000);
    }

    function setHeatmapData(data) {
        DEBUG && console.log('setHeatmapData called');
        jQuery('#loading').text("Drawing canvas...");
        minimap = jQuery('#minimap'), cleanHeatmap();
        heatmap = h337.create({
            container: document.getElementById("heatmap"),
            radius: options.radius
        });
        if (data === undefined || data.length === 0) {
            jQuery('#loading').text("No data stored in database...");
            return;
        }
        if (options.what == 'scrollmap') {
            drawScrollMap(data);
            return;
        }
        if (settings.static == "true" || settings.static === true) {
            oIframe.contentWindow.postMessage(JSON.stringify({
                task: 'STATIC'
            }), '*');
        } else {
            firstStaticX = 0;
        }
        setTimeout(function() {
            if (DEBUG) var starTime = new Date();
            var processedData = [],
                obj;
            for (var i = 0; i < data.length; ++i) {
                obj = JSON.parse(data[i]);
                for (el in obj) {
                    obj[el].x = parseInt(obj[el].x) + parseInt(firstStaticX);
                    obj[el].y = parseInt(obj[el].y);
                    obj[el].value = parseInt(obj[el].count);
                    delete obj[el].count;
                    if (isNaN(obj[el].x) || isNaN(obj[el].y) || obj[el].y < 0) {
                        DEBUG && console.log('Invalid point:', obj[el]);
                        continue;
                    }
                    processedData.push(obj[el]);
                }
            }
            processedData.sort(function(a, b) {
                if (a.y > b.y)
                    return 1;
                if (a.y == b.y) {
                    if (a.x > b.x)
                        return 1;
                    return 0;
                }
                return -1;
            });
            data = [processedData[0]];
            var N = 0;
            var maxValue = data[0].value;
            for (i = 1; i < processedData.length; ++i) {
                if (data[N].y == processedData[i].y && data[N].x == processedData[i].x) {
                    data[N].value += processedData[i].value;
                } else {
                    data.push(processedData[i]);
                    ++N;
                }
                if (data[N].value > maxValue)
                    maxValue = data[N].value;
            }
            heatmap.setData({
                max: maxValue,
                data: data
            });
            generateMinimap();
            jQuery('#loading').stop(1, 0).fadeOut(200);
            DEBUG && console.log('Total points: ', data.length);
            DEBUG && console.log('Time spent drawing: ', new Date() - starTime, 'ms');
        }, 100);
    }

    function cleanHeatmap() {
        jQuery('.heatmap-canvas').remove();
        if (!minimap) minimap = jQuery('#minimap');
        minimap.css('display', 'none');
        heatmap = 0;
    }

    function generateMinimap() {
        minimap.height(jQuery('#heatmapWrap').height() - 3);
        var oldCanvas = jQuery(".heatmap-canvas").get(0);
        scrollMinimap(0, 0);
        var newCanvas = document.getElementById("minimapCanvas");
        var context = newCanvas.getContext('2d');
        newCanvas.width = minimap.width();
        var ratio = newCanvas.width / oldCanvas.width;
        newCanvas.height = oldCanvas.height * ratio;
        context.drawImage(oldCanvas, 0, 0, oldCanvas.width, oldCanvas.height, 0, 0, newCanvas.width, newCanvas.height);
        minimap.show(200);
    }

    function scrollMinimap(scrollTop, scrollLeft) {
        var cursor = jQuery('#minimapCursor'),
            heatmapCanvas = jQuery(".heatmap-canvas"),
            minimapCanvas = jQuery('#minimapCanvas');
        var ratio = minimap.width() / heatmapCanvas.width();
        cursor.width(minimap.width() - 2);
        cursor.height(jQuery('#heatmapWrap').height() * ratio);
        scrollTop *= ratio;
        actualScroll = scrollTop;
        var miniH = minimap.height();
        miniH -= 3 / 10 * miniH;
        if (scrollTop + cursor.height() > miniH) {
            scrollTop = miniH - cursor.height();
        }
        if (actualScroll != scrollTop) {
            minimapCanvas.css('marginTop', -(actualScroll - scrollTop));
        } else {
            minimapCanvas.css('marginTop', 0);
        }
        cursor.css('top', scrollTop);
    }
    return {
        clean: cleanHeatmap,
        setData: setHeatmapData,
        generateMinimap: generateMinimap,
        scrollMinimap: scrollMinimap,
        draw: drawHeatmap,
    };
}());

userTrackHeatmapDownload.js

'use strict';
var userTrackDownload = {};
(function($) {
    $(function() {
        $('#downloadHeatmap').click(function() {
            jQuery('#loading').show().text("Adding the html2canvas library...");
            oIframe.contentWindow.postMessage(JSON.stringify({
                task: 'addHtml2canvas'
            }), "*");
        });
    });
}(jQuery));
userTrackDownload.start = function(base64Screenshot) {
    jQuery('#loading').show().text("Downloading the heatmap.");
    var heatmapCanvas = jQuery('.heatmap-canvas').get(0);
    var auxCanvas = sameSizeCanvas(heatmapCanvas);
    var context = auxCanvas.getContext('2d');
    var screenshot = new Image();
    screenshot.onload = function() {
        context.drawImage(screenshot, 0, 0, auxCanvas.width, auxCanvas.height);
        context.save();
        if (options.what === "scrollmap") {
            context.globalAlpha = parseFloat($(heatmapCanvas).css('opacity'));
        }
        context.drawImage(heatmapCanvas, 0, 0);
        context.restore();
        var a = document.createElement('a');
        a.href = auxCanvas.toDataURL("image/png");
        a.download = "userTrack_heatmap_" + options.domain + ".png";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        jQuery('#loading').hide(100);
    }
    screenshot.src = base64Screenshot;
}

function sameSizeCanvas(oldCanvas) {
    var newCanvas = document.createElement('canvas');
    newCanvas.width = oldCanvas.width;
    newCanvas.height = oldCanvas.height;
    return newCanvas;
}

userTrackRecords.js

var userTrackRecord = (function() {
    var lastElement = null;
    var scroll = {
        left: 0,
        top: 0
    };
    var cursor = jQuery('<img id="cursor" src="images/cursor.png"/>');
    var numberOfClicks = 0;
    jQuery(function() {
        jQuery('body').append(cursor);
    });

    function prepareRecord(id, page, res) {
        DEBUG && console.log('Prepare record: ', id, page, res);
        artificialTrigger = 1;
        fromList = 1;
        options.resolution = res;
        options.lastid = options.recordid = id;
        options.url = page;
        options.stopLoadEvents = true;
        var res = options.resolution.split(' ');
        iframeFit(res[0], res[1]);
        var absolutePath = '';
        if (options.domain !== '')
            absolutePath = '//' + options.domain;
        setIframeSource(absolutePath + options.url);
        userTrackAjax.getRecord(options.lastid);
        jQuery('#recordList').fadeOut(300);
        jQuery('#recordControls button').attr('disabled', false);
    }

    function setNextRecord(data) {
        DEBUG && console.log('Set next record: ', data);
        if (data.id !== 0) {
            artificialTrigger = true;
            prepareRecord(data.id, data.page, data.res);
        } else {
            inPlaybackMode = false;
            alertify.alert('User has left the website.');
        }
    }

    function setCurrentRecord(data) {
        record = data;
        setTimeout(function() {
            jQuery('#play').trigger('click');
            jQuery('#pagesHistory div').removeClass('active');
            jQuery('#pagesHistory div[data-id=' + options.recordid + ']').addClass('active');
        }, 500);
        fromList = 0;
    }

    function resetElements(minimizeBar) {
        scroll = {
            left: 0,
            top: 0
        };
        numberOfClicks = 0;
        jQuery('.clickBox').remove();
        if (minimizeBar === true)
            jQuery('#header').addClass("minified");
    }

    function startPlayback(data) {
        if (fromList) {
            userTrackAjax.getRecord(options.recordid);
            artificialTrigger = false;
        } else {
            if (artificialTrigger) {
                userTrackAjax.getRecord(options.lastid);
                artificialTrigger = false;
            }
        }
    }
    var lastP = {};

    function playRecord(i) {
        if (i === 0)
            resetElements(1);
        var p = record[i];
        if (p === undefined) {
            recordPlaying = false;
            jQuery('#recordControls button#play').text('Play');
            return;
        }
        progressBar.animate({
            width: Math.round((i + 1) * 100 / record.length) + '%'
        }, 50);
        p.x -= scroll.left;
        p.y -= scroll.top;
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'EL',
            x: p.x,
            y: p.y
        }), "*");
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'HOV'
        }), "*");
        if (p.t == 's') {
            scrollIframe(p.x + scroll.left, p.y + scroll.top);
            if (recordPlaying) {
                if (i + 1 < record.length) {
                    setTimeout(function() {
                        playRecord(i + 1);
                    }, 30);
                } else {
                    recordPlaying = false;
                    jQuery('#recordControls button#play').text('Play');
                    userTrackAjax.getNextRecord(options.lastid);
                }
            }
        } else {
            var dist = Math.max((Math.abs(lastP.x - p.x) * 2 + Math.abs(lastP.y - p.y) * 2), 100);
            dist = Math.min(dist, 800);
            cursor.animate({
                'top': p.y + jQuery('#heatmapIframe').offset().top,
                'left': p.x + jQuery('#heatmapIframe').offset().left
            }, dist, function() {
                lastP.x = p.x;
                lastP.y = p.y;
                if (p.t == 'c')
                    triggerClick(p.x, p.y);
                if (p.t == 'b') {
                    triggerValueChange(p.p, p.v, 0, i);
                    return;
                }
                if (playNext !== 0) {
                    i = Math.floor(playNext / 100 * record.length);
                    playNext = 0;
                }
                if (i + 1 < record.length) {
                    if (recordPlaying)
                        if (p.t == 'c')
                            setTimeout(function() {
                                playRecord(i + 1);
                            }, 200);
                        else
                            playRecord(i + 1);
                } else {
                    recordPlaying = false;
                    jQuery('#recordControls button#play').text('Play');
                    userTrackAjax.getNextRecord(options.lastid);
                }
            });
        }
    }

    function triggerClick(x, y) {
        x += jQuery('#heatmapIframe').offset().left;
        var circle = jQuery("<div class='clickRadius'>&nbsp;</div>");
        var radius = 30;
        circle.css('top', y).css('left', x);
        jQuery('#pageWrap').append(circle);
        circle.animate({
            'height': radius,
            'width': radius,
            'top': y - radius / 2,
            'left': x - radius / 2,
            'opacity': 0.3
        }, 500, function(v) {
            circle.animate({
                'height': 2 * radius,
                'width': 2 * radius,
                'top': y - radius,
                'left': x - radius,
                'opacity': 0
            }, 100, function() {
                jQuery(this).remove();
            });
        });
        numberOfClicks++;
        var clickBox = jQuery("<span class='clickBox' data-top='" + (y + scroll.top) + "' data-left='" + (x + scroll.left) + "'>" +
            numberOfClicks + "</span>");
        clickBox.css('top', y).css('left', x);
        jQuery('#pageWrap').append(clickBox);
        clickBox.delay(200).fadeIn(500);
        clickSound.play();
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'CLK'
        }), "*");
    }

    function triggerValueChange(sel, val, l, i) {
        if (val.length >= l) {
            oIframe.contentWindow.postMessage(JSON.stringify({
                task: 'VAL',
                sel: sel,
                val: val.slice(0, l)
            }), "*");
            setTimeout(function() {
                triggerValueChange(sel, val, l + 1, i);
            }, 60);
        } else {
            if (i + 1 < record.length) {
                playRecord(i + 1);
            } else {
                recordPlaying = false;
                jQuery('#recordControls button#play').text('Play');
                userTrackAjax.getNextRecord(options.lastid);
            }
        }
    }

    function iframeRealClick() {
        if (elementUnder !== null) {
            if (elementUnder.nodeName == 'SELECT')
                jQuery(elementUnder).get(0).setAttribute('size', elementUnder.options.length);
            else {
                var link = jQuery(elementUnder).parents('a').eq(0);
                if (link !== undefined) {
                    link = link.attr('href');
                    if (link !== undefined && (link.indexOf('//') != -1 || link.indexOf('www.') != -1) && link.indexOf(window.location.host) == -1)
                        link = 'external';
                }
                if (link != 'external')
                    fireEvent(elementUnder, 'click');
                else {
                    alertify.alert('User has left the website');
                }
            }
        }
        if (lastElement !== null && lastElement.nodeName == 'SELECT')
            jQuery(lastElement).get(0).setAttribute('size', 1);
        lastElement = elementUnder;
    }

    function scrollIframe(x, y) {
        scroll.left = x;
        scroll.top = y;
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'SCR',
            top: y,
            left: x
        }), "*");
    }

    function setRecordList(data) {
        var pageHistoryDiv = jQuery('#pagesHistory');
        pageHistoryDiv.html('');
        for (var v in data) {
            var page = data[v];
            var div = jQuery('<div></div>');
            div.attr('data-url', page.page);
            div.attr('data-resolution', page.res);
            div.attr('data-date', page.date);
            div.attr('data-id', page.id);
            div.text(page.page);
            div.attr('title', page.page);
            if (page.id == 0) {
                div.addClass('disabled');
                div.attr('title', 'User visited this page but left it before any data could be recorded');
            }
            pageHistoryDiv.append(div);
        }
    }
    return {
        startPlayback: startPlayback,
        setCurrent: setCurrentRecord,
        setNext: setNextRecord,
        prepare: prepareRecord,
        playFrom: playRecord,
        setRecordList: setRecordList,
        reset: resetElements,
    };
}());

userTrackScrollmap.js

function drawScrollMap(data) {
    var ctx = document.querySelector('canvas');
    ctx = ctx.getContext('2d');
    ctx.fillStyle = "rgba(100, 200, 200, 1)";
    ctx.fillRect(0, 0, 2000, 5000);
    var dataCount = 0;
    var yPos = new Array();
    yPos.push(0);
    for (i in data) {
        data[i] = JSON.parse(data[i]);
        for (move in data[i])
            yPos.push(parseInt(data[i][move].y));
    }
    yPos.sort(function(a, b) {
        return a - b
    });
    yPos = yPos.filter(function(elem, pos) {
        return yPos.indexOf(elem) == pos
    });
    var step = 200 / yPos.length;
    var green = 0;
    for (i in yPos) {
        ctx.fillStyle = "rgb(" + (250 - Math.floor(green)) + ", 70, 100)";
        ctx.fillRect(0, yPos[i - 1 < 0 ? 0 : i - 1], 2000, yPos[i] + 100);
        green += step;
    }
    jQuery('.heatmap-canvas').css('opacity', 0.5);
    userTrackHeatmap.generateMinimap();
    jQuery('#loading').stop(1, 0).fadeOut(200);
}

userTrackAjax.js

var DEBUG = false;
var userTrackAjax = (function() {
    function catchFail() {
        alert("Something went wrong on the server-side. Please try again!");
    }

    function getResolutions() {
        if (options.url === undefined) {
            alert('No pages saved in the database. Database may be empty.');
            return;
        }
        jQuery.ajax({
            type: 'POST',
            dataType: "json",
            url: 'helpers/getResolutions.php',
            data: {
                url: options.url,
                domain: options.domain
            },
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                DEBUG && console.log('resolutions', data);
                jQuery('#resolution').html('<option value="-1" selected>Any</option>');
                for (var v in data)
                    jQuery('#resolution').append('<option value="' + data[v][0] + '">' + data[v][0].split(' ')[0] + ' x ' + data[v][0].split(' ')[1] + '</option>');
                if (artificialTrigger) {
                    jQuery('#resolution option[value="' + options.resolution + '"]').trigger('change');
                }
                if (options.what != 'record') {
                    jQuery('#resolution option').each(function() {
                        var curEl = jQuery(this);
                        var curWidth = curEl.val().split(' ')[0];
                        if (curWidth < 0)
                            return 1;
                        curEl.text(curWidth);
                        var index = curEl.index();
                        for (var i = index + 1; i < jQuery('#resolution option').length; ++i) {
                            var el = jQuery('#resolution option').eq(i);
                            if (el.val().split(' ')[0] == curWidth)
                                jQuery('#resolution option').eq(i).val('-2');
                        }
                        jQuery('#resolution option[value="-2"]').remove();
                    });
                }
            },
            error: function(data) {
                alert(data.responseText);
            }
        });
    }

    function getPages() {
        jQuery.ajax({
            type: 'POST',
            dataType: "json",
            url: 'helpers/getPages.php',
            data: {
                domain: options.domain
            },
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                jQuery('#page').html('');
                for (var v in data)
                    jQuery('#page').append('<option value="' + data[v] + '">' + data[v] + '</option>');
                if (data[0]) jQuery('#page option[value="' + data[0] + '"]').attr('selected', true);
                var defaultUrl = jQuery('#page option:first').val() || '/';
                if (localStorage.what !== undefined) {
                    options.what = localStorage.what;
                    jQuery('.opt').removeClass('selected');
                    jQuery('.opt[data-value=' + options.what + ']').addClass('selected');
                    if (options.what == 'record')
                        jQuery('.opt.selected').trigger('click');
                }
                jQuery('#loading').text("Loading webpage");
                var absolutePath = '';
                if (options.domain !== '')
                    absolutePath = '//' + options.domain;
                setIframeSource(absolutePath + defaultUrl);
                options.url = defaultUrl;
                jQuery("select,input").uniform();
                jQuery('#page').trigger('change');
            },
            error: function(data) {
                if (data.responseText.indexOf('login') != -1)
                    window.location = 'login.php';
                else
                    alert("Could not load pages list from db." + data.responseText);
            }
        });
    }

    function limitRecordNumber() {
        jQuery.ajax({
            type: 'POST',
            dataType: "json",
            url: 'helpers/limitRecordNumber.php',
            data: {
                domain: options.domain
            },
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {},
            error: function(data) {
                alert(data.responseText);
            }
        });
    }

    function setRecordLimit(domain, limit) {
        jQuery.ajax({
            type: 'POST',
            url: 'helpers/setRecordLimit.php',
            data: {
                limit: limit,
                domain: domain
            },
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {},
            error: function(data) {
                alert(data.responseText);
            }
        });
    }

    function getRecordLimit(domain, callback, elIndex) {
        jQuery.post("helpers/getRecordLimit.php", {
            domain: domain
        }).done(function(data) {
            callback(elIndex, data);
        }).fail(function(data) {
            alert(data.responseText);
        });
    }

    function populateClientsList(from) {
        var take = jQuery('#numberFilter select').val();
        var startDate = jQuery('#rangeFilter input[name="from"]').val();
        var endDate = jQuery('#rangeFilter input[name="to"]').val();
        jQuery.ajax({
            type: 'POST',
            dataType: "json",
            url: 'helpers/getClients.php',
            data: {
                from: from,
                take: take,
                domain: options.domain,
                startDate: startDate,
                endDate: endDate,
                order: localStorage.order
            },
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                jQuery('#recordList table tr:has(td)').remove();
                if (data === null || data.clients.length === 0) {
                    jQuery('#recordList table').append('<tr><td colspan="6"><h3>Database is empty.</h3></td></tr>');
                    return;
                }
                var cnt = data.count;
                data = data.clients;
                for (var v in data) {
                    jQuery('#recordList table').append('<tr data-id="' + data[v].token + '"></tr>');
                    if (data[v].recordid == null)
                        data[v].nr = 0;
                    var n = 0;
                    for (var i in data[v]) {
                        if (++n > 5)
                            break;
                        var td = jQuery('<td class="' + i + '">' + data[v][i] + '</td>');
                        jQuery('#recordList table tr:last').append(td);
                        switch (i) {
                            case 'ip':
                                var newIP = data[v].ip;
                                if (censorIP) {
                                    newIP = data[v].ip.slice(0, -3) + '***';
                                }
                                td.html('<img src="images/flags/xx.png"/> ' + newIP);
                                if (localStorage['c' + data[v].ip] !== undefined) {
                                    td.html('<img src="images/flags/' + localStorage['c' + data[v].ip] + '.png"/> ' + newIP);
                                } else {
                                    (function(ip, td, newIP) {
                                        setTimeout(function() {
                                            addCountryFlag(ip, td, newIP);
                                        }, 300 * v);
                                    })(data[v].ip, td, newIP);
                                }
                                break;
                            case 'pageHistory':
                                td.empty();
                                td.append(data[v].nr + ' page' + (data[v].nr != 1 ? 's' : ''));
                                var timeSpentString = '';
                                var ts = data[v].timeSpent;
                                if (ts > 3600) {
                                    var hours = ts / 3600 | 0;
                                    timeSpentString += hours + 'h ';
                                    ts -= hours * 3600;
                                }
                                if (ts > 60) {
                                    var mins = ts / 60 | 0;
                                    timeSpentString += mins + 'm ';
                                    ts -= mins * 60;
                                }
                                timeSpentString += ts + 's';
                                td.append(' in ' + timeSpentString + ':');
                                var pageList = data[v][i].split(' ');
                                for (var index in pageList) {
                                    var pageName = pageList[index];
                                    var dotPosition = pageName.lastIndexOf('.');
                                    if (dotPosition != -1 && pageName.length - dotPosition - 1 < 5) {
                                        pageName = pageName.slice(0, pageName.lastIndexOf('.'));
                                    }
                                    pageList[index] = '<div class="pageEntry">' + pageName + '</div>';
                                    td.append(pageList[index]);
                                }
                                td.attr('width', '50%');
                                break;
                        }
                    }
                    var disabled = data[v].nr == 0 ? ' disabled title="No movements were recorded. Client may have left immediately." ' : '';
                    var firstPage = data[v].pageHistory;
                    if (data[v].pageHistory.indexOf(' ') != -1) {
                        firstPage = data[v].pageHistory.split(' ')[0];
                    }
                    jQuery('#recordList table tr:last').append('<td><button ' + 'data-recordid="' + data[v].recordid + '" ' + 'data-page="' + firstPage + '" ' + 'data-resolution="' + data[v].resolution + '" ' +
                        disabled + '>Play record</button></td>');
                    var br = jQuery('#recordList table tr:last td.browser').text();
                    if (br.indexOf('chrome') != -1)
                        br = br.replace('chrome', '<img src="images/icons/chrome.png" title="Google Chrome"/>');
                    else if (br.indexOf('opera') != -1)
                        br = br.replace('opera', '<img src="images/icons/opera.png" title="Opera"/>');
                    else if (br.indexOf('msie') != -1)
                        br = br.replace('msie', '<img src="images/icons/ie.png" title="Internet Explorer"/>');
                    else if (br.indexOf('firefox') != -1)
                        br = br.replace('firefox', '<img src="images/icons/firefox.png" title="Mozilla Firefox"/>');
                    else if (br.indexOf('mozilla') != -1)
                        br = br.replace('mozilla', '<img src="images/icons/firefox.png" title="Mozilla Firefox"/>');
                    else if (br.indexOf('safari') != -1)
                        br = br.replace('safari', '<img src="images/icons/safari.png" title="Safari"/>');
                    jQuery('#recordList table tr:last td.browser').html(br);
                }
                jQuery('#pagination').html('');
                if (cnt) {
                    var totalPages = cnt / take + (cnt % take != 0);
                    var currentPage = from / take + 1;
                    for (var i = 1; i <= totalPages; ++i) {
                        var selected = i == currentPage ? "selected" : "";
                        jQuery('#pagination').append('<span class="' + selected + '">' + i + '</span>');
                    }
                }
                bindClickToList();
            },
            error: function(data) {
                alert(data.responseText);
            }
        });
    }

    function addCountryFlag(ip, td, newIP) {
        jQuery.ajax({
            type: 'POST',
            url: 'helpers/getCountry.php',
            data: {
                ip: ip
            },
            success: function(data) {
                if (data.length == 2) {
                    td.html('<img src="images/flags/' + data.toLowerCase() + '.png"/> ' + newIP);
                    if (data.toLowerCase() != 'xx')
                        localStorage.setItem('c' + ip, data.toLowerCase());
                }
            },
            error: function(data) {}
        });
    }

    function getNextRecord(id) {
        jQuery.ajax({
            type: 'POST',
            dataType: "json",
            url: 'helpers/getNextRecord.php',
            data: {
                id: id,
                domain: options.domain
            },
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                DEBUG && console.log("getNextRecord", data);
                data.id = Number(data.id);
                userTrackRecord.setNext(data);
            },
            error: function(data) {
                alert(data.responseText);
            }
        });
    }

    function getRecord(id) {
        options.lastid = id;
        jQuery.ajax({
            type: "POST",
            dataType: "json",
            data: {
                recordid: id,
                page: options.url,
                resolution: options.resolution,
                what: options.what
            },
            url: 'getData.php',
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                userTrackRecord.setCurrent(data);
            },
            error: function(data) {
                alert("Could not load data!" + data.responseText);
            }
        });
    }

    function getRecordList(token) {
        jQuery.ajax({
            type: "POST",
            dataType: "json",
            data: {
                token: token
            },
            url: 'helpers/getRecordList.php',
            beforeSend: function(x) {
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                userTrackRecord.setRecordList(data);
            },
            error: function(data) {
                alert("Could not load data!" + data.responseText);
            }
        });
    }

    function loadHeatmapData() {
        DEBUG && console.log("loadHeatmapData");
        jQuery.ajax({
            type: "POST",
            dataType: "json",
            data: {
                page: options.url,
                resolution: options.resolution,
                what: options.what,
                domain: options.domain
            },
            url: 'getData.php',
            beforeSend: function(x) {
                jQuery('#loading').text("Retrieving data from database...");
                if (x && x.overrideMimeType) {
                    x.overrideMimeType("application/j-son;charset=UTF-8");
                }
            },
            success: function(data) {
                userTrackHeatmap.setData(data);
            },
            error: function(data) {
                console.log(data);
                if (data.responseText.indexOf('login') != -1)
                    window.location = 'login.php';
                else
                    alert("Could not load heatmap data." + data.responseText);
            }
        });
    }

    function deleteRecord(id) {
        if (id > 0) {
            jQuery.post('helpers/deleteRecord.php', {
                recordid: id
            }).done(function(data) {
                alert('Record deleted!');
            }).fail(function(data) {
                console.log(data);
                alert("Could not delete record!" + data.responseText);
            });
        } else {
            alert('Incorect id format.');
            return 0;
        }
    }

    function deleteClient(token, el) {
        jQuery.post('helpers/deleteClient.php', {
            token: token
        }).done(function(data) {
            jQuery(el).slideUp(200, function() {
                jQuery(this).remove();
            });
        }).fail(function(data) {
            console.log(data);
            alert("Could not delete client!" + data.responseText);
        });
    }

    function cleanDataForDomain(domain) {
        jQuery.post('helpers/cleanDatabase.php', {
            domain: domain
        }, function(data) {
            if (data === '')
                alert('All data stored in the database has been deleted.');
            else alert("Error: " + data);
            window.location.reload();
        });
    }

    function deleteZeroRecords(domain) {
        jQuery.post('helpers/deleteZeroRecords.php', {
            domain: domain
        }, function(data) {
            if (data === '')
                alert('Sessions with 0 data have been deleted.');
            else alert("Error: " + data);
            window.location.reload();
        });
    }

    function getUsersList(callback) {
        jQuery.getJSON('helpers/users/getUserList.php', function(data) {
            callback(data);
        });
    }

    function setUserData(dataType, value, id, shouldLogout) {
        if (dataType.indexOf('name') != -1)
            dataType = 'name';
        jQuery.post('helpers/users/setUserData.php', {
            dataType: dataType,
            value: value,
            userId: id
        }).done(function(data) {
            if (data !== '') {
                alert(data);
            } else {
                if (shouldLogout) {
                    window.location = 'helpers/users/logout.php';
                }
            }
        }).fail(catchFail);
    }

    function changeUserAccess(type, domain, userid) {
        jQuery.post('helpers/users/changeAccess.php', {
            type: type,
            domain: domain,
            userid: userid
        }).done(function(data) {
            if (data !== '')
                alert(data);
            else
                location.reload();
        }).fail(catchFail);
    }

    function addUser(name, pass) {
        jQuery.post('helpers/users/addUser.php', {
            name: name,
            pass: pass
        }).done(function(data) {
            if (data !== '')
                alert(data);
            else
                location.reload();
        }).fail(catchFail);
    }

    function deleteUser(userId) {
        jQuery.post('helpers/users/deleteUser.php', {
            id: userId
        }).done(function(data) {
            if (data !== '')
                alert(data);
            else
                location.reload();
        }).fail(catchFail);
    }
    return {
        getPages: getPages,
        populateClientsList: populateClientsList,
        loadHeatmapData: loadHeatmapData,
        getResolutions: getResolutions,
        limitRecordNumber: limitRecordNumber,
        setRecordLimit: setRecordLimit,
        getRecordLimit: getRecordLimit,
        getRecord: getRecord,
        getRecordList: getRecordList,
        getNextRecord: getNextRecord,
        deleteClient: deleteClient,
        cleanDataForDomain: cleanDataForDomain,
        deleteZeroRecords: deleteZeroRecords,
        deleteRecord: deleteRecord,
        getUsersList: getUsersList,
        setUserData: setUserData,
        changeUserAccess: changeUserAccess,
        addUser: addUser,
        deleteUser: deleteUser
    };
}());

userTrackAjaxSettings.js

var options = {};
options.radius = 30;
options.url = '';
options.resolution = '-1';
options.what = localStorage.what !== undefined ? localStorage.what : 'movements';
options.domain = '';

function saveSettings() {
    DEBUG && console.log("Saving settings...");
    jQuery.ajax({
        type: "POST",
        url: 'helpers/saveSettings.php',
        data: {
            delay: jQuery('#delayRange').val(),
            static: jQuery('#staticWebsite').is(':checked'),
            recordClick: jQuery('#recordClicks').is(':checked'),
            recordMove: jQuery('#recordMove').is(':checked'),
            recordKey: jQuery('#recordKey').is(':checked'),
            maxMove: jQuery('#maxMoves').val(),
            serverPath: jQuery('#serverPath').val(),
            ignoreGET: jQuery('#ignoreGET').val(),
            percentangeRecorded: jQuery('#percentangeRecorded').val(),
        },
        success: function() {
            alert("Settings successfully saved!");
        },
        error: function(data) {
            alert("Could not save settings!" + data.responseText);
        }
    });
}

function loadSettings() {
    jQuery.getJSON('helpers/loadSettings.php', function(data) {
        settings = data;
    }).fail(function(data) {
        if (data.responseText.indexOf('login') != -1)
            window.location = 'login.php';
        else
            alert("Could not load settings from file." + data.responseText);
    });
}

userTrack.js

var record = {};
var recordPlaying = false;
var inPlaybackMode = false;
var playNext = 0;
var drawTimeout;
var progressBar;
var artificialTrigger = false;
var fromList = false;
var censorIP = true;
var settings = JSON.parse('{"delay":"200","recordClicks":"true","recordMoves":"true","static":"false","maxMoves":"300"}');
var iframePath, oIframe;
var firstStaticX = 0;
var recordsPlayed = [];
jQuery(function() {
    progressBar = jQuery('#progressBar div');
    if (localStorage.getItem('domain') !== null)
        options.domain = localStorage.getItem('domain');
    loadSettings();
    userTrackAjax.getPages();
    userTrackAjax.populateClientsList(0);
    jQuery('#deleteRecord').dblclick(function() {
        var id = options.recordid;
        if (userTrackAjax.deleteRecord(id) !== 0) {
            jQuery("#records option[value=" + id + "]").remove();
        }
    });
    jQuery('#resolution').change(function() {
        DEBUG && console.log("resolution changed");
        options.resolution = jQuery(this).val();
        userTrackHeatmap.clean();
        if (options.resolution != -1) {
            var res = jQuery(this).val().split(' ');
            iframeFit(res[0], res[1]);
        } else {
            iframeFit();
        }
    });
    jQuery('#page').change(function() {
        DEBUG && console.log("page changed to", jQuery(this).val());
        if (jQuery(this).children().length === 0)
            return;
        options.url = localStorage.url = jQuery(this).val();
        var absolutePath = '';
        if (options.domain !== '')
            absolutePath = '//' + options.domain;
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'PTH'
        }), "*");
        if (fromList || iframePath != options.url)
            setIframeSource(absolutePath + options.url);
        if (!artificialTrigger) {
            options.resolution = '-1';
            setIframeSource(absolutePath + options.url);
        }
        userTrackAjax.getResolutions();
    });
    jQuery('.opt').click(function() {
        userTrackRecord.reset();
        options.what = localStorage.what = jQuery(this).attr('data-value');
        jQuery('.opt').removeClass('selected');
        jQuery(this).addClass('selected');
        jQuery('#heatmapWrap').css('opacity', 0).hide();
        if (options.what == 'record') {
            showRecordsList();
            jQuery('#recordControls button').attr('disabled', true);
            jQuery('#cursor,#recordControls,#heatmapIframe').animate({
                opacity: 1
            }, 300);
            jQuery('#windowWidth').slideUp(200);
            jQuery('#hoverWrap, #progressBar').slideDown(200);
            jQuery('#loading').hide(100);
            jQuery('#downloadHeatmap').hide(100);
        } else {
            jQuery('#recordList #close').trigger('click');
            jQuery('#loading').show().text("Retrieving " + options.what + " statistics...");
            jQuery('#cursor,#recordControls').animate({
                opacity: 0
            }, 300);
            jQuery('#windowWidth').slideDown(200);
            jQuery('#hoverWrap, #progressBar').slideUp(200);
            jQuery('#resolution').trigger('change');
            jQuery('#downloadHeatmap').show(100);
        }
    });

    function iframeFit(width, height) {
        DEBUG && console.log('iframeFit');
        if (width === undefined)
            width = jQuery(window).width() - 29;
        if (height === undefined)
            height = jQuery(window).height() - jQuery('#header').outerHeight() - 10;
        jQuery('#heatmapIframe').height(height);
        jQuery('#heatmapIframe').width(parseInt(width) + 24);
        jQuery('#heatmapIframe').center();
        if (options.what != 'record')
            jQuery('#heatmapWrap').show();
        jQuery('#heatmapWrap').width(jQuery('#heatmapIframe').width() - 20);
        jQuery('#heatmapWrap').height(jQuery('#heatmapIframe').height() - 10);
        if (options.what != 'record')
            jQuery('#heatmapWrap').fadeIn(200);
        jQuery('#heatmapWrap').css('left', jQuery('#heatmapIframe').offset().left);
        jQuery('#heatmapWrap').css('top', jQuery('#heatmapIframe').offset().top);
        if (options.what == 'record') {
            jQuery('#loading').fadeOut(200);
            return;
        }
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'SZ'
        }), "*");
        if (options.what != 'record')
            jQuery('#heatmapWrap').fadeIn(200);
        jQuery('#heatmapWrap').css('left', jQuery('#heatmapIframe').offset().left);
        jQuery('#heatmapWrap').css('top', jQuery('#heatmapIframe').offset().top);
    };
    window.iframeFit = iframeFit;
    jQuery('#heatmapIframe').load(function() {
        DEBUG && console.log("iframe loaded");
        if (inPlaybackMode)
            return;
        if (fromList)
            return;
        if (options.stopLoadEvents) {
            options.stopLoadEvents = false;
            return;
        }
        iframeFit(undefined, undefined);
        fromList = 0;
    });
    oIframe = document.getElementsByTagName('iframe')[0];
    jQuery('#recordControls button#play').click(function() {
        if (recordPlaying) {
            inPlaybackMode = recordPlaying = false;
            jQuery(this).text('Play');
            return;
        }
        jQuery(this).text('Stop');
        inPlaybackMode = recordPlaying = true;
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'SCR',
            top: 0,
            left: 0,
            delay: 0
        }), "*");
        oIframe.contentWindow.postMessage(JSON.stringify({
            task: 'CSS'
        }), "*");
        if (progressBar.css('width') == '0%' || progressBar.css('width') == '0px')
            userTrackRecord.playFrom(0);
        else
            progressBar.animate({
                width: '0%'
            }, 500, function() {
                userTrackRecord.playFrom(0);
            });
    });
    jQuery('#progressBar').click(function(e) {
        playNext = Math.floor(100 * (e.pageX - jQuery(this).offset().left) / jQuery(this).width());
    });
});

function bindClickToList() {
    jQuery('#recordList tr').on('click', function() {
        jQuery(this).toggleClass('selected');
    });
    jQuery('#pagination span').on('click', function() {
        var val = jQuery(this).text();
        var take = jQuery('#numberFilter select').val();
        userTrackAjax.populateClientsList((val - 1) * take);
    });
}

function handleIframeResponse(e) {
    if (e.data[0] == '!' || e.data[0] > 'A' && e.data[0] < 'z')
        return;
    var data = jQuery.parseJSON(e.data);
    switch (data.task) {
        case 'SZ':
            jQuery('#heatmap').width(data.w);
            jQuery('#heatmap').height(data.h);
            if (options.what != 'record')
                userTrackHeatmap.draw();
            break;
        case 'PTH':
            iframePath = data.path;
            break;
        case 'SCROLL':
            jQuery('#heatmap').css('top', -data.top);
            jQuery('#heatmap').css('left', -data.left);
            userTrackHeatmap.scrollMinimap(data.top, data.left);
            jQuery('.clickBox').each(function() {
                var t = jQuery(this);
                t.css({
                    'top': Number(t.attr('data-top')) - data.top,
                    'left': Number(t.attr('data-left')) - data.left
                });
            });
            break;
        case 'STATIC':
            firstStaticX = data.X;
            break;
        case 'html2canvasAdded':
            jQuery('#loading').show().text("Generating the screenshot.");
            oIframe.contentWindow.postMessage(JSON.stringify({
                task: 'screenshot'
            }), "*");
            break;
        case 'screenshot':
            userTrackDownload.start(data.img);
            break;
    }
}

function setIframeSource(link) {
    if (window.location.href.indexOf("http://www.") == -1)
        link = link.replace('http://www.', 'http://');
    link = link.replace('http://', '//');
    link = link.replace('https://', '//');
    jQuery('#heatmapIframe').prop('src', link);
}
window.addEventListener('message', handleIframeResponse, false);

userTrackUI.js

jQuery.fn.center = function() {
    this.css("position", "absolute");
    this.css("left", Math.max(0, (jQuery(window).width() - jQuery(this).outerWidth()) / 2 + jQuery(window).scrollLeft()) + "px");
    return this;
};
jQuery.fn.centerv = function() {
    this.css("position", "absolute");
    this.css("top", Math.max(0, (jQuery(window).height() - jQuery(this).outerHeight()) / 2 + jQuery(window).scrollTop()) + "px");
    return this;
};

function showSettings() {
    jQuery('#delayRange').val(settings.delay).trigger('change');
    jQuery('#maxMoves').val(settings.maxMoves).trigger('change');
    jQuery('#staticWebsite').prop('checked', settings.static == "true");
    jQuery('#recordClicks').prop('checked', settings.recordClicks == "true");
    jQuery('#recordMove').prop('checked', settings.recordMoves == "true");
    jQuery('#recordKey').prop('checked', settings.recordKey == "true");
    jQuery('#serverPath').val(settings.serverPath).trigger('change');
    jQuery('#ignoreGET').val(eval(settings.ignoreGET)).trigger('change');
    jQuery('#percentangeRecorded').val(settings.percentangeRecorded).trigger('change');
    jQuery('#settings').center();
    jQuery('#settings').centerv();
    jQuery('#settings').fadeIn(300);
    jQuery.uniform.update();
}

function showRecordsList() {
    jQuery('#recordList tr').removeClass('selected');
    jQuery('#recordList').center();
    jQuery('#recordList').fadeIn(300);
}

function minimizeIfNeeded() {
    if (options.what == 'record' && lastPosY > 50)
        if (Date.now() - lastMouseMove > 3200)
            jQuery('#header').addClass("minified");
}
var lastMouseMove = Date.now();
var lastPosY = 1000;
jQuery(document).mousemove(function(e) {
    if (e.pageY < 50) {
        lastMouseMove = Date.now();
        jQuery('#header').removeClass("minified");
    }
    lastPosY = e.pageY;
});
var clickSound = new Audio("images/click.mp3");
jQuery(function() {
    jQuery('.ust_dialog #close').click(function() {
        jQuery(this).parent().fadeOut(300);
    });
    jQuery('#show_settings').click(function(e) {
        e.preventDefault();
        loadSettings();
        showSettings();
    });
    jQuery('#delayRange').on('change mousemove', function() {
        jQuery('#range_value').text(jQuery(this).val() + 'ms');
    });
    jQuery('#maxMoves').on('change mousemove', function() {
        jQuery('#range_value2').text(jQuery(this).val());
    });
    jQuery('#percentangeRecorded').on('change mousemove', function() {
        jQuery('#range_value3').text(jQuery(this).val() + '%');
    });
    jQuery('#save_settings').click(function() {
        saveSettings();
    });
    if (localStorage.censorIP == 'false') {
        censorIP = false;
        jQuery('#censorIP').prop('checked', false);
    }
    jQuery('#censorIP').change(function() {
        if (!jQuery(this).is(':checked'))
            localStorage.censorIP = 'false';
        else
            localStorage.censorIP = 'true';
    });
    jQuery("select,input").uniform();
    jQuery("*[title]").qtip({
        content: {
            attr: 'title'
        },
        style: {
            classes: 'qtip-rounded qtip-red tooltip'
        },
        position: {
            target: 'mouse',
            adjust: {
                y: 20,
                x: 20
            },
            viewport: jQuery(window)
        }
    });
    jQuery(".opt").qtip({
        content: {
            attr: 'title'
        },
        style: {
            classes: 'qtip-rounded qtip-red tooltip'
        },
        position: {
            my: 'top center',
            at: 'bottom center',
            adjust: {
                y: 5,
            }
        }
    });
    jQuery(document).on('click', '#recordList button', function() {
        userTrackRecord.prepare(jQuery(this).attr('data-recordid'), jQuery(this).attr('data-page'), jQuery(this).attr('data-resolution'));
        setRecordInfo(jQuery(this));
    });
    jQuery('#recordInfo').click(function() {
        jQuery(this).toggleClass('active');
    });

    function setRecordInfo(selectButton) {
        var parent = selectButton.parent().parent();
        jQuery('#userFlag').html(parent.find('.ip img').clone());
        jQuery('#resolutionInfo').text(options.resolution.replace(' ', 'x') + ' ');
        jQuery('#resolutionInfo').append(parent.find('.browser').html());
        jQuery('#urlInfo').text(options.url);
        jQuery('#dateInfo').text(parent.find('.date').text());
        userTrackAjax.getRecordList(parent.attr('data-id'));
    }
    jQuery('button#nextPage').click(function() {
        if (jQuery('#play').text() == 'Stop')
            jQuery('#play').trigger('click');
        userTrackAjax.getNextRecord(options.lastid);
    });
    jQuery('#pagesHistory').on('click', 'div', function() {
        if (jQuery('#play').text() == 'Stop')
            jQuery('#play').trigger('click');
        userTrackRecord.prepare(jQuery(this).attr('data-id'), jQuery(this).attr('data-url'), jQuery(this).attr('data-resolution'));
    });
    localStorage.order = localStorage.order || 'DESC';
    jQuery('#recordList th:contains("Date")').addClass('orderedBy ' + localStorage.order);
    jQuery('#recordList th').click(function() {
        switch (jQuery(this).text()) {
            case 'Date':
                jQuery(this).removeClass('ASC DESC');
                localStorage.order = localStorage.order == 'DESC' ? 'ASC' : 'DESC';
                jQuery(this).addClass(localStorage.order);
                break;
        }
        userTrackAjax.populateClientsList(0);
    });
});

clientList.js

jQuery(function($) {
    var rangeFilter = function(el) {
        el = el || $('body');
        var fromInput = $('input[name=from]', el);
        var toInput = $('input[name=to]', el);

        function shortISO(date) {
            return date.toISOString().substring(0, 10);
        }
        this.setRange = function(start, end) {
            fromInput.val(shortISO(start));
            toInput.val(shortISO(end));
        }
    };
    var range = new rangeFilter($('#rangeFilter'));
    var end = new Date();
    var start = new Date(end);
    start.setMonth(start.getMonth() - 6);
    range.setRange(start, end);
    $('.filter *').on('change', function() {
        userTrackAjax.populateClientsList(0);
    });
    jQuery('#deleteRecords').dblclick(function() {
        if (jQuery('#recordList tr.selected').length !== 0) {
            jQuery('#recordList tr.selected').each(function() {
                userTrackAjax.deleteClient(jQuery(this).attr('data-id'), this);
            });
            return;
        } else {
            alert("No records selected!");
        }
    });
    jQuery('#cleanDatabase').dblclick(function() {
        userTrackAjax.cleanDataForDomain(options.domain);
    });
    jQuery('#deleteZeroRecords').dblclick(function() {
        userTrackAjax.deleteZeroRecords(options.domain);
    });
});


Simple Mouse Mover Recoredr and Player

jsfiddle.net/Szar/4gyqd16u/

HTML

<button id="record">Record</button>
<button id="play">Play</button>

<div class="cursor"></div>

CSS

.cursor {
  border-radius: 50%;
  background: red;
  width: 10px;
  height: 10px;
  position: fixed;
  top: 0;
  left: 0;
}

.click {
  border-radius: 50%;
  background: red;
  position: fixed;
  width: 20px;
  height: 20px;
}

JavaScript

$(function() {
    var move = [];
    $('#record').toggle(function() {
        $(document).mousemove(function(e) {
            move.push({
                x: e.pageX,
                y: e.pageY
            });
        });

    }, function() {
        $(document).off('mousemove');
    });

    $('#play').click(function() {
        var $replay = $('.cursor'),
            pos, i = 0,
            len = move.length,
            t;

        (function anim() {
            pos = move[i];
            $replay.css({
                top: pos.y,
                left: pos.x
            });

            i++;

            if (i === len) {
                clearTimeout(t);
            } else {
                t = setTimeout(anim, 10);
            }
        })()

    });
});

Mouse Movement Ghost

var MouseGhost = new Class({
 
        Implements : [Options],
        points : [],
        tracepoints : [],
        options : {
            delay : 200,
            offset : { x : -20, y : 20 },
            color : '#666',
            size : 20,
            zindex : 20
        },
 
        initialize : function(options){
            this.setOptions(options);
            this.cursor = new Element('div',{
                'styles' : {
                    'position' : 'absolute',
                    'top' : -1000,
                    'left' : -1000,
                    'height' : this.options.size,
                    'width' : this.options.size,
                    'background-color' : this.options.color,
                    'z-index' : this.options.zindex
                }
            }).injectInside(document.body);
 
            window.addEvent('mousemove',this.listener.bindWithEvent(this));
        },
 
        listener : function(event){
            $clear(this.timeout);
            this.points.push($merge(event.page,{t : new Date().getTime()}));
            this.timeout = this.traceback.delay(this.options.delay,this);
        },
 
        traceback : function(){
            this.tracepoints = $A(this.points);
            this.points = [];
            this.animate();
        },
 
        animate : function(){
            var l = this.tracepoints.length;
            if(l){
                var p = this.tracepoints.shift();
                this.cursor.setStyles({
                    'top' : p.y + this.options.offset.y,
                    'left' : p.x + this.options.offset.x
                });
                if(l > 1){
                    var d = this.tracepoints[0].t - p.t;
                    this.animate.delay(d,this);
                }
            }
        }
});

new MouseGhost({delay : 400, color: '#33FF00'});
new MouseGhost({delay : 300, color: '#FF3300', 'offset' : {x: 30, y : -20 }, 'size' : 10});
new MouseGhost({delay: 200, color: '#3300FF', 'offset' : {x : 10, y : 0}, 'size' : 35});