二. 绘制
问题描述:
假设有一块画布,1200px*2000px尺寸,一组坐标数据,格式为[x,y]二维数组,量级为10000~100000,采样粒度为7*7。依据点坐标的分布密度绘制热力图方法一思路:使用canvas元素标签将所有点绘制到画布上,每个点给予较低的透明度。然后获取画布每个点的位数据,根据其alpha值(alpha ∈ [0, 255])的大小计算每一位的r,g,b的值,得出所有新的位数据之后,重新绘制。使之呈现为红色↔蓝色渐变。
代码:1 /*假设点坐标为aXY,二维数组*/
2 var aXY = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...];
3 //获取canvas的context
4 var context = canvas.getContext('2d');
5 var pi2 = Math.PI * 2;
6 //设置填充样式,透明度为0.1
7 context.fillStyle = 'rgba(255,30,0,0.1)';
8 for (var i = 0, len = aXY.length; i < len; i++) {
9     var x = aXY[i][0], y = aXY[i][1];
10     context.beginPath();
11     //绘制圆点
12     context.arc(x, y, 6, 0, pi2, true);
13     context.closePath();
14     context.fill();
15 }
16 //获取这个画布的位数据
17 var imgd = context.getImageData(0, 0, 1200, 2000);
18 var pix = imgd.data;
19 // 循环计算rgb,使之根据alpha值映射到红蓝渐变
20 for (var i = 0, n = pix.length; i < n; i += 4) {
21     //位数据的格式为[rgbargbargba……],每个rgba代表了每个点的rgba四个通道的值
22     var a = pix[i+3]; //alpha
23     //red
24     pix[i  ] = 128 * Math.sin((1 / 256 * a - 0.5 ) * Math.PI ) + 200;
25     //green
26     pix[i+1] = 128 * Math.sin((1 / 128 * a - 0.5 ) * Math.PI ) + 127;
27     //blue,128之后直接衰减为0
28     pix[i+2] = 256 * Math.sin((1 / 256 * a + 0.5 ) * Math.PI );
29     pix[i+3] = pix[i+3] * 0.8;
30 }
31 context.putImageData(imgd, 0, 0);上面的代码将会呈现:显而易见,这并不是热力图,但是可以精确反映每个点的分布密度,红色表示在该区域的点数据较多,浅,蓝色表示密度小。那么如何改进?
使用径向渐变代替圆点的绘制,用以表示每一个点向周围的点的辐射,渐变色的叠加可以展现梯度变换的效果。代码如下:1 var aXY = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...];
2 var context = canvas.getContext('2d');
3 for (var i = 0, len = aXY.length; i < len; i++) {
4     var x = aXY[i][0], y = aXY[i][1];
5     //绘制径向渐变
6     var radgrad = this.context.createRadialGradient(x, y, 1, x, y, 8);
7     //锚点
8     radgrad.addColorStop( 0, 'rgba(255,30,0,1)');
9     //锚点
10     radgrad.addColorStop( 1, 'rgba(255,30,0,0)');
11     context.fillStyle = radgrad;
12     context.fillRect( x - 8, y - 8, 16, 16);
13 }
效果如下:
方案度量:这是比较简单的实现方案,稍微麻烦的地方在于根据alpha值计算红蓝绿值,使得alpha高的地方显示红色,alpha低的显示蓝色,中间部分显示黄/绿色(考虑到效率与简单性,使用了简单的三角函数,如果需要更为精确的色相渐变,可以使用幂次变换)。同时这个方案的缺点也十分明显:在点数据量低的时候效率很高,但是点数据超过10000之后就会有明显的时间延迟>3s,原因在于循环绘制渐变色会消耗资源。其次该方案的性能也会取决于画布的大小。画布大的情况,比如画布尺寸为1200*3000,对其取位数据的时候,将会循环360万次,同时进行3*360万sin运算~~对于客户端性能是个问题。
方法二
思路:对所有点数据进行计算,得出每个点的密度值,然后依据密度值由低到高,绘制点数据。
代码:1 var points = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...];
2 var cache = {};
3 //计算每个点的密度
4 for (var i = 0, len = points.length; i < len; i++) {
5     for (var j = 0, len2 = points[i].length; j < len2; j++) {
6         var key = points[i][j][0] + '*' + points[i][j][1];
7         if (cache[key]) {
8             cache[key] ++;
9         } else {
10             cache[key] = 1;
11         }
12     }
13 }
14 //点数据还原
15 var oData = [];
16 for (var m in cache) {
17     if (m == '0*0') continue;
18     var x = parseInt(m.split('*')[0], 10);
19     var y = parseInt(m.split('*')[1], 0);
20     oData.push([x, y, cache[m]]);
21 }
22 //简单排序,使用数组内建的sort
23 oData.sort(function(a, b){
24     return a[2] - b[2];
25 });
26 var max = oData[oData.length - 1][2];
27 var pi2 = Math.PI * 2;
28 //设置阈值,可以过滤掉密度极小的点
29 var threshold = this._points_min_threshold * max;
30 //alpha增强参数
31 var pr = (Math.log(245)-1)/245;
32 for (var i = 0, len = oData.length; i < len; i++) {
33     if (oData[i][2]  0 ? 0 : 1);
34     //q参数用于平衡梯度差,使之符合人的感知曲线log2N,如需要精确梯度,去掉log计算
35     var q = parseInt(Math.log(oData[i][2]) / Math.log(max) * 255);
36     var r = parseInt(128 * Math.sin((1 / 256 * q - 0.5 ) * Math.PI ) + 200);
37     var g = parseInt(128 * Math.sin((1 / 128 * q - 0.5 ) * Math.PI ) + 127);
38     var b = parseInt(256 * Math.sin((1 / 256 * q + 0.5 ) * Math.PI ));
39     var alp = (0.92 * q + 20) / 255;
40     //如果需要灰度增强,则取消此行注释
41     //var alp = (Math.exp(pr * q + 1) + 10) / 255
42     var radgrad = this.context.createRadialGradient(oData[i][0], oData[i][1], 1, oData[i][0], oData[i][1], 8);
43     radgrad.addColorStop( 0, 'rgba(' + r + ',' + g + ','+ b + ',' + alp + ')');
44     radgrad.addColorStop( 1, 'rgba(' + r + ',' + g + ','+ b + ',0)');
45     this.context.fillStyle = radgrad;
46     this.context.fillRect( oData[i][0] - 8, oData[i][1] - 8, 16, 16);
47 }
以上代码结果如下:
大约处理了25000个点,用时大约700ms(鄙人的小本性能还行)。属于可接受范围内。
方案度量:此方案性能比方案一有明显优势。目前Marmot采用此方案。