說明
今天早上看了下heatMap.js的原始碼,瞭解了他是如何繪製熱力圖的,這裡我們拋開其資料處理的部分,聚焦熱力圖的繪製。
如果要繪製一個點的熱力圖,可以簡單是的使用createRadialGradient
來實現,但是如果兩個點的熱力圖發生了重疊,重疊部分當然不是簡單的覆蓋。這種情況下我們當然可以使用畫素級的操作,結合兩個點的熱力圖通過複雜的計算得到覆蓋之後的熱力圖,但顯然過於複雜。
我們仔細觀察下熱力圖,他其實就是一些顏色的漸變產生的效果,中間部分顏色深一點,外圍淺一點,我們實際上就是根據權重的大小來著色。比如我們在[80, 80]的地方有一個點,像半徑10的周圍輻射,我們把重心的權重設為100,最外圍設為10,我們很容易想到,使用一個單色繪製。最方便就是使用灰色,只需要使用透明度就可,其畫素點的rgb值都是0,這樣的資料就方便處理,如下圖。
所以步驟就是先使用這種灰度先繪製到一個canvas
上,其每一個點的rgba都是(0, 0, 0, 0)
到 (0, 0, 0, 255)
之間。現在就可以根據其alpha值將其著色。現在有一個漸變色卡如下,其對應關係就是alpha的值為0,對應色卡的左邊,255對應右邊。
一種簡單的方式就是使用漸變色繪製一個寬為256的canvas,取得這256個點的顏色,然後與canvas進行一一對應。比如,我們的主canvas中某個畫素點的alpha值為100,那麼就將該店的顏色修改為色卡中第100(程式設計師計數)個點的顏色。
具體實現過程如下:
- 一個離屏canvas,繪製一個黑色(rgb都是0,方便處理)的alpha通道的圓
- 將一個個點通過這個離屏canvas繪製到主canvas上
- 繪製一個寬256高1的離屏canvas,講漸變色繪製到這個canvas上面,得到取色卡
- 通過
getImageData
方法獲取畫布資料,並通過資料中的alpha存在值的資料獲取取色卡中的對應的rgb,填入相應的rgb - 最後將畫布資料填充到主畫布上;
注:1. 每一個點根據值得大小設定顏色深度可以根據值得大小修改相應的
globalAlpha
。 2. 灰度canvas的繪製也不一定必須的繪製到主canvas,也可以使用離屏canvas,最後一步在將結果繪製到主canvas(heatMap.js就是如此)。 3. 灰度資料可以使用Uint8ClampedArray來運算,不一定非得畫出灰色的canvas來獲取資料,計算並不複雜。
思路就是如此,下面就是一個簡單的實現方式。
interface HeatMapConfig {
gradient?: object;
radius?: number;
width?: number;
height?: number;
min?: number;
max?: number;
container: HTMLElement
}
interface PointData{
x: number;
y: number;
value: number;
}
class HeatMap {
static defaultConfig = {
gradient: {
0.3: "blue",
0.5: "lime",
0.7: "yellow",
1: "red"
},
min: 0,
max: 100,
radius: 40,
width: 400,
height: 400
}
private config: HeatMapConfig;
private canvas = this.createCanvas();
private ctx = this.canvas.getContext('2d');
private data: PointData[] = [];
constructor(config: HeatMapConfig) {
this.initConfig(config);
}
private initConfig(config: HeatMapConfig) {
if(!config.container) {
throw Error('no container');
}
this.config = {
...HeatMap.defaultConfig,
...config
};
const {width, height} = this.config;
this.canvas.width = width;
this.canvas.height = height;
this.config.container.appendChild(this.canvas);
}
initData(data: PointData[]) {
this.data = data;
this.render();
}
private render() {
this.renderAlpha();
this.putColor()
}
// 繪製alpha通道的圓
private renderAlpha(){
const shadowCanvas = this.createShadowTpl();
const {min, max} = this.config;
for(let point of this.data) {
const alpha = (point.value - min) / (max - min);
this.ctx.globalAlpha = alpha;
this.ctx.drawImage(shadowCanvas, point.x, point.y);
}
}
// 為alpha通道的圓著色
private putColor() {
const colorData = this.createColordata();
const imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const {data} = imgData
for(let i = 0; i < data.length; i++) {
const value = data[i];
if(value) {
data[i - 3] = colorData[4 * value];
data[i - 2] = colorData[4 * value + 1];
data[i - 1] = colorData[4 * value + 2];
}
}
this.ctx.putImageData(imgData, 0, 0);
}
private createCanvas(){
return document.createElement('canvas')
}
private createColordata(){
const cCanvas = this.createCanvas();
const cCtx = cCanvas.getContext('2d');
cCanvas.width = 256;
cCanvas.height = 1;
const tuple: [number, number, number, number] =
[0, 0, cCanvas.width, cCanvas.height]
const grd = cCtx.createLinearGradient(...tuple);
const {gradient} = this.config;
for(let key in gradient) {
grd.addColorStop(parseFloat(key), gradient[key]);
}
cCtx.fillStyle = grd;
cCtx.fillRect(0, 0, cCanvas.width, cCanvas.height);
return cCtx.getImageData(...tuple).data;
}
/**
* 離屏canvas繪製一個黑色(rgb都是0,方便處理)的alpha通道的圓
*/
private createShadowTpl() {
const tCanvas = this.createCanvas();
const tCtx = tCanvas.getContext('2d');
const blur = 0;
const radius = this.config.radius;
tCanvas.width = 2 * radius;
tCanvas.height = 2 * radius;
const grd = tCtx.createRadialGradient(radius, radius, blur, radius, radius, radius);
grd.addColorStop(0, 'rgba(0,0,0,1)');
grd.addColorStop(1, 'rgba(0,0,0,0)');
tCtx.fillStyle = grd;
tCtx.fillRect(0, 0, 2 * radius, 2 * radius);
return tCanvas;
}
}
const heatmap = new HeatMap({
container: document.body
});
const data: PointData[] = [];
for(var i = 0; i < 100; i++) {
data.push({
x: Math.random() * 400,
y : Math.random() * 400,
value: Math.random() * 100
})
}
heatmap.initData(data);
複製程式碼