位置数据是链接线上线下的重要信息资源,在前端借助于图形化的手段将数据有效呈现是进行数据分析的重要手段。基于此,咱们开发了基于地图的数据可视化组件,以附加库的形式加入到JSAPI中,目前主要包括热力图、散点图、区域图、迁徙图。前端
热力图是以颜色来表现数据强弱大小及分布趋势的可视化类型,如上图左上角所示,热力图可应用于人口密度分析、活跃度分析等。呈现热力图的数据主要包括离散的坐标点及对应的强弱数值。canvas
本文只关心热力图的基础实现,不管你是用于地图,仍是网页焦点分析仍是其余场景,均需将对应场景的坐标转化为Canvas画布上的二维坐标,最终咱们须要的数据格式以下:浏览器
// x, y 表示二维坐标; value表示强弱值
var data = [
{x: 471, y: 277, value: 25},
{x: 438, y: 375, value: 97},
{x: 373, y: 19, value: 71},
{x: 473, y: 42, value: 63},
{x: 463, y: 95, value: 97},
{x: 590, y: 437, value: 34},
{x: 377, y: 442, value: 66},
{x: 171, y: 254, value: 20},
{x: 6, y: 582, value: 64},
{x: 387, y: 477, value: 14},
{x: 300, y: 300, value: 80}
];
复制代码
让咱们从结果来反推咱们应该如何实现热力图。 性能优化
根据咱们的直观感觉,咱们须要作的是:bash
以上步骤中须要注意的是第2步,咱们并不是直接以强度色谱填充圆形,由于这样获得的颜色是3个维度的,在叠加的时候不是线性的。本文选取了alpha
即颜色中的透明度做为表示强弱的维度,你也能够选取r
或者g
或者其余,后文会解释选择alpha
的好处。markdown
Canvas 中绘制弧线或者圆形可使用arc()
方法:函数
arc(x, y, radius, startAngle, endAngle, anticlockwise)
复制代码
x
和y
对应到数据的坐标,radius
可自由设置,startAngle
和endAngle
表示起止角度,分别取0
和2 * Math.PI
,anticlockwise
表示是否逆时针,可不设置。工具
Canvas 中可使用canvasGradient
对象建立渐变色,分为直线渐变createLinearGradient(x1, y1, x2, y2)
和径向渐变createRadialGradient(x1, y1, r1, x2, y2, r2)
,咱们采用后者。建立径向渐变色须要定义两个圆,颜色在两个圆之间的区域进行渐变,故而咱们将两个圆心都设置在数据的坐标点,而第一个圆半径取0,第二个半径同咱们须要绘制的圆形半径一致。性能
而后咱们须要经过addColorStop(position, color)
定义在两个圆之间颜色渐变的规则。咱们要达到的效果是颜色在某一个维度上的数值从中心随半径增长逐渐变小,并且同时,该维度的数值与数据的value
正相关,不然全部数据点绘制出的图形都会如出一辙。因此,咱们选择alpha
做为变化维度,由于咱们可使用globalAlpha
来设置一个全局的透明度,这个透明度与value
正相关,这样的话咱们就能够统一使用rgba(r,g,b,1)
和rgba(r,g,b,0)
做为中心点和半径边缘的颜色。测试
那么咱们经过如下代码来实现以上两个步骤:
/* * radius: 绘制半径,请自行设置 * min, max: 强弱阈值,可自行设置,也可取数据最小最大值 */ data.forEach(point => { let {x, y, value} = point; context.beginPath(); context.arc(x, y, radius, 0, 2 * Math.PI); context.closePath(); // 建立渐变色: r,g,b取值比较自由,咱们只关注alpha的数值 let radialGradient = context.createRadialGradient(x, y, 0, x, y, radius); radialGradient.addColorStop(0.0, "rgba(0,0,0,1)"); radialGradient.addColorStop(1.0, "rgba(0,0,0,0)"); context.fillStyle = radialGradient; // 设置globalAlpha: 需注意取值需规范在0-1之间 let globalAlpha = (value - min) / (max - min); context.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0); // 填充颜色 context.fill(); }); 复制代码
在示例中min
为0,max
为数据最大值,至此,咱们获得的图形以下:
可见图中的透明度已能表明数据强弱及辐射效果,且在相交处进行了线性的叠加。咱们如今要给图形上色,须要使用ImageData
对象对图像进行像素操做,读取每一个像素点的透明度,而后使用其映射后的颜色改写ImageData
数值。
先不急着了解像素操做如何进行,咱们首先要肯定的是透明度数值到颜色的映射关系。ImageData
中的透明度数值是取值在[0, 255]之间的整数,咱们要建立一个离散的映射函数,使0对应到最弱色(示例中为浅蓝色,你也能够自由设置),255对应到最强色(示例中为正红色)。而这个渐变的过程并非单一维度的递增,好在咱们已有工具解决渐变的问题,即上文已介绍过的createLinearGradient(x1, y1, x2, y2)
。
如上图所示,咱们能够建立一个跨度为 256 像素的直线渐变色,用其填充一个 256*1 的矩形,至关于一个调色盘。在这个调色盘上(0, 0)位置的像素呈现最弱色,(255, 0)位置的像素呈现最强色,因此对于透明度a,(a, 0)位置的像素颜色即为其映射颜色。代码以下:
const defaultColorStops = { 0: "#0ff", 0.2: "#0f0", 0.4: "#ff0", 1: "#f00", }; const width = 20, height = 256; function Palette(opts) { Object.assign(this, opts); this.init(); } Palette.prototype.init = function() { let colorStops = this.colorStops || defaultColorStops; // 建立canvas let canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; let ctx = canvas.getContext("2d"); // 建立线性渐变色 let linearGradient = ctx.createLinearGradient(0, 0, 0, height); for (const key in colorStops) { linearGradient.addColorStop(key, colorStops[key]); } // 绘制渐变色条 ctx.fillStyle = linearGradient; ctx.fillRect(0, 0, width, height); // 读取像素数据 this.imageData = ctx.getImageData(0, 0, 1, height).data; this.canvas = canvas; }; /** * 取色器 * @param {Number} position 像素位置 * @return {Array.<Number>} [r, g, b] */ Palette.prototype.colorPicker = function(position) { return this.imageData.slice(position * 4, position * 4 + 3); }; 复制代码
简单介绍一下ImageData
对象,其存储着Canvas对象真实的像素数据,包括width
, height
, data
三个属性。咱们能够:
createImageData(anotherImageData | width, height)
来建立一个新对象getImageData(left, top, width, height)
来建立带有Canvas画布中特定区域的像素数据的对象putImageData(myImageData, left, top)
来向Canvas画布写入像素数据基于此,咱们先获取画布数据,遍历像素点读取透明度,获取透明度映射颜色,改写像素数据并最终写入画布便可。
// 像素着色 let imageData = context.getImageData(0, 0, width, height); let data = imageData.data; for (var i = 3; i < data.length; i+=4) { let alpha = data[i]; let color = palette.colorPicker(alpha); data[i - 3] = color[0]; data[i - 2] = color[1]; data[i - 1] = color[2]; } context.putImageData(imageData, 0, 0); 复制代码
至此,咱们已经完成了热力图的绘制,看看效果吧:
若是咱们在地图上呈现热力图,随着地图的移动,数据点的坐标会变化,但其对应的圆形图像实际上是不变的。因此为了不更新坐标时重复地建立渐变色、设置globalAlpha
、绘制及填充颜色等,咱们能够预先绘制好每一个数据点的图像,经过一个不在文档流中的Canvas保存下来,在从新渲染的时候经过drawImage
将其绘制到画布上:
function Radiation(opts) { Object.assign(this, opts); this.init(); } Radiation.prototype.init = function() { let {radius, globalAlpha} = this; // 建立canvas let canvas = document.createElement("canvas"); canvas.width = canvas.height = radius * 2; // 获取上下文,初始化设置 let ctx = canvas.getContext("2d"); ctx.translate(radius, radius); ctx.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0); // 建立径向渐变色:灰度由强到弱 let radialGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radius); radialGradient.addColorStop(0.0, "rgba(0,0,0,1)"); radialGradient.addColorStop(1.0, "rgba(0,0,0,0)"); ctx.fillStyle = radialGradient; // 画圆 ctx.arc(0, 0, radius, 0, Math.PI * 2); ctx.fill(); this.canvas = canvas; }; Radiation.prototype.draw = function(context) { let {canvas, x, y, radius} = this; context.drawImage(canvas, x - radius, y - radius); }; 复制代码
2019.1.14更新:通过性能测试发现,上文所述有误。离屏渲染主要应用于局部绘制过程较复杂,而该局部又被重复绘制的场景下;同时应保证这个离屏的画布大小适中,由于复制过大的画布会带来很大的性能损耗。在上文中,局部绘制过程其实很简单,与直接使用drawImage
的耗时相差无几,因此无需使用离屏渲染。
使用drawImage
时若是使用了浮点数坐标,浏览器为了达到抗锯齿的效果,会作额外计算,渲染子像素。因此尽可能使用整数坐标。