基于Canvas的热力图绘制方法
发布日期:2010年8月31日一. 介绍
最近参与的一个项目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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /*假设点坐标为aXY,二维数组*/ var aXY = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...]; //获取canvas的context var context = canvas.getContext('2d'); var pi2 = Math.PI * 2; //设置填充样式,透明度为0.1 context.fillStyle = 'rgba(255,30,0,0.1)'; for (var i = 0, len = aXY.length; i < len; i++) { var x = aXY[i][0], y = aXY[i][1]; context.beginPath(); //绘制圆点 context.arc(x, y, 6, 0, pi2, true); context.closePath(); context.fill(); } //获取这个画布的位数据 var imgd = context.getImageData(0, 0, 1200, 2000); var pix = imgd.data; // 循环计算rgb,使之根据alpha值映射到红蓝渐变 for (var i = 0, n = pix.length; i < n; i += 4) { //位数据的格式为[rgbargbargba……],每个rgba代表了每个点的rgba四个通道的值 var a = pix[i+3]; //alpha //red pix[i ] = 128 * Math.sin((1 / 256 * a - 0.5 ) * Math.PI ) + 200; //green pix[i+1] = 128 * Math.sin((1 / 128 * a - 0.5 ) * Math.PI ) + 127; //blue,128之后直接衰减为0 pix[i+2] = 256 * Math.sin((1 / 256 * a + 0.5 ) * Math.PI ); pix[i+3] = pix[i+3] * 0.8; } context.putImageData(imgd, 0, 0); |
上面的代码将会呈现:

显而易见,这并不是热力图,但是可以精确反映每个点的分布密度,红色表示在该区域的点数据较多,浅,蓝色表示密度小。那么如何改进?
使用径向渐变代替圆点的绘制,用以表示每一个点向周围的点的辐射,渐变色的叠加可以展现梯度变换的效果。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var aXY = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...]; var context = canvas.getContext('2d'); for (var i = 0, len = aXY.length; i < len; i++) { var x = aXY[i][0], y = aXY[i][1]; //绘制径向渐变 var radgrad = this.context.createRadialGradient(x, y, 1, x, y, 8); //锚点 radgrad.addColorStop( 0, 'rgba(255,30,0,1)'); //锚点 radgrad.addColorStop( 1, 'rgba(255,30,0,0)'); context.fillStyle = radgrad; context.fillRect( x - 8, y - 8, 16, 16); } |
效果如下:

方案度量:这是比较简单的实现方案,稍微麻烦的地方在于根据alpha值计算红蓝绿值,使得alpha高的地方显示红色,alpha低的显示蓝色,中间部分显示黄/绿色(考虑到效率与简单性,使用了简单的三角函数,如果需要更为精确的色相渐变,可以使用幂次变换)。同时这个方案的缺点也十分明显:在点数据量低的时候效率很高,但是点数据超过10000之后就会有明显的时间延迟>3s,原因在于循环绘制渐变色会消耗资源。其次该方案的性能也会取决于画布的大小。画布大的情况,比如画布尺寸为1200*3000,对其取位数据的时候,将会循环360万次,同时进行3*360万sin运算~~对于客户端性能是个问题。
方法二
思路:对所有点数据进行计算,得出每个点的密度值,然后依据密度值由低到高,绘制点数据。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | var points = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...]; var cache = {}; //计算每个点的密度 for (var i = 0, len = points.length; i < len; i++) { for (var j = 0, len2 = points[i].length; j < len2; j++) { var key = points[i][j][0] + '*' + points[i][j][1]; if (cache[key]) { cache[key] ++; } else { cache[key] = 1; } } } //点数据还原 var oData = []; for (var m in cache) { if (m == '0*0') continue; var x = parseInt(m.split('*')[0], 10); var y = parseInt(m.split('*')[1], 0); oData.push([x, y, cache[m]]); } //简单排序,使用数组内建的sort oData.sort(function(a, b){ return a[2] - b[2]; }); var max = oData[oData.length - 1][2]; var pi2 = Math.PI * 2; //设置阈值,可以过滤掉密度极小的点 var threshold = this._points_min_threshold * max; //alpha增强参数 var pr = (Math.log(245)-1)/245; for (var i = 0, len = oData.length; i < len; i++) { if (oData[i][2] 0 ? 0 : 1); //q参数用于平衡梯度差,使之符合人的感知曲线log2N,如需要精确梯度,去掉log计算 var q = parseInt(Math.log(oData[i][2]) / Math.log(max) * 255); var r = parseInt(128 * Math.sin((1 / 256 * q - 0.5 ) * Math.PI ) + 200); var g = parseInt(128 * Math.sin((1 / 128 * q - 0.5 ) * Math.PI ) + 127); var b = parseInt(256 * Math.sin((1 / 256 * q + 0.5 ) * Math.PI )); var alp = (0.92 * q + 20) / 255; //如果需要灰度增强,则取消此行注释 //var alp = (Math.exp(pr * q + 1) + 10) / 255 var radgrad = this.context.createRadialGradient(oData[i][0], oData[i][1], 1, oData[i][0], oData[i][1], 8); radgrad.addColorStop( 0, 'rgba(' + r + ',' + g + ','+ b + ',' + alp + ')'); radgrad.addColorStop( 1, 'rgba(' + r + ',' + g + ','+ b + ',0)'); this.context.fillStyle = radgrad; this.context.fillRect( oData[i][0] - 8, oData[i][1] - 8, 16, 16); } |
以上代码结果如下:

