heatmap.js(熱力圖)原始碼解讀

小番茄子發表於2020-04-04

前端閱讀室
heatmap.js (github.com/pa7/heatmap…)可以很方便地繪製熱力圖,官網首頁www.patrick-wied.at/static/heat…) 給出的程式碼示例:

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

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

那麼它是如何實現熱力圖繪製的呢?本文將為你全面解讀heatmap.js原始碼。

熱力圖原理

點模板

點模板對應熱力圖資料點。它是一個圓點,根據可配置的模糊因子(blurFactor,預設.85),可使圓點帶有模糊效果(藉助createRadialGradient)。

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;
};
複製程式碼

灰度(透明度)疊加

這個熱力圖的"靈魂"。rgb通道是無法線性疊加呈現效果的,但是透明度是近似線性的。var templateAlpha = (value-min)/(max-min);,根據資料點的比率,對應於透明度的值alpha,我們在canvas上(shadowCtx)繪製一個資料點。它們的透明度是可以疊加的,值越大,越"不透明"。

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

	}
},
複製程式碼

線性色譜

通過createLinearGradient你可以自主定製自己的熱力圖色譜(config.gradient)。

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;
};
複製程式碼

著色

最後,透明度的疊加值(this.shadowCtx.getImageData)對映到線性色譜(palette),取線性色譜中的顏色為canvas上色(putImageData)就得到最終的熱力圖了。

_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];

},
複製程式碼

其他

以上就是heatmap.js庫最精華的部分了。當然,為了讓庫的設計更完備,它還做了很多其他工作。

Coordinator

自己實現了一個釋出訂閱模式來作為整個類庫功能的排程者。

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;
})();
複製程式碼

如你需要renderpartial、renderall,只需要emit就可以了。

coordinator.on('renderpartial', renderer.renderPartial, renderer);
coordinator.on('renderall', renderer.renderAll, renderer);
複製程式碼

plugin

提供了外掛介面,你可以使用heatmap.js提供的如gmaps-heatmap等各種外掛。

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);
    }
}
複製程式碼

Heatmap

實現了Heatmap構造方法,使使用者可以通過heatmap例項呼叫各種功能。

Heatmap.prototype = {
    addData: function() {
    },
    removeData: function() {
    },
    setData: function() {
    },
    setDataMax: function() {
    },
    setDataMin: function() {
    },
    configure: function(config) {
    },
    repaint: function() {
    },
    getData: function() {
    },
    getDataURL: function() {
    },
    getValueAt: function(point) {
    }
};

return Heatmap;
複製程式碼

Store

實現了自己的Store來統一管理熱力圖資料。

  Store.prototype = {
    // when forceRender = false -> called from setData, omits renderall event
    _organiseData: function(dataPoint, forceRender) {
    },
    _unOrganizeData: function() {
    },
    _onExtremaChange: function() {
    },
    addData: function() {
    },
    setData: function(data) {
    },
    removeData: function() {
      // TODO: implement
    },
    setDataMax: function(max) {
    },
    setDataMin: function(min) {
    },
    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();
    }
  };
複製程式碼

總結

以上,就是heatmap全部原始碼的內容(除各種plugins不一一介紹了),總體實現上其實並不複雜,但確實可以很方便地繪製熱力圖。基於它的原理,我們可以進行二次開發,實現定製的熱力圖。比如,我們一般都是通過xpath來儲存無埋點資料的,而一般xpath元素並不是圓點,大部分時候它都是長方形元素。這時用圓點擬合就不太合適了,我們改造為橢圓點可以更好地擬合實際點選情況。

前端閱讀室

相關文章