最近參與的一個(gè)項(xiàng)目Marmot中需要根據(jù)點(diǎn)坐標(biāo)繪制熱力圖。
熱力圖
以特殊高亮的形式顯示訪客熱衷的頁(yè)面區(qū)域或訪客所在的地理區(qū)域
特點(diǎn)為:
1. 可以顯示不可點(diǎn)擊區(qū)域發(fā)生的事情。你將發(fā)現(xiàn)用戶經(jīng)常會(huì)點(diǎn)擊那些不是鏈接的地方,也許你應(yīng)該在那個(gè)地方放置一個(gè)資源鏈接。比如:如果你發(fā)現(xiàn)人們總是在點(diǎn)擊某個(gè)產(chǎn)品圖片,你能想到的是,他們也許想看大圖,或者是想了解該產(chǎn)品的更多信息。 同樣,他們可能會(huì)錯(cuò)誤地認(rèn)為特別的圖片就是導(dǎo)航鏈接。
2. 熱力圖同時(shí)還能告訴你,頁(yè)面的哪些部分吸引了大多數(shù)用戶的注意。這對(duì)那些對(duì)web分析數(shù)據(jù)沒(méi)有很多經(jīng)驗(yàn)的產(chǎn)品人員非常有用。
3. 如果你在一個(gè)頁(yè)面上有多個(gè)鏈接指向同一個(gè)URL,例如:如果有不同位置的3個(gè)鏈接指到同一個(gè)特定的產(chǎn)品頁(yè)面 ,那么熱力圖將會(huì)顯示你的訪客最喜歡點(diǎn)擊哪一個(gè)鏈接,這將幫助你提升網(wǎng)頁(yè)的設(shè)計(jì)并讓它對(duì)用戶更加友好,不過(guò)實(shí)現(xiàn)這個(gè)功能需要一些設(shè)置。
…………
實(shí)例如下:
需要注意的是上圖實(shí)例粒度粗,梯度小,容差大。反映了熱力圖的一個(gè)屬性:趨勢(shì)相關(guān)。不過(guò),熱力圖也可以做到粒度細(xì),梯度大,容差小。這完全是依據(jù)采樣數(shù)據(jù)的精確性以及分析需求來(lái)做的。給個(gè)例子(Google的眼動(dòng)分析[焦點(diǎn)梯度]圖):
下面介紹熱力圖繪制的方法,注意,以下代碼并沒(méi)有檢測(cè)數(shù)據(jù)有效性,也沒(méi)有對(duì)數(shù)據(jù)進(jìn)行過(guò)濾,剔除臟數(shù)據(jù),同時(shí)沒(méi)有處理異常。實(shí)際使用時(shí)請(qǐng)不要忽略此類情況,否則會(huì)對(duì)最終結(jié)果造成干擾……
問(wèn)題描述:
假設(shè)有一塊畫布,1200px*2000px尺寸,一組坐標(biāo)數(shù)據(jù),格式為[x,y]二維數(shù)組,量級(jí)為10000~100000,采樣粒度為7*7。依據(jù)點(diǎn)坐標(biāo)的分布密度繪制熱力圖
思路:使用canvas元素標(biāo)簽將所有點(diǎn)繪制到畫布上,每個(gè)點(diǎn)給予較低的透明度。然后獲取畫布每個(gè)點(diǎn)的位數(shù)據(jù),根據(jù)其alpha值(alpha ∈ [0, 255])的大小計(jì)算每一位的r,g,b的值,得出所有新的位數(shù)據(jù)之后,重新繪制。使之呈現(xiàn)為紅色↔藍(lán)色漸變。
代碼:
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 | /*假設(shè)點(diǎn)坐標(biāo)為aXY,二維數(shù)組*/ var aXY = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]...]; //獲取canvas的context var context = canvas.getContext('2d'); var pi2 = Math.PI * 2; //設(shè)置填充樣式,透明度為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(); //繪制圓點(diǎn) context.arc(x, y, 6, 0, pi2, true); context.closePath(); context.fill(); } //獲取這個(gè)畫布的位數(shù)據(jù) var imgd = context.getImageData(0, 0, 1200, 2000); var pix = imgd.data; // 循環(huán)計(jì)算rgb,使之根據(jù)alpha值映射到紅藍(lán)漸變 for (var i = 0, n = pix.length; i < n; i += 4) { //位數(shù)據(jù)的格式為[rgbargbargba……],每個(gè)rgba代表了每個(gè)點(diǎn)的rgba四個(gè)通道的值 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); |
上面的代碼將會(huì)呈現(xiàn):
顯而易見(jiàn),這并不是熱力圖,但是可以精確反映每個(gè)點(diǎn)的分布密度,紅色表示在該區(qū)域的點(diǎn)數(shù)據(jù)較多,淺,藍(lán)色表示密度小。那么如何改進(jìn)?
使用徑向漸變代替圓點(diǎn)的繪制,用以表示每一個(gè)點(diǎn)向周圍的點(diǎn)的輻射,漸變色的疊加可以展現(xiàn)梯度變換的效果。代碼如下:
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); //錨點(diǎn) radgrad.addColorStop( 0, 'rgba(255,30,0,1)'); //錨點(diǎn) radgrad.addColorStop( 1, 'rgba(255,30,0,0)'); context.fillStyle = radgrad; context.fillRect( x - 8, y - 8, 16, 16); } |
效果如下:
方案度量:這是比較簡(jiǎn)單的實(shí)現(xiàn)方案,稍微麻煩的地方在于根據(jù)alpha值計(jì)算紅藍(lán)綠值,使得alpha高的地方顯示紅色,alpha低的顯示藍(lán)色,中間部分顯示黃/綠色(考慮到效率與簡(jiǎn)單性,使用了簡(jiǎn)單的三角函數(shù),如果需要更為精確的色相漸變,可以使用冪次變換)。同時(shí)這個(gè)方案的缺點(diǎn)也十分明顯:在點(diǎn)數(shù)據(jù)量低的時(shí)候效率很高,但是點(diǎn)數(shù)據(jù)超過(guò)10000之后就會(huì)有明顯的時(shí)間延遲>3s,原因在于循環(huán)繪制漸變色會(huì)消耗資源。其次該方案的性能也會(huì)取決于畫布的大小。畫布大的情況,比如畫布尺寸為1200*3000,對(duì)其取位數(shù)據(jù)的時(shí)候,將會(huì)循環(huán)360萬(wàn)次,同時(shí)進(jìn)行3*360萬(wàn)sin運(yùn)算~~對(duì)于客戶端性能是個(gè)問(wèn)題。
思路:對(duì)所有點(diǎn)數(shù)據(jù)進(jìn)行計(jì)算,得出每個(gè)點(diǎn)的密度值,然后依據(jù)密度值由低到高,繪制點(diǎn)數(shù)據(jù)。
代碼:
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 = {}; //計(jì)算每個(gè)點(diǎn)的密度 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; } } } //點(diǎn)數(shù)據(jù)還原 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]]); } //簡(jiǎn)單排序,使用數(shù)組內(nèi)建的sort oData.sort(function(a, b){ return a[2] - b[2]; }); var max = oData[oData.length - 1][2]; var pi2 = Math.PI * 2; //設(shè)置閾值,可以過(guò)濾掉密度極小的點(diǎn) var threshold = this._points_min_threshold * max; //alpha增強(qiáng)參數(shù) 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參數(shù)用于平衡梯度差,使之符合人的感知曲線log2N,如需要精確梯度,去掉log計(jì)算 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; //如果需要灰度增強(qiáng),則取消此行注釋 //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); } |
以上代碼結(jié)果如下:
大約處理了25000個(gè)點(diǎn),用時(shí)大約700ms(鄙人的小本性能還行)。屬于可接受范圍內(nèi)。
方案度量:此方案性能比方案一有明顯優(yōu)勢(shì)。目前Marmot采用此方案。
聯(lián)系客服