「AntV」热力图 heatmap 绘制原理解析
最近在 G2 5.0 中加上了 heatmap 热力图的 mark,让 G2 5.0 可以轻松绘制热力图,应该社区上大部分绘制热力图的库,原理基本都是类似的,可以参考 heatmap.js 源码,所以这边文档就是对于 heatmap.js 的源码解读。
什么是热力图
上面是一些热力图的截图效果,一般用红色来代表这个区域的数据更大,更密集。再举一个实际的例子:
在支付宝或者其他网站中,进场会有些热力分析,通过大量用户的交互情况,在 APP 或者网站页面上,显示出用户交互的热力图,从而可以分析出用户的操作习惯,从而对于产品功能的设计提供一些帮助和指导。
所以,热力图怎么绘制?需要让数据更大的地方显示的颜色更红,数据更小的地方,数据更绿。
绘制原理
以上面的这个案例,来介绍整个绘图过程的。原始的数据结构为:
[
{
"g": 541,
"l": 85,
"tmp": 858
},
{
"g": 937,
"l": 465,
"tmp": 299
},
{
"g": 566,
"l": 131,
"tmp": 326
},
// ...
]
这里将 heatmap.js 源码,它的绘制过程主要分成为几个步骤:
1. 使用散点展示数据分布
function getPoint(x: number, y: number, radius: number) {
const tplCanvas = document.createElement('canvas');
const tplCtx = tplCanvas.getContext('2d');
tplCanvas.width = radius * 2;
tplCanvas.height = radius * 2;
const x = radius;
const y = radius;
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
tplCtx.fillStyle = 'rgba(0,0,0,1)';
tplCtx.fill();
return tplCanvas;
}
2. 追加透明度渐变
因为最终效果是一种模糊效果,所以需要使用圆弧渐变 createRadialGradient
。
const 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);
这样画出来的一系列的渐变透明的点,每个点代表着一个数据,每个点重叠在一起,让透明度包含了数据密度信息,只不过整体的是黑白的,所需要对数据进行指定色板的着色。
3. 按照指定的渐变色,生成色板
/**
* Get a color palette with len = 256 base on gradient.
* @param gradientConfig
* @returns
*/
function getColorPalette(gradientConfig: HeatmapGradient, createCanvas) {
const paletteCanvas = document.createElement('canvas');
const paletteCtx = paletteCanvas.getContext('2d');
const gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
parseGradient(gradientConfig).forEach(([r, c]) => {
gradient.addColorStop(r, c);
});
paletteCtx.fillStyle = gradient;
paletteCtx.fillRect(0, 0, 256, 1);
return paletteCtx.getImageData(0, 0, 256, 1).data;
}
本质很简单,就是用 createLinearGradient
方法讲一个指定的色板生成为色板 rgba 数据。函数入参为以下,也是 G2 默认的色板:
[ [0.25, 'rgb(0,0,255)'],
[0.55, 'rgb(0,255,0)'],
[0.85, 'yellow'],
[1.0, 'rgb(255,0,0)'],
]
4. 着色,让数据透明度对应到颜色
着色逻辑就很简单了,将第三步生成的色板在第二部生成的散点图中,按照像素点、透明度数据,一一对应即可。
const img = shadowCtx.getImageData(x, y, width, height);
const imgData = img.data;
for (let i = 3; i < imgData.length; i += 4) {
const alpha = imgData[i];
const offset = alpha * 4;
imgData[i - 3] = palette[offset];
imgData[i - 2] = palette[offset + 1];
imgData[i - 1] = palette[offset + 2];
}
因为,最后热力图通过透明度迭代,最终其实使用透明度信息来表达数据密度的,而数据密度是通过 256*1 的色板决定的,所以讲不同的透明度数据映射到色板上。
整体代码可以看 src/shape/heatmap/renderer/index.ts。
在 G2 中封装
以上是 heatmap,js 的源码解读,那怎么融入到 G2 的 API 设计中了?首先我们预定在 G2 中使用的 API 风格和方式。
API 设计
chart
.heatmap()
.data({
type: 'fetch',
value: 'https://assets.antv.antgroup.com/g2/heatmap.json',
})
.encode('x', 'g')
.encode('y', 'l')
.encode('color', 'tmp')
.encode('size', 20)
.style('opacity', 0)
.tooltip(false);
需要指定 x、y、color 通道和数据绑定,以便绘制散点的位置、以及密度数据字段,指定 size 通道设置绘制点的半径,默认为 40。
heatmap mark 处理数据
因为 heatmap 最终的渲染直接使用 heatmap.js,所以需要在 mark 中处理好坐标信息,密度字段、圆半径即可。
- x
- y
- value
- radius
export const Heatmap: MC<HeatmapOptions> = (options) => {
return (index, scale, value, coordinate) => {
const { x: X, y: Y, size: S, color: C } = value;
const P = Array.from(index, (i) => {
// Default size = 40.
const r = S ? +S[i] : 40;
// Warning: x, y, value, radius.
return [...coordinate.map([+X[i], +Y[i]]), C[i], r] as unknown as Vector2;
});
return [[0], [P]];
};
};
返回以四个信息作为一个数组单元,组成数组,用来绘制一个 heatmap 图形。
heatmap shape 绘制图形
接受 mark 中的 Array<[x, y, value, radius]> 绘制热力图,这部分就很简单的一个数据适配器,可以直接看代码。最后追加一个文档,DEMO 等即可。
效果
最后来一个 G2 热力图案例吧。获取鼠标在画布交互的的频率,绘制出画布的鼠标交互热力图。
总结
heatmap 对于 G2 来说,其实更像是一个图表分类的图表,而不是基于图形语法,只不过套在一个看似图形语法的 API 壳子中。但是这些图表绘制能力对于 G2 来说有不能不具备,类似的还有:
- 韦恩图
- 仪表盘
- 水波图
- ...
在添加这些能力的过程中,其实也是对可视化底层渲染能力的学习和复习,了解他怎么实现才能更好的使用他。
转载自:https://juejin.cn/post/7239045873903829050