本文做者:TalkingData 可视化工程师李凤禄编辑:Aresngit
欢迎加入 QQ 群参与技术讨论:618308202github
inMap 是一款基于 canvas 的大数据可视化库,专一于大数据方向点线面的可视化效果展现。目前支持散点、围栏、热力、网格、聚合等方式;致力于让大数据可视化变得简单易用。算法
热力图这个名字听起来很高大上,其实等同于咱们常说的密度图。canvas
如图表示,红色区域表示分析要素的密度大,而蓝色区域表示分析要素的密度小。只要点密集,就会造成聚类区域。
看到这么炫的效果,是否是本身也很想实现一把?接下来手把手实现一个热力(带你装逼带你飞、 哈哈),郑重声明:下面代码片断均来自 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),就能够作下面的事情了。this
建立一个由黑到白的渐变圆spa
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);
效果如图:
那么问题就来了,若是每一个数据权重值 count 不同,咱们该如何表示呢?3d
根据不一样的count值设置不一样的Alpha,假设最大的count的Alpha等于1,最小的count的Alpha为0,那么我根据count求出Alpha。code
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(); }
效果跟上一个截图有很大区别,能够对比一下透明度的变化。
(这么黑乎乎的一团,跟热力差距好大啊)
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)值。
代码以下:
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;
建立出来的调色面板效果图以下:(看起来像一个渐变颜色条)
最终咱们实现的热力图以下:
下一节,咱们将重点介绍 inMap 文字避让算法的实现。