Heatmap.js
是基于canvas
开源的热力图框架,使用该框架可以方便的实现热力图,其效果图如下所示:

最近正好在学习
canvas
,顺便研究了一下Heatmap
的源码,Heatmap
的代码还是比较好理解的,代码结构清晰,有很多值得学习的,比如代码的架构,热力图渲染原理、着色等。下面来说下具体的实现过程。讲的不是很清楚,可以看下精简版的代码实现。
架构
Heatmap
其架构主要由两部分组成,分别是Renderer
和Store
。
Renderer
:渲染器,主要用于热力图画布的创建,绘制、着色等。
Store
:数据管理器,主要用于管理热力图的数据,包括添加数据、更新数据、删除数据、组装渲染器需要的数据格式等。
Heatmap对象
Heatmap
对象主要用于构造一个外部调用的对象,将公共方法写在原型对象中,在构造函数中,创建对象的时候,初始化了一个_renderer
对象和_store
对象,分别用于渲染热力和管理数据。
// 核心代码,创建heatmap对象
var heatmapFactory = {
create: function(config) {
return new Heatmap(config);
},
register: function(pluginKey, plugin) {
HeatmapConfig.plugins[pluginKey] = plugin;
}
};
return heatmapFactory;
//默认属性
var HeatmapConfig = {
defaultRadius: 40,//半径
defaultRenderer: 'canvas2d',//默认为2D画布
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',//x坐标字段名
defaultYField: 'y',//主坐标字段名
defaultValueField: 'value', //热度值字段名
plugins: {}//插件
};
数据管理器
Store
是一个数据管理器,用于管理热力图所需要的数据。主要方法如下所示:
_organiseData
用于构造渲染器所需要的数据格式,主要用于查找出热力值的最大值和最小值,通过这两个值来决定各热力图的颜色渲染比例。其构造的每个对象如下所示:
{
x: x,
y: y,
value: value,
radius: radius,
min: min,
max: max
}
_unOrganizeData
:用于还原_organiseData
组装的数据,方便外部获取。其对象格式如下所示:
{
x: x,
y: y,
radius: radi[x][y],
value: data[x][y]
}
_onExtremaChange
:min
或者max
有变化时通知_coordinator
执行extremachange
方法进行重新渲染。
addData
:动态添加数据进行渲染。
setDataMax
:设置最大值。
setDataMin
:设置最小值。
getData
:获取原始数据。
渲染器
Renderer
是整个框架中的核心代码,通过该渲染器可以完美的创建出热力图。通过Canvas2dRenderer
来创建画布和创建调色板等。
调色板
调色板是通过使用defaultGradient
设置的颜色来创建一个256*1的画布,使用createLinearGradient
创建渐变的图像,最后将返回一个Uint8ClampedArray类型的图片数据,该是一个包含RGBA像素信息的Uint8ClampedArray,数组中所有的值都是整数,范围是0~255。
//获取颜色调色板
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);//绘制一个宽为256,高为1的矩形
//返回一个Uint8ClampedArray类型的图片数据
return paletteCtx.getImageData(0, 0, 256, 1).data;
};
绘制热力图
热力图的绘制是使用_drawAlpha
方法来完成的,通过获取到数据的x坐标和y坐标、半径等值来生成一个canvas,然后再更新渲染边界。
//绘制热点图
_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;
// 如果point的值大于最大值,选用max作为绘制的值
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];
}
// 设置透明度
var templateAlpha = (value-min)/(max-min);
// 小于0.01的图片将无法显示
shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;
//绘制图片
shadowCtx.drawImage(tpl, rectX, rectY);
// 更新渲染边界
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
来完成后,其核心原理是获取画布的RGBA像素信息,再使用调色板的颜色对画布上的RGBA像素信息进行修改,就可以将热力图渲染出不同的颜色了。
_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;
}
debugger
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];
},
以上只是对代码的初步了解,要想完全了解期思想和原理,还需要花更多的时间来进行研究,至少要自己能快速的模仿一个出来才算真正的吃透了。
个人博客
网友评论