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

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});

JavaScript Классный пример оформления многострочных комментариев

/*\
|*|  COOKIE LIBRARY THANKS TO MDN
|*|
|*|  A complete cookies reader/writer framework with full unicode support.
|*|
|*|  Revision #1 - September 4, 2014
|*|
|*|  https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
|*|  https://developer.mozilla.org/User:fusionchess
|*|
|*|  This framework is released under the GNU Public License, version 3 or later.
|*|  http://www.gnu.org/licenses/gpl-3.0-standalone.html
|*|
|*|  Syntaxes:
|*|
|*|  * mr_cookies.setItem(name, value[, end[, path[, domain[, secure]]]])
|*|  * mr_cookies.getItem(name)
|*|  * mr_cookies.removeItem(name[, path[, domain]])
|*|  * mr_cookies.hasItem(name)
|*|  * mr_cookies.keys()
|*|
\*/

var mr_cookies = {
  getItem: function (sKey) {
    if (!sKey) { return null; }
    return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
  },
  setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
    if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
    var sExpires = "";
    if (vEnd) {
      switch (vEnd.constructor) {
        case Number:
          sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
          break;
        case String:
          sExpires = "; expires=" + vEnd;
          break;
        case Date:
          sExpires = "; expires=" + vEnd.toUTCString();
          break;
      }
    }
    document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
    return true;
  },
  removeItem: function (sKey, sPath, sDomain) {
    if (!this.hasItem(sKey)) { return false; }
    document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
    return true;
  },
  hasItem: function (sKey) {
    if (!sKey) { return false; }
    return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
  },
  keys: function () {
    var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
    for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) { aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); }
    return aKeys;
  }
};

/*\
|*|  END COOKIE LIBRARY
\*/

JavaScript code to string converter

github.com/latentflip/deval

Sometimes you're doing interesting things, and you want a block of code as a multiline string.
But doing this is super annoying:

var codeString = [
  "var foo = 'bar'",
  "function stuff () {",
  "  console.log('The thing is \"10\"');"
  "}"
].join('\n');

Quotes everywhere, keeping track of indentation is a pain if you want it properly formatted, no syntax highlighting.

Function code to string makes it look like this:

var codeToString = require('codeToString');

var codeString = codeToString(function () {
    var foo = 'bar';
    function stuff () {
        console.log('The thing is "10"');
    }
});

// codeString -> "var foo = 'bar';\nfunction stuff () {\n    console.log('The thing is \"10\"');\n}"


It even figures out what indentation you meant and cleans that up.

If eval() takes a string representing code, and turns it into actual code, codeToString() takes actual code, and returns a string representation of it.

Basic usage.

Call codeToString() with a function containing the code you want to get back as a string.
The function wrapper will be removed.

var codeToString = require('codeToString');

var codeString = codeToString(function(){
    var foo = 'bar';
    function stuff () {
        console.log('The thing is "10"');
    }
});

// codeString will be:
//    "var foo = 'bar';
//    function stuff () {
//        console.log('The thing is \"10\"');
//    }"

Advanced usage.

Sometimes you want to interpolate strings / numbers / etc into your generated code.
You can't just use normal scoping rules, because this code won't be executed in the current scope.
So instead you can do a little templating magic.

To interpolate:

