/* * heatmap.js v2.0.5 | JavaScript Heatmap Library * * Copyright 2008-2016 Patrick Wied - All rights reserved. * Dual licensed under MIT and Beerware license * * :: 2016-09-05 01:16 */ ;(function (name, context, factory) { // Supports UMD. AMD, CommonJS/Node.js and browser context if (typeof module !== 'undefined' && module.exports) { module.exports = factory() // eslint-disable-next-line no-undef } else if (typeof define === 'function' && define.amd) { // eslint-disable-next-line no-undef 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: 0.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 = 10 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 } var storedVal = store[x][y] if (storedVal > max) { if (!forceRender) { this._max = storedVal } else { this.setDataMax(storedVal) } return false } else if (storedVal < min) { if (!forceRender) { this._min = storedVal } else { this.setDataMin(storedVal) } 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) { // if it's the first datapoint initialize the extremas with it if (this._data.length === 0) { this._min = this._max = organisedEntry.value } 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 < 0.01 ? 0.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 })