你不知道的前端算法之热力图的实现 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
Aresn
V2EX    Javascript

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

  •  
  •   Aresn 2017-12-26 15:33:55 +08:00 5892 次点击
    这是一个创建于 2846 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文作者:TalkingData 可视化工程师李凤禄

    编辑:Aresn

    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 文字避让算法的实现。

    3 条回复    2017-12-27 10:08:21 +08:00
    Aresn
        1
    Aresn  
    OP
       2017-12-26 16:02:57 +08:00
    图片有问题,重新发一个吧,因为 V2EX 不能删除和修改 t/417755
    yutou527
        2
    yutou527  
       2017-12-26 20:27:50 +08:00
    ???我死循环了???
    ResidualSoils
        3
    ResidualSoils  
       2017-12-27 10:08:21 +08:00
    我也死循环
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2678 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 34ms UTC 02:29 PVG 10:29 LAX 19:29 JFK 22:29
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86