简介

热力图是以特殊高亮的形式显示访客热衷的页面区域和访客所在的地理区域的图示。

实现原理

  1. 为离散点信息创建一个Mask,Mask是一个圆形区域,半径为该点可以对最终图像产生影响的半径。
  2. 对每个Mask圆形区域使用渐变的灰度带由内向外填充,中心点权重为1,越向边缘辐射,权重越低,边缘部分权重为0。Mask渐变过程可以使用如线性变化、二次曲线等。
  3. 将离散点的Mask叠加,产生一幅灰度图像。由于灰度值的叠加,值越大颜色越亮,在灰度带中显得越白。相邻Mask重叠区域进行权重叠加操作,最终灰度图每个像素点的数值大小就是所有和其有关的Mask中的权重之和。离散点密度越高的地方灰度值越大,图像越亮。
  4. 以叠加后的灰度值为索引,从一条有256种颜色的色带中映射颜色,并对图像重新着色,从而实现热力图。

实现

数据

数据格式可以自由定义,只要每个点上包含 坐标权重 这两个必要信息。

创建mask

创建一个由黑到白的渐变圆

1
2
3
4
5
6
//创建一个径向渐变
let gradient=ctx.createRadialGradient(x,y,0,x,y,radius);
gradient.addColorStop(0,'rgba(0,0,0,1)');
gradient.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle=gradient;
ctx.fill();

设置alpha

在计算机图形学中,一个RGB颜色模型的真彩图形,用由红、绿、蓝三个色彩信息通道合成的,每个通道用了8位色彩深度,共计24位,包含了所有彩色信息。为实现图形的透明效果,采取在图形文件的处理与存储中附加上另一个8位信息的方法,这个附加的代表图形中各个素点透明度的通道信息就被叫做Alpha通道。Alpha通道使用8位二进制数,就可以表示256级灰度,即256级的透明度。白色(值为255)的Alpha像素用以定义不透明的彩色像素,而黑色(值为0)的Alpha通道像素用以定义透明像素,介于黑白之间的灰度(值为30-255)的Alpha像素用以定义不同程度的半透明像素。因而通过一个32位总线的图形卡来显示带Alpha通道的图形,就可能呈现出透明或半透明的视觉效果。
由于热力图是一个个数据点叠加上去的,那么在最后一个点画完之前是不能确定图上任意一点的颜色的。

上图中箭头指向的区域为什么是黄色的,是因为上下侧的绿色部分叠加后权重提升,从而编程黄色,也就是说下面的圆完成绘制之前并不能确定箭头处的颜色。
一个像素点的状态是由rgba4个元素决定的,在热力图中的颜色渐变并不是简单的线性叠加。但纯色灰度图像却可以,右边的图像是黑白的,严格来说只是纯黑色,只是alpha值不同。alpha值可以线性累加。也就是
只需要不断向canvas画小圆就行,每画一个圆,如果和前面图片重叠,那么重叠区域的alpha值会自动积累在canvas上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let x=heatData[i][0],
y=heatData[i][1],
w=heatData[i][2];
//创建一个由黑到白的渐变圆
ctx.beginPath();
//设置globalAlpha
let alpha=(w-minW)/(maxW-minW);
ctx.globalAlpha=alpha;
ctx.arc(x,y,radius,0,2*Math.PI,true);
//创建一个径向渐变
let gradient=ctx.createRadialGradient(x,y,0,x,y,radius);
gradient.addColorStop(0,'rgba(0,0,0,1)');
gradient.addColorStop(1,'rgba(0,0,0,0)');

ctx.fillStyle=gradient;
ctx.fill();
ctx.closePath();

设置颜色梯度

  1. 首先生成一个颜色字典,规定数据从低到高该使用哪些颜色,然后把它拉成一个线性结构,给出一个比值就能得到一个对应的颜色(比如可以生成的颜色字典,在这个字典里0对应蓝色1对应红色)。
  2. 遍历黑白图像的每一个像素,读取它的alpha值(0-1),按照该前面的配置,查询颜色字典,填色。
  3. 得到彩色图像
    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
    45
    46
    47
    48
    49
    50
    //可以通过canvas提供的getImageData返回ImageData对象,该对象拷贝了画布指定矩形的像素数据
    /*
    ImageData对象中的每个像素,都包含rgba四个信息
    r-红色 g-绿色 b-蓝色 a-alpha通道
    ImageData:{
    width,
    height,
    data:[0,0,255,0,0,...] //其中每四项分别对应一个像素点
    }
    canvas的方法putimageData()可以将像素级的数据放回到画布中
    */

    //建立一条长度为256px的颜色渐变带
    let createColorRamp=()=>{
    let colorCanvas=document.createElement('canvas');
    let colorCtx=colorCanvas.getContext('2d');
    let gradientColor={
    0.2:'rgba(0,0,255,0.2)',
    0.3:'rgba(43,111,231,0.3)',
    0.4:'rgba(2,192,241,0.4)',
    0.6:'rgba(44,222,148,0.6)',
    0.8:'rgba(254,237,83,0.8)',
    0.9:'rgba(255,118,50,0.9)',
    1.0:'rgba(255,64,28,1)'
    };
    colorCanvas.width=256;
    colorCanvas.height=1;
    let gradient=colorCtx.createLinearGradient(0,0,256,1);
    for(let key in gradientColor){
    gradient.addColorStop(key,gradientColor[key]);
    }
    colorCtx.fillStyle=gradient;
    colorCtx.fillRect(0,0,256,1);
    return colorCtx.getImageData(0,0,256,1).data
    }

    //映射颜色过程
    let toColor=createColorRamp();
    let img=ctx.getImageData(0,0,canvas.width,canvas.height);
    let imgData=img.data;
    for(let i=3;i<imgData.length;i++){
    let alpha=imgData[i];
    let offset=alpha * 4;
    if(offset){
    imgData[i-3]=toColor[offset];
    imgData[i-2]=toColor[offset+1];
    imgData[i-1]=toColor[offset+2];
    }
    }
    ctx.putImageData(img,0,0,0,0,canvas.width,canvas.height);

最终渲染效果如下: