Initial commit
diff --git a/README.markdown b/README.markdown
new file mode 100644
index 0000000..457ee4a
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,36 @@
+HeatmapJS
+=========
+
+Node port of excellent heatmap.js library
+Info and docs can be found on the [original github page](https://github.com/pa7/heatmap.js)
+
+
+Example
+=======
+
+````javascript
+var heatmap = require('heatmaps').Heatmap;
+
+var h = heatmap({
+ radius: 15,
+ width: 500,
+ height: 500
+});
+
+h.store.setDataSet({
+ max: 1000,
+ // Originaly data was an array of objects {x:, y:, count:}
+ // in my port it is an array of arrays [x, y, count]
+ data: [[0, 0, 500], [250, 250, 1000], [500, 500, 500]]
+});
+
+// Get buffer which you could use as a request response or write it to a file
+image = h.getImageBuffer()
+````
+
+Install
+=======
+
+With [npm](http://npmjs.org), just do:
+
+ npm install heatmaps
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..1d89516
--- /dev/null
+++ b/index.js
@@ -0,0 +1,409 @@
+/*
+ * Node port of heatmap.js
+ * Arek Mytych (amytych@gmail.com)
+ *
+ * Original release info:
+ * heatmap.js 1.0 - JavaScript Heatmap Library
+ *
+ * Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at)
+ * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
+ */
+
+var Canvas = require('canvas');
+
+function createCanvas(width, height) {
+ if (typeof Canvas !== 'undefined') {
+ return new Canvas(width, height);
+ }
+}
+
+module.exports.Heatmap = function (config) {
+ return new Heatmap(config);
+};
+
+// store object constructor
+// a heatmap contains a store
+// the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added
+var Store = function (hmap) {
+ var _ = {
+ // data is a two dimensional array
+ // a datapoint gets saved as data[point-x-value][point-y-value]
+ // the value at [point-x-value][point-y-value] is the occurrence of the datapoint
+ data: [],
+ // tight coupling of the heatmap object
+ heatmap: hmap
+ };
+
+ // the max occurrence - the heatmaps radial gradient alpha transition is based on it
+ this.max = 1;
+
+ this.get = function (key) {
+ return _[key];
+ };
+
+ this.set = function (key, value) {
+ _[key] = value;
+ };
+};
+
+ // function for adding datapoints to the store
+ // datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence
+Store.prototype.addDataPoint = function (x, y) {
+ if (x < 0 || y < 0) {
+ return;
+ }
+
+ var me = this,
+ heatmap = me.get('heatmap'),
+ data = me.get('data');
+
+ if (!data[x]) {
+ data[x] = [];
+ }
+
+ if (!data[x][y]) {
+ data[x][y] = 0;
+ }
+
+ // if count parameter is set increment by count otherwise by 1
+ data[x][y] += (arguments.length < 3) ? 1 : arguments[2];
+
+ me.set('data', data);
+ // do we have a new maximum?
+ if (me.max < data[x][y]) {
+ // max changed, we need to redraw all existing(lower) datapoints
+ heatmap.get('actx').clearRect(0, 0, heatmap.get('width'), heatmap.get('height'));
+ me.setDataSet({ max: data[x][y], data: data }, true);
+ return;
+ }
+ heatmap.drawAlpha(x, y, data[x][y], true);
+};
+
+Store.prototype.setDataSet = function (obj, internal) {
+ var me = this,
+ heatmap = me.get('heatmap'),
+ data = [],
+ d = obj.data,
+ dlen = d.length;
+
+ // clear the heatmap before the data set gets drawn
+ heatmap.clear();
+ this.max = obj.max;
+
+ if (internal != null && internal) {
+ for (var one in d) {
+ // jump over undefined indexes
+ if (one === undefined) {
+ continue;
+ }
+ for (var two in d[one]) {
+ if (two === undefined) {
+ continue;
+ }
+ // if both indexes are defined, push the values into the array
+ heatmap.drawAlpha(one, two, d[one][two], false);
+ }
+ }
+ } else {
+ while (dlen--) {
+ var point = d[dlen];
+ heatmap.drawAlpha(point[0], point[1], point[2], false);
+ if (!data[point[0]]) {
+ data[point[0]] = [];
+ }
+
+ if (!data[point[0]][point[1]]) {
+ data[point[0]][point[1]] = 0;
+ }
+
+ data[point[0]][point[1]] = point[2];
+ }
+ }
+ heatmap.colorize();
+ this.set('data', d);
+};
+
+
+var Heatmap = function (config) {
+ // private variables
+ var _ = {
+ radius : 40,
+ canvas : {},
+ acanvas: {},
+ ctx : {},
+ actx : {},
+ legend: null,
+ visible : true,
+ width : 0,
+ height : 0,
+ max : false,
+ gradient : false,
+ opacity: 180,
+ premultiplyAlpha: false,
+ bounds: {
+ l: 1000,
+ r: 0,
+ t: 1000,
+ b: 0
+ },
+ debug: false
+ };
+
+ // heatmap store containing the datapoints and information about the maximum
+ // accessible via instance.store
+ this.store = new Store(this);
+
+ this.get = function (key) {
+ return _[key];
+ };
+
+ this.set = function (key, value) {
+ _[key] = value;
+ };
+
+ // configure the heatmap when an instance gets created
+ this.configure(config);
+ // and initialize it
+ this.init();
+};
+
+Heatmap.prototype.configure = function (config) {
+ var me = this;
+
+ me.set('radius', config.radius || 40);
+ me.set('visible', (config.visible !== null) ? config.visible : true);
+ me.set('max', config.max || false);
+ // default is the common blue to red gradient
+ me.set('gradient', config.gradient || {0.55: 'rgb(0,0,255)', 0.65: 'rgb(0,255,255)', 0.75: 'rgb(0,255,0)', 0.95: 'yellow', 1.0: 'rgb(255,0,0)'});
+ me.set('opacity', parseInt(255 / (100 / config.opacity), 10) || 180);
+ me.set('width', config.width || 0);
+ me.set('height', config.height || 0);
+ me.set('debug', config.debug);
+};
+
+Heatmap.prototype.resize = function () {
+ var me = this,
+ canvas = me.get('canvas'),
+ acanvas = me.get('acanvas');
+
+ canvas.width = acanvas.width = me.get('width');
+ this.set('width', canvas.width);
+ canvas.height = acanvas.height = me.get('height');
+ this.set('height', canvas.height);
+};
+
+Heatmap.prototype.init = function () {
+ var me = this,
+ canvas = createCanvas(me.get('width'), me.get('height')),
+ acanvas = createCanvas(me.get('width'), me.get('height')),
+ ctx = canvas.getContext('2d'),
+ actx = acanvas.getContext('2d');
+
+ me.initColorPalette();
+
+ me.set('canvas', canvas);
+ me.set('ctx', ctx);
+ me.set('acanvas', acanvas);
+ me.set('actx', actx);
+
+ me.resize();
+
+ actx.shadowOffsetX = 15000;
+ actx.shadowOffsetY = 15000;
+ actx.shadowBlur = 15;
+};
+
+Heatmap.prototype.initColorPalette = function () {
+ var me = this,
+ canvas = createCanvas(1, 256),
+ gradient = me.get('gradient'),
+ ctx, grad, testData, imageData;
+
+ ctx = canvas.getContext('2d');
+ grad = ctx.createLinearGradient(0, 0, 1, 256);
+
+ // Test how the browser renders alpha by setting a partially transparent pixel
+ // and reading the result. A good browser will return a value reasonably close
+ // to what was set. Some browsers (e.g. on Android) will return a ridiculously wrong value.
+ testData = ctx.getImageData(0, 0, 1, 1);
+ testData.data[0] = testData.data[3] = 64; // 25% red & alpha
+ testData.data[1] = testData.data[2] = 0; // 0% blue & green
+ ctx.putImageData(testData, 0, 0);
+ testData = ctx.getImageData(0, 0, 1, 1);
+ me.set('premultiplyAlpha', (testData.data[0] < 60 || testData.data[0] > 70));
+
+ for (var x in gradient) {
+ grad.addColorStop(parseFloat(x), gradient[x]);
+ }
+
+ ctx.fillStyle = grad;
+ ctx.fillRect(0, 0, 1, 256);
+
+ imageData = ctx.getImageData(0, 0, 1, 256).data;
+ me.set('gradient', imageData);
+};
+
+Heatmap.prototype.colorize = function (x, y) {
+ // get the private variables
+ var me = this,
+ width = me.get('width'),
+ radius = me.get('radius'),
+ height = me.get('height'),
+ actx = me.get('actx'),
+ ctx = me.get('ctx'),
+ x2 = radius * 3,
+ premultiplyAlpha = me.get('premultiplyAlpha'),
+ palette = me.get('gradient'),
+ opacity = me.get('opacity'),
+ bounds = me.get('bounds'),
+ left, top, bottom, right,
+ image, imageData, length, alpha, offset, finalAlpha;
+
+ if (x != null && y != null) {
+ if (x + x2 > width) {
+ x = width - x2;
+ }
+ if (x < 0) {
+ x = 0;
+ }
+ if (y < 0) {
+ y = 0;
+ }
+ if (y + x2 > height) {
+ y = height - x2;
+ }
+ left = x;
+ top = y;
+ right = x + x2;
+ bottom = y + x2;
+
+ } else {
+ if (bounds.l < 0) {
+ left = 0;
+ } else {
+ left = bounds.l;
+ }
+ if (bounds.r > width) {
+ right = width;
+ } else {
+ right = bounds.r;
+ }
+ if (bounds.t < 0) {
+ top = 0;
+ } else {
+ top = bounds.t;
+ }
+ if (bounds.b > height) {
+ bottom = height;
+ } else {
+ bottom = bounds.b;
+ }
+ }
+
+ image = actx.getImageData(left, top, right - left, bottom - top);
+ imageData = image.data;
+ length = imageData.length;
+
+ // loop thru the area
+ for (var i = 3; i < length; i += 4) {
+
+ // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
+ alpha = imageData[i];
+ offset = alpha * 4;
+
+ if (!offset) {
+ continue;
+ }
+
+ // we ve started with i=3
+ // set the new r, g and b values
+ finalAlpha = (alpha < opacity) ? alpha : opacity;
+ imageData[i - 3] = palette[offset];
+ imageData[i - 2] = palette[offset + 1];
+ imageData[i - 1] = palette[offset + 2];
+
+ if (premultiplyAlpha) {
+ // To fix browsers that premultiply incorrectly, we'll pass in a value scaled
+ // appropriately so when the multiplication happens the correct value will result.
+ imageData[i - 3] /= 255 / finalAlpha;
+ imageData[i - 2] /= 255 / finalAlpha;
+ imageData[i - 1] /= 255 / finalAlpha;
+ }
+
+ // we want the heatmap to have a gradient from transparent to the colors
+ // as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value
+ imageData[i] = finalAlpha;
+ }
+ // the rgb data manipulation didn't affect the ImageData object(defined on the top)
+ // after the manipulation process we have to set the manipulated data to the ImageData object
+ image.data = imageData;
+ ctx.putImageData(image, left, top);
+};
+
+Heatmap.prototype.drawAlpha = function (x, y, count, colorize) {
+ // storing the variables because they will be often used
+ var me = this,
+ radius = me.get('radius'),
+ ctx = me.get('actx'),
+ bounds = me.get('bounds'),
+ xb = x - (1.5 * radius) >> 0,
+ yb = y - (1.5 * radius) >> 0,
+ xc = x + (1.5 * radius) >> 0,
+ yc = y + (1.5 * radius) >> 0,
+ g, a;
+
+ // Shadows where used in original implementation. See:
+ // https://github.com/pa7/heatmap.js/blob/master/src/heatmap.js#L559-L568
+ // Unfortunately they produced weird results in node's canvas.
+ // That's why I used radial gradients instead.
+ g = ctx.createRadialGradient(x, y, 0, x, y, radius);
+ a = count ? count / me.store.max : 0.1;
+
+ g.addColorStop(0, 'rgba(0,0,0,' + a + ')');
+ g.addColorStop(1, 'rgba(0,0,0,0)');
+
+ ctx.fillStyle = g;
+ ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
+
+ if (colorize) {
+ // finally colorize the area
+ me.colorize(xb, yb);
+ } else {
+ // or update the boundaries for the area that then should be colorized
+ if (xb < bounds.l) {
+ bounds.l = xb;
+ }
+ if (yb < bounds.t) {
+ bounds.t = yb;
+ }
+ if (xc > bounds.r) {
+ bounds.r = xc;
+ }
+ if (yc > bounds.b) {
+ bounds.b = yc;
+ }
+ }
+};
+
+// dataURL export
+Heatmap.prototype.getImageData = function () {
+ return this.get('canvas').toDataURL();
+};
+
+// Export image to buffer
+Heatmap.prototype.getImageBuffer = function () {
+ return this.get('canvas').toBuffer();
+};
+
+Heatmap.prototype.clear = function () {
+ var me = this,
+ w = me.get('width'),
+ h = me.get('height');
+
+ me.store.set('data', []);
+ // @TODO: reset stores max to 1
+ //me.store.max = 1;
+ me.get('ctx').clearRect(0, 0, w, h);
+ me.get('actx').clearRect(0, 0, w, h);
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8359eaa
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "heatmaps",
+ "version": "0.0.1",
+ "description": "Library for node canvas based heatmaps",
+ "main": "index.js",
+ "directories": {
+ "example": "example"
+ },
+ "dependencies": {
+ "canvas": "1.0.x"
+ },
+ "devDependencies": {},
+ "scripts": {},
+ "repository": {
+ "type": "git",
+ "url": "http://github.com/amytych/node-heatmaps.git"
+ },
+ "keywords": [
+ "heatmap",
+ "canvas",
+ "node"
+ ],
+ "author": "Arek Mytych <amytych@gmail.com>",
+ "license": "MIT/Beerware",
+ "engine": {
+ "node": ">=0.4"
+ },
+ "bugs": {
+ "url": "https://github.com/amytych/node-heatmaps/issues"
+ },
+ "_from": "heatmaps@0.0.1",
+ "homepage": "https://github.com/amytych/node-heatmaps"
+}