大约处理了25000个点,用时大约700ms(鄙人的小本性能还行)。属于可接受范围内。
方案度量:此方案性能比方案一有明显优势。目前Marmot采用此方案。

哈哈,真好玩,网页是直接在canvas里渲染的还是在canvas的下一层?
canvas是只是作为一个DOM节点覆盖在上面
深了!!
其实早就想做一个这样的分析工具了
可惜不会做,太谢谢你的代码了
如果想直接使用的话~~注意数据过滤以及错误/异常分析,另外制作红蓝渐变的时候也可以使用hsv的颜色格式,根据密点度改变h(色相),也可以达到色阶的效果。缺点是过度比较平缓。
这个创意真不错,canvas应用前景无可限量。
嗯。问一下
if (oData[i][2] 0 ? 0 : 1);这个是什么意思.过滤用的?
还有points[i][j][0],是三级数组?
额,貌似代码里面的>号怎么都没有了,那个是用于处理参考点带来的一像素分割用的。
实际使用points是三维数组,真正的格式是[[点击序列], [点击序列], [点击序列]]。文章中的代码有一点小问题,改为二维更合适
嗯,明白了。
避免Math.log(max)为0吧.
threshold 怎么没用上?
我那边Max肯定不会为0,因为我的每个点基数是1,有一个点就会是2~~,threshold放在这里是提示可能会使用,事实上用不用看需求。
赞,我也正在做这个,你给我了较大的思考。
建议将完整代码贴出,学习学习.谢谢!
额,这……单独联系吧,百度HI:remember2015
请更换图床,图都挂了
[WORDPRESS HASHCASH] The poster sent us ‘0 which is not a hashcash value.
图都好好的呢~~再试试吧,你不会设了host吧
[WORDPRESS HASHCASH] The poster sent us ‘0 which is not a hashcash value.
楼主,方法二中的没试出来,是否数据格式变了。能否发个demo给我,不胜感激
747038335@qq.com
done,请查收
你好,你成功运行方法一的功能,能不能发个demo给我啊?谢谢,我的邮箱是:136073220@qq.com。
done,请查收
var a = pix[i+3]; //alpha
//red
pix[i ] = 128 * Math.sin((1 / 256 * a – 0.5 ) * Math.PI ) + 200;
//green
pix[i+1] = 128 * Math.sin((1 / 128 * a – 0.5 ) * Math.PI ) + 127;
//blue,128之后直接衰减为0
pix[i+2] = 256 * Math.sin((1 / 256 * a + 0.5 ) * Math.PI );
pix[i+3] = pix[i+3] * 0.8;
这几行代码可以解释一下吗谢谢
根据透明度,映射出颜色,比如透明度为1.0,偏向红色,0.1偏向蓝色。你也可以使用hsv->rgb的算法做转换,不过缺点是梯度显示不足
能把Demo发给我吗,谢谢
E-mail:iam@songze.name
Done,请查收
第一次了解,我也想要个demo,谢谢
danielxia1234@gmail.com
Done,请查收
也烦请发我 zhaoxjmail@sohu.com 一份demo.谢谢!
done
我也想要份demo,thx!
znaiguang.g@gmail.com
小弟雪地跪求demo一份。感激不尽!
能否也发份demo给我,不胜感激!
komoxo(a)163.com
这个只是记录鼠标的动作吧?
我也需要一份demo,麻烦你了
Glad I’ve finllay found something I agree with!
很感兴趣,在最近做的统计产品里,我们做了点击率分布,没有热力图这种高级货,你们如何对待和处理canvas的兼容性呢?
另外感谢您Demo发给我一份,学习
zyflmw@qq.com
求一份DEMO学习 谢谢
邮箱 271285393@qq.com