heatmap.js 一個用canvas畫熱力圖的利器
最近做的一個東西,需要以熱力圖的形式去展示資料。於是就想找一找熱力圖的演算法。找到了很多生成熱力圖的工具,它們的演算法幾乎是一致的,都是首先用alpha透明度方式畫發散的圓在頁面上,然後利用一個調色盤,把對應的透明度改成相應的顏色即可。
一個很不錯的中文的演算法介紹在這裡 淺談Heatmap
雖說我本來打算的是找到演算法自己去實現一下的,但是一不小心我發現萬能的google在搜尋記錄裡面給了我一個heatmap.js的連結。我好奇的點進去發現這就是我所需要實現的東西…
heatmap.js可以使用canvas畫出來一張漂亮的heatmap。更重要的是它支援資料的動態新增。比如,上圖的演示就是一個利用mousemove事件生成heatmap的例子。它會自動的重新整理canvas,實時顯示滑鼠運動的heatmap。
開啟heatmap.js發現裡面的程式碼並不多,但是真的很精悍。
頁面程式碼請點選這裡[heatmap.js],下面我做一個code的分析吧,看了那麼久了,寫下來一方面是自己加深記憶,另一方面就是可以更好的理清思路吧。[寫就是為了更好的思考]麼。
code中包含兩個主要的物件,store heatmap。store是heatmap的資料部分,算是model吧。而heatmap則是真正繪製圖像的物件。heatmap部分可以被配置,可以自定義很多的內容,尤其是配色也是可以配置的,那麼我們除了做出來正真的heatmap的效果之外還可以做出來各種各樣不錯的效果的。
首先看看儲存部分吧,比較簡單,註釋也比較清楚。
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 |
// store object constructor
// a heatmap contains a store
// the store has to know about the heatmap
// in order to trigger heatmap updates when
// datapoints get added
function
store(hmap){
var
_ = {
// data is a two dimensional array
// a datapoint gets saved as data[point-x-value][point-y-value] // the value at [point-x-value][point-y-value]
// is the occurrence of the datapoint
data: [],
// tight coupling of the heatmap object
heatmap: hmap
};
// the max occurrence - the heatmaps radial gradient
// alpha transition is based on it
this .max = 1;
this .get =
function (key){
return
_[key];
},
this .set =
function (key, value){
_[key] = value;
};
};
|
在model裡面,支援一次新增一個數據點。這也是heatmapjs支援實時繪製的關鍵。一旦max值有變化就會重新繪製整個canvas。
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 |
addDataPoint:
function (x, y){
if (x < 0 || y < 0)
return ;
var
me = this ,
heatmap = me.get( "heatmap" ),
data = me.get( "data" );
if (!data[x])
data[x] = [];
if (!data[x][y])
data[x][y] = 0;
// if count parameter is set increment by count otherwise by 1
data[x][y]+=(arguments.length me.set( "data" , data);
// do we have a new maximum?
if (me.max < data[x][y]){
me.max = data[x][y];
// max changed, we need to redraw all existing(lower) datapoints
heatmap.get( "actx" ).clearRect(0,0,heatmap.get( "width" ),heatmap.get( "height" ));
for ( var
one in
data)
for ( var
two in
data[one])
heatmap.drawAlpha(one, two, data[one][two]);
// @TODO
// implement feature
// heatmap.drawLegend(); ?
return ;
}
heatmap.drawAlpha(x, y, data[x][y]);
},
|
下面就是畫的部分了。這裡是最重要的兩個方法,drawAlpha colorize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
drawAlpha:
function (x, y, count){
// storing the variables because they will be often used
var
me = this ,
r1 = me.get( "radiusIn" ),
r2 = me.get( "radiusOut" ),
ctx = me.get( "actx" ),
max = me.get( "max" ),
// create a radial gradient with the defined parameters.
// we want to draw an alphamap
rgr = ctx.createRadialGradient(x,y,r1,x,y,r2),
xb = x-r2, yb = y-r2, mul = 2*r2;
// the center of the radial gradient has .1 alpha value
rgr.addColorStop(0,
'rgba(0,0,0,' +((count)?(count/me.store.max): '0.1' )+ ')' );
// and it fades out to 0
rgr.addColorStop(1,
'rgba(0,0,0,0)' );
// drawing the gradient
ctx.fillStyle = rgr;
ctx.fillRect(xb,yb,mul,mul);
// finally colorize the area
me.colorize(xb,yb);
},
|
策略很簡單,
1 2 3 |
rgr.addColorStop(0,
'rgba(0,0,0,' +((count)?(count/me.store.max): '0.1' )+ ')' );
// and it fades out to 0
rgr.addColorStop(1,
'rgba(0,0,0,0)' );
|
利用當前點的count除以最大的count獲取的結果做為alpha值。然後做一個RadialGradient畫出來這個圖就可以了。那麼由於多個相近的點aphla效果的疊加就可以獲取想要的效果了。這裡就是canvas的nb之處了,看別的語言實現都是採用將一個這樣的png圖片畫到畫板上,但是canvas就可以直接實現這個效果。
有了這幅aphla版本的heatmap 我們利用一個配送版做著色就大功告成了。
這裡又用到了上面所說的canvas的nb之處,在通常需要一個圖片作為配色板的時候canvas可以自己做出來一個快取起來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
initColorPalette:
function (){
var
me = this ,
canvas = document.createElement( "canvas" );
canvas.width =
"1" ;
canvas.height =
"256" ;
var
ctx = canvas.getContext( "2d" ),
grad = ctx.createLinearGradient(0,0,1,256),
gradient = me.get( "gradient" );
for ( var
x in
gradient){
grad.addColorStop(x, gradient[x]);
}
ctx.fillStyle = grad;
ctx.fillRect(0,0,1,256);
//這裡太強大了,快取了我的畫板資料,然後刪除了畫板
me.set( "gradient" , ctx.getImageData(0,0,1,256).data);
delete
canvas;
delete
grad;
delete
ctx;
},
|
這種方式也給我們實現各種各樣的配色提供了方便,我們只需要改變那個 **gradient** 就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
for ( var
i=3; i < length; i+=4){ // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
var
alpha = imageData[i],
offset = alpha*4;
if (!offset)
continue ;
// we ve started with i=3
// set the new r, g and b values
// 根據透明度選擇配色板上的配色
imageData[i-3]=palette[offset];
imageData[i-2]=palette[offset+1];
imageData[i-1]=palette[offset+2];
// we want the heatmap to have a gradient from transparent to the colors
// as long as alpha is lower than the defined opacity (maximum),
// we'll use the alpha value
imageData[i] = (alpha < opacity)?alpha:opacity;
}
|
還是很簡練的吧,看到heatmap.js的風格,真的像是在看一個不錯的藝術品一樣。強烈推薦一看~