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元素並不是圓點,大部分時候它都是長方形元素。這時用圓點擬合就不太合適了,我們改造為橢圓點可以更好地擬合實際點選情況。