- Name some positional arguments in the function you pass to codeToString: codeToString(function (arg1, arg2) { ...
- Insert them where you want them in your code by wrapping in dollars: $arg1$
- Pass the values of those arguments as additional arguments to codeToString itself. codeToString(function (arg1, arg2) { ... }, "one", 2)

var codeString = codeToString(function (foo, bar) {
    var thing = $bar$;
    console.log('$foo$');
    console.log(thing);
}, "hi", 5);

// codeString will be:
//    "var thing = 5;
//    console.log('hi');
//    console.log(thing)"

Don't try to be too clever with this, and if you're passing strings, you'll want to wrap them in quotes inside the code block, as shown about for "hi" -> '$foo$'

Source Code of codeToString():

var min = function (arr) {return Math.min.apply(Math, arr);};

var REGEXES = {
    functionOpening: /^function\s*\((.*)\)[^{]{/
};


module.exports = function (fn/*, interpolateArgs... */) {
    var str = fn.toString();
    var interpolateArgs = Array.prototype.slice.call(arguments, 1);
    var argNames;
    if (interpolateArgs.length) {argNames = getArgumentNames(str);}
    str = removeFunctionWrapper(str);
    str = dedent(str);
    if (argNames && argNames.length) {str = interpolate(str, argNames, interpolateArgs);}
    return str;
};

function getArgumentNames (str) {
    var argStr = str.match(REGEXES.functionOpening);
    return argStr[1].split(',').map(function (s) { return s.trim(); });
}

function removeFunctionWrapper (str) {
    var closingBraceIdx, finalNewlineIdx, lastLine;

    // remove opening function bit
    str = str.replace(REGEXES.functionOpening, '');

    // remove closing function brace
    closingBraceIdx = str.lastIndexOf('}');
    if (closingBraceIdx > 0) {str = str.slice(0, closingBraceIdx - 1);}

    // If there was no code on opening wrapper line, remove it
    str = str.replace(/^[^\S\n]*\n/, '');

    // If there was no code on final line, remove it
    finalNewlineIdx = str.lastIndexOf('\n');
    lastLine = str.slice(finalNewlineIdx);
    if (lastLine.trim() === '') str = str.slice(0, finalNewlineIdx);

    return str;
}

// Reset indent on the code to minimum possible
function dedent (str) {
    var lines = str.split('\n');
    var indent = min(lines.map(function (line) {return line.match(/^\s*/)[0].length;}));
    lines = lines.map(function (line) {return line.slice(indent);});
    return lines.join('\n');
}

function interpolate (str, argNames, args) {
    argNames.forEach(function (name, i) {
        var regex = new RegExp('\\$' + name + '\\$', 'g');
        str = str.replace(regex, args[i]);
    });
    return str;
}

Tests:

var test = require('tape');
var codeToString = require('./codeToString');

test('it serializes multiline code', function (t) {
    var serialized = codeToString(function () {
        console.log('hi');
        console.log('there');
    });

    var expected = [
        "console.log('hi');",
        "console.log('there');"
    ].join('\n');

    t.equal(serialized, expected);
    t.end();
});

test('it serializes inline code', function (t) {
    var serialized = codeToString(function () { console.log('hi'); console.log('there'); });

    var expected = [
        "console.log('hi'); console.log('there');"
    ].join('\n');

    t.equal(serialized, expected);
    t.end();
});

test('it even interpolates things', function (t) {
    var serialized = codeToString(function (foo, bar) {
        console.log('$foo$');
        console.log($bar$);
    }, "hi", 5);

    var expected = [
        "console.log('hi');",
        "console.log(5);"
    ].join('\n');

    t.equal(serialized, expected);
    t.end();
});

JavaScript Code Execution Visualization

latentflip.com/loupe
github.com/latentflip/loupe
pythontutor.com/javascript.html#mode=edit

JavaScript Turing Machine

All it needs are some instructions, the initial state of the tape as a list, an end state and a start state. It will return either the final tape state or false if the end state is never reached. ‘B’ is considered a blank state and the tape behaves as infinite in both directions.

function tm (I, tape, end, state, i, cell, current) {
    i = 0;
    while (state != end) {
        cell = tape[i];
        current = (cell) ? I[state][cell] : I[state].B;
        if (!current) {return false;}
        tape.splice(i, 1, current.w);
        i += current.m;
        state = current.n;
    }
    return tape;
}

// For testing purposes, run in Node.js as command:
// node turing-140.js machine-140.json 111 q5
// Instructions tape endstate.

console.log(
    tm(
           JSON.parse(
               require('fs').readFileSync(process.argv[2], 'utf-8')
           )
         , process.argv[3].split("")
         , process.argv[4]
         , "q0"
    ).join("")
);

For testing use a simple multiply program that basically turns 111 into 1110111. So this is what the algorithm’s implementation ends up looking like

{
    "q0": {"1": {"w": "B", "m": 1, "n": "q1"}},
    "q1": {"1": {"w": "1", "m": 1, "n": "q1"},
        "0": {"w": "0", "m": 1, "n": "q2"},
        "B": {"w": "0", "m": 1, "n": "q2"}},
    "q2": {"1": {"w": "1", "m": 1, "n": "q2"},
        "B": {"w": "1", "m": -1, "n": "q3"}},
    "q3": {"1": {"w": "1", "m": -1, "n": "q3"},
        "0": {"w": "0", "m": -1, "n": "q3"},
        "B": {"w": "1", "m": 1, "n": "q4"}},
    "q4": {"1": {"w": "B", "m": 1, "n": "q1"},
        "0": {"w": "0", "m": 1, "n": "q5"}}
}

There’s a smaller solution of Turing Machine in a less verbose language:

function(a,b,c,d,e){for(e=0;d<c;)with(a[d][b[e]||"B"])b[e]=w,e+=m,d=n;return b}

пятница, 1 апреля 2016 г.

JavaScript Observer, Iterator, Generator, Promise and Observable Patterns

Итератор перебирает подряд набор значений и по очереди выдает элементы iterator.next(), как array.shift()
Генератор по очереди принимает элементы generator.next(1), как array.push(1)
Промис по очереди выполняет цепочку зависимых друг от друга асинхронных событий
Обозреватель добавляет к объекту к объекту функцию события и вызывает её, когда это событие генерируется в коде.
Обозреваемое добавляет к последовательности элементов или асинхронных событий итератор, который по порядку перебирает элементы и обозреватель, который вызывает для перебираемых элементов функции события.
Декоратор обертывает одну функцию в другую функцию decorate(base, wrapper).
Фабрика конструирует объекты по заданным параметрам.
Фасад скрывает сложную функциональность, выдавая вместо неё простую функцию.
Прокси оборачивает функцию в декоратор.

Observer Pattern

// Observer

function Observer () {
    this.events = {};
}

Observer.prototype = {
      addEvent: function (name, func) {if (this.events[name]) {this.events[name].push(func);} else {this.events[name] = [func];}}
    , removeEvent: function (name) {if (this.events[name]) {delete this.events[name];}}
    , dispatchEvent: function (name, args) {if (this.events[name]) {this.events[name].forEach(function (func) {func.apply(null, args);});}}
};

// Observer Test

var observer = new Observer();
observer.addEvent('one', function (a) {console.log('1: ' + a);});
observer.addEvent('one', function (b) {console.log('2: ' + b);});
observer.addEvent('two', function (c) {console.log('3: ' + c);});
observer.dispatchEvent('one', ['one']);
observer.dispatchEvent('two', ['two']);
observer.removeEvent('one');
observer.dispatchEvent('one', ['one']);

Iterator Pattern

// Iterator

function Iterator (items) {
    var i = 0;
    return {
        next: function () {
            var done = (i >= items.length)
                , value = !done ? items[i++] : undefined;
            return {value: value, done: done};
        }
    };
}

// Iterator Test

var iterator = new Iterator([1, 2, 3]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

Generator Pattern

// Generator

function Generator (items) {
    var i = 0;
    return {
        next: function (value) {
            items = items.slice();
            var done = (i >= items.length)
            if (!done) {items[i] = value; i++;}
            return {value: items, done: done};
        }
    };
}

// Generator Test

var generator = new Generator(new Array(3));
console.log(generator.next(1)); // {value: [1, undefined, undefined], done: false}
console.log(generator.next(2)); // {value: [1, 2, undefined], done: false}
console.log(generator.next(3)); // {value: [1, 2, 3], done: false}
console.log(generator.next(4)); // {value: [1, 2, 3], done: true}

Promise Pattern

// Promise

function Promise () {
    this.promises = [];
}

Promise.prototype = {
      then: function (success, error) {
        if (Object.prototype.toString.call(success) !== '[object Function]') {success = function () {};}
        if (Object.prototype.toString.call(error) !== '[object Function]') {error = function () {};}
        this.promises.push({success: success, error: error});
        return this;
      }
    , success: function () {
        if (this.promises.length) {
            var args = Array.prototype.slice.call(arguments);
            args.unshift(this);
            this.promises.shift().success.apply(null, args);
        }
      }
    , error: function () {
        if (this.promises.length) {
            this.promises.shift().error.apply(null, arguments);
        }
      }
};

// Promise Test

function timeout1 (seconds, error) {
    var promise = new Promise();
    setTimeout(function () {
        if (seconds > 2) {
            promise.error(seconds);
        } else {
            console.log('Success 1: ' + seconds);
            promise.success(seconds);
        }
    }, seconds * 1000);
    return promise;
}

function timeout2 (promise, seconds) {
    setTimeout(function () {
        if (seconds > 2) {
            promise.error(seconds);
        } else {
            console.log('Success 2: ' + seconds);
            promise.success(seconds);
        }
    }, seconds * 1000);
    return promise;
}

function timeout3 (promise, seconds) {
    setTimeout(function () {
        if (seconds > 1) {
            promise.error(seconds);
        } else {
            console.log('Success 3: ' + seconds);
            promise.success(seconds);
        }
    }, seconds * 1000);
    return promise;
}

function done (promise, seconds) {
    console.log('Done: ' + seconds);
}


function timeout1error (seconds) {console.log('Error 1 seconds: ' + seconds);}
function timeout2error (seconds) {console.log('Error 2 seconds: ' + seconds);}
function timeout3error (seconds) {console.log('Error 3 seconds: ' + seconds);}

timeout1(2).then(timeout2, timeout1error).then(timeout3, timeout2error).then(done, timeout3error);

Observable Pattern = Interator + Promise for async + Observer

Перебираем с помощью итератора все элементы массива или цепочки промисов и для каждого из них тригерим подставленные функции события (успешно, неуспешно, завершено).

// Observable

var Observable = {
    from: function (items) {
        var iterator = new Iterator(items);
        return {subscribe: function (next, error, complete) {
            var item = iterator.next();
            while (!item.done) {
                next(item.value);
                item = iterator.next();
            }
            complete();
        }};
    }
};

// Observable Test

Observable.from(['Adria', 'Jen', 'Sergi']).subscribe(
      function onNext (value) {console.log('Next: ' + value);}
    , function onError (error) {console.log('Error: ' + error);}
    , function onCompleted () {console.log('Completed');}
);