可视化-热力图绘制原理

网上那些炫酷的热力图是如何绘制的? 相信你也很好奇,本文将以 canvas 作为绘图示例来讲解热力图的原理。

透明度

我们经常遇到透明度的概念,如 CSS 中的 opacity 属性、rgba 颜色中的 alpha 变量、canvas 中的 globalAlpha 属性等。

它们的取值范围一般是 0-1 之间,0 表示完全透明,1 表示不透明,值越小,越透明。

透明度叠加

思考一个问题: 透明度为 0.2 的矩形跟透明度为 0.6 的矩形叠加后的透明度为多少?

结果可以看以下示例,通过 canvas 的 getImageData 方法输出了叠加后的透明度(值除以 255 即可)

See the Pen canvas-opacity by linghuam (@linghuam) on CodePen.

很多人的第一感觉也许是 0.8,其实这是一种想当然的理解。正确的思路如下:

假设把透明度理解成玻璃的透光性,这样 alpha=0.2 表示一束光照射到玻璃上,有 20% 的光线被反射回来(这一部分光线会进入你的眼睛),80% 穿透过去,这时我们看到的东西就会很模糊。同理,alpha=0 表示光线全部穿透过去,所以我们什么都看不见,alpha=1 表示光线全部被反射,所以我们能看见全部。

那么 alpha=0.2 和 alpha=0.6 的叠加相当于两块玻璃叠加,第一块玻璃有 80% 光线穿透,
第二块在第一块穿透过的光线中,有 40% 光线穿透,这样穿过两块玻璃后被反射多少光线呢,计算方法如下:1*0.2 + (1-0.2)*0.6 = 0.68, 所以最后的透明度是 0.68 而不是 0.8。

下面一篇文章总结了其计算公式:两个半透明颜色色的叠加计算方法

热力图原理

其实热力图就是根据透明度的大小和叠加来渲染的。

首先我们的数据集是一个对象数组,每个元素包含了 { x, y, value } 属性。我们首先从这一组值中找出 value 最大值 max,然后用 value/max 的值来表示透明度,这样我们可以在画布中绘制不同透明度的小圆圈,起初这些圆圈都是黑白的,所以下一步需要根据不同透明度来进行着色处理。

通过查看 mapv 源码发现,它实现了一个 Intensity 类用来对不同透明度实现一个渐变色。

首先他创建了一个 256 * 1 的 canvas ,然后利用 canvas 的 createLinearGradient 来将渐变色填充进去,这样一个透明度值就可以对应 canvas 上的一个颜色值,通过 getImageData 方法就可以根据透明度来取对应的颜色值。

截取部分核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 创建一个 256*1 的 canvas 并填充渐变色
Intensity.prototype.initPalette = function () {

var gradient = this.gradient;

var canvas = new Canvas(256, 1);

var paletteCtx = this.paletteCtx = canvas.getContext('2d');

var lineGradient = paletteCtx.createLinearGradient(0, 0, 256, 1);

for (var key in gradient) {
lineGradient.addColorStop(parseFloat(key), gradient[key]);
}

paletteCtx.fillStyle = lineGradient;
paletteCtx.fillRect(0, 0, 256, 1);

}

// 通过透明度值取到对应的颜色
Intensity.prototype.getImageData = function (value) {

var imageData = this.paletteCtx.getImageData(0, 0, 256, 1).data;

if (value === undefined) {
return imageData;
}

var max = this.max;
var min = this.min;

if (value > max) {
value = max;
}

if (value < min) {
value = min;
}

var index = Math.floor((value - min) / (max - min) * (256 - 1)) * 4;

return [imageData[index], imageData[index + 1], imageData[index + 2], imageData[index + 3]];
}

当然,为了热力图更好看,作者用了 canvas 的 shadowBlur 来实现一个边缘模糊效果。

至此,热力图的实现原理就介绍完了,下面是我根据这个原理做的一个小 Demo:

See the Pen canvas-heatmap by linghuam (@linghuam) on CodePen.

参考资料