103

你不知道的前端算法之热力图的实现 - Aresn - SegmentFault

 6 years ago
source link: https://segmentfault.com/a/1190000012589613?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本文作者:TalkingData 可视化工程师李凤禄

编辑:Aresn

欢迎加入 QQ 群参与技术讨论:618308202

inMap 是一款基于 canvas 的大数据可视化库,专注于大数据方向点线面的可视化效果展示。目前支持散点、围栏、热力、网格、聚合等方式;致力于让大数据可视化变得简单易用。

GitHub 地址:https://github.com/TalkingData/inmap (点个 Star 支持下作者吧!)

热力图这个名字听起来很高大上,其实等同于我们常说的密度图。

image

如图表示,红色区域表示分析要素的密度大,而蓝色区域表示分析要素的密度小。只要点密集,就会形成聚类区域。
看到这么炫的效果,是不是自己也很想实现一把?接下来手把手实现一个热力(带你装逼带你飞、 哈哈),郑重声明:下面代码片段均来自 inMap

inMap 接收的是经纬度数据,需要把它映射到 canvas 的像素坐标,这就用到了墨卡托转换,墨卡托算法很复杂,以后我们会有单独的一篇文章来讲讲他的原理。经过转换,你得到的数据应该是这样的:

[
  {
    "lng": "116.395645",
    "lat": 39.929986,
    "count": 6,
    "pixel": { //像素坐标
      "x": 689,
      "y": 294
    }
  },
  {
    "lng": "121.487899",
    "lat": 31.249162,
    "count": 10,
    "pixel": { //像素坐标
      "x": 759,
      "y": 439
    }
  },
  ...
]

好了,我们得到转换后的像素坐标数据(x、y),就可以做下面的事情了。

创建 canvas 渐变填充

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

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.arc(x, y, radius, 0, Math.PI * 2, true);
  • createRadialGradient() 创建线性的渐变对象
  • addColorStop() 定义一个渐变的颜色带

效果如图:

image

那么问题就来了,如果每个数据权重值 count 不一样,我们该如何表示呢?

设置 globalAlpha

根据不同的count值设置不同的Alpha,假设最大的count的Alpha等于1,最小的count的Alpha为0,那么我根据count求出Alpha。

let alpha = (count - minValue) / (maxValue - minValue);

然后我们代码如下:

drawPoint(x, y, radius, alpha) {
    let ctx = this.ctx;
    ctx.globalAlpha = alpha; //设置 Alpha 透明度
    ctx.beginPath();
    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.arc(x, y, radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
}

效果跟上一个截图有很大区别,可以对比一下透明度的变化。

image

(这么黑乎乎的一团,跟热力差距好大啊)

image

重置 canvas 画布颜色

  • getImageData() 复制画布上指定矩形的像素数据
  • putImageData() 将图像数据放回画布:

getImageData()返回的数据格式如下:

{
  "data": {
    "0": 0,   //R
    "1": 128, //G
    "2": 0,   //B
    "3": 255, //Aplah
    "4": 0, //R
    "5": 128, //G
    "6": 0,  //B
    "7": 255, //Aplah
    "8": 0,
    "9": 128,
    "10": 0,
    "11": 255,
    "12": 0,
    "13": 128,
    "14": 0,
    "15": 255,
    "16": 0,
    "17": 128,
    "18": 0,
    "19": 255,
    "20": 0,
    "21": 128,
    "22": 0
    ...

返回的数据是一维数组,每四个元素表示一个像素(rgba)值。

实现热力原理:读取每个像素的alpha值(透明度),做一个颜色映射。

代码如下:

let palette = this.getColorPaint(); //取色面板
let img = ctx.getImageData(0, 0, container.width, container.height);
    let imgData = img.data;
    let max_opacity = normal.maxOpacity * 255;
    let min_opacity = normal.minOpacity * 255;
    //权重区间
    let max_scope = (normal.maxScope > 1 ? 1 : normal.maxScope) * 255;
    let min_scope = (normal.minScope < 0 ? 0 : normal.minScope) * 255;
    let len = imgData.length;
    for (let i = 3; i < len; i += 4) {
        let alpha = imgData[i]; 
        let offset = alpha * 4;
        if (!offset) {
            continue;
        }
        //映射颜色
        imgData[i - 3] = palette[offset];
        imgData[i - 2] = palette[offset + 1];
        imgData[i - 1] = palette[offset + 2];

        // 范围区间
        if (imgData[i] > max_scope) {
            imgData[i] = 0;
        }
        if (imgData[i] < min_scope) {
            imgData[i] = 0;
        }

        // 透明度
        if (imgData[i] > max_opacity) {
            imgData[i] = max_opacity;
        }
        if (imgData[i] < min_opacity) {
            imgData[i] = min_opacity;
        }
    }
    //将设置后的像素数据放回画布
ctx.putImageData(img, 0, 0, 0, 0, container.width, container.height);

创建颜色映射,一个好的颜色映射决定最终效果。
inMap 创建一个长256px的调色面板:

let paletteCanvas = document.createElement('canvas');
let paletteCtx = paletteCanvas.getContext('2d');
paletteCanvas.width = 256;
paletteCanvas.height = 1;
let gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);

inMap 默认颜色如下:

this.gradient = {
    0.25: 'rgb(0,0,255)',
    0.55: 'rgb(0,255,0)',
    0.85: 'yellow',
    1.0: 'rgb(255,0,0)'
};

将gradient颜色设置到调色面板对象中

for (let key in gradient) {
    gradient.addColorStop(key, gradientConfig[key]);
}

返回调色面板的像素点数据:

return paletteCtx.getImageData(0, 0, 256, 1).data;

创建出来的调色面板效果图如下:(看起来像一个渐变颜色条)

image

最终我们实现的热力图如下:

image

下一节,我们将重点介绍 inMap 文字避让算法的实现。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK