基于Canvas的热力图绘制方法

简介:

. 介绍

最近参与的一个项目Marmot中需要根据点坐标绘制热力图。

热力图

以特殊高亮的形式显示访客热衷的页面区域或访客所在的地理区域

特点为:

1. 可以显示不可点击区域发生的事情。你将发现用户经常会点击那些不是链接的地方,也许你应该在那个地方放置一个资源链接。比如:如果你发现人们总是在点击某个产品图片,你能想到的是,他们也许想看大图,或者是想了解该产品的更多信息。 同样,他们可能会错误地认为特别的图片就是导航链接。

2. 热力图同时还能告诉你,页面的哪些部分吸引了大多数用户的注意。这对那些对web分析数据没有很多经验的产品人员非常有用。

3. 如果你在一个页面上有多个链接指向同一个URL,例如:如果有不同位置的3个链接指到同一个特定的产品页面 ,那么热力图将会显示你的访客最喜欢点击哪一个链接,这将帮助你提升网页的设计并让它对用户更加友好,不过实现这个功能需要一些设置。

…………

实例如下:

 

 

需要注意的是上图实例粒度粗,梯度小,容差大。反映了热力图的一个属性:趋势相关。不过,热力图也可以做到粒度细,梯度大,容差小。这完全是依据采样数据的精确性以及分析需求来做的。给个例子(Google的眼动分析[焦点梯度]图):

 

下面介绍热力图绘制的方法,注意,以下代码并没有检测数据有效性,也没有对数据进行过滤,剔除脏数据,同时没有处理异常。实际使用时请不要忽略此类情况,否则会对最终结果造成干扰……

二. 绘制

问题描述:

假设有一块画布,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 = 0len = 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 = 0n = 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 = 0len = 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);  

 

效果如下:

 

方案度量:这是比较简单的实现方案,稍微麻烦的地方在于根据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 = 0len = points.length; i < len; i++) {  
  5. for (var j = 0len2 = 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 = 0len = 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);  

以上代码结果如下:

大约处理了25000个点,用时大约700ms(鄙人的小本性能还行)。属于可接受范围内。

方案度量:此方案性能比方案一有明显优势。目前Marmot采用此方案。

 














本文转自百度技术51CTO博客,原文链接:http://blog.51cto.com/baidutech/747822 ,如需转载请自行联系原作者
相关文章
从rtsp视频流中截取图片
从rtsp视频流中截取图片
2499 0
|
存储 JSON API
批量采集抖音商品详情数据:推荐你使用API(通过商品id取商品详情商品主图sku属性)
批量采集抖音商品详情,建议使用API接口。步骤包括:注册抖音开放平台获取App Key和Secret,调用商品详情API接口传入商品ID及相关参数,解析返回的JSON获取商品信息(如名称、价格、主图和SKU)。此外,接口列表提供商品搜索、销售量查询、历史价格、订单管理等多种功能。已封装的API接口地址:c0b.cc/R4rbK2,可测试并联系获取SDK文件。
2113 1
|
存储 数据挖掘
YUV色彩空间
本文介绍 YUV存储格式,什么是色调?什么是色饱和度?人类视觉系统是如何感知YUV的?YUV比RGB好在哪里
716 0
|
自然语言处理 JavaScript 前端开发
|
Dart 前端开发 Android开发
【Flutter前端技术开发专栏】Flutter中的平台特定代码实现
【4月更文挑战第30天】Flutter旨在实现跨平台移动应用开发,但有时需针对iOS或Android编写特定代码。平台通道是关键机制,允许Dart代码与原生代码交互。通过`MethodChannel`等实现跨平台通信,然后在iOS和Android上响应调用。条件编译则在编译时决定特定平台代码。本文展示了如何在Flutter中处理平台特定功能,包括示例代码和总结。
679 0
【Flutter前端技术开发专栏】Flutter中的平台特定代码实现
|
SQL 关系型数据库 数据库
万字带你走过数据库的这激荡的三年
2023 年数据库回顾:向量数据库虽然大火,但没有技术壁垒;2022 年数据库回顾:江山代有新人出,区块链数据库还是那个傻主意;2021 年数据库回顾:性能之争烽烟起,不如低调搞大钱…
774 3
万字带你走过数据库的这激荡的三年
|
关系型数据库 MySQL Linux
在Linux中,如何配置邮件服务器?
在Linux中,如何配置邮件服务器?
|
存储 SQL Python
`urllib.parse`模块是Python标准库`urllib`中的一个子模块,它提供了处理URL(统一资源定位符)的实用功能。这些功能包括解析URL、组合URL、转义URL中的特殊字符等。
`urllib.parse`模块是Python标准库`urllib`中的一个子模块,它提供了处理URL(统一资源定位符)的实用功能。这些功能包括解析URL、组合URL、转义URL中的特殊字符等。
|
网络虚拟化
【Loopback Detection 环回检测以及原理解读】
【Loopback Detection 环回检测以及原理解读】
1547 0

热门文章

最新文章