Eisen's Blog

© 2024. All rights reserved.

一个用 canvas 画热力图的利器 heatmap.js

2011 December-31

最近做的一个东西,需要以热力图的形式去展示数据。于是就想找一找热力图的算法。找到了很多生成热力图的工具,它们的算法几乎是一致的,都是首先用 alpha 透明度方式画发散的圆在页面上,然后利用一个调色板,把对应的透明度改成相应的颜色即可。

一个很不错的中文的算法介绍在这里 浅谈 Heatmap

一个英文的在这里 How to Make Heat Map

虽说我本来打算的是找到算法自己去实现一下的,但是一不小心我发现万能的 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 的效果之外还可以做出来各种各样不错的效果的。

首先看看存储部分吧,比较简单,注释也比较清楚。

// 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。

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

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);
},

策略很简单,

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 可以自己做出来一个缓存起来。

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 就可以了。

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 的风格,真的像是在看一个不错的艺术品一样。强烈推荐一看~


利用 JS 做抓取的 demo

2011 December-07

传统的爬虫模拟 http 请求获取页面没有办法模拟 ajax 请求,导致很多数据就难以获取。爬虫一直在试图把自己弄得像一个浏览器那样,可以处理各种诡异的情况。但是很遗憾,如果这个爬虫没有 js 解析的能力那怎么能够去处理 ajax 呢,如果爬虫不知道怎么渲染界面,它怎么知道什么元素是可见的,什么元素是不可见的呢。现在很多页面都是采用了前端脚本。乱七八糟的 template 里面是什么真正的数据都没有的。json 里面的东西才是有意义的内容。所以,爬虫如果想完全处理那些问题,那它就要具备很多浏览器的功能了~

但是,如果反过来怎么样呢?我们其实可以就用一个真真正正的浏览器去做这些工作的呀。我们可以让一个浏览器去模拟爬虫的抓取形式。当页面 load 完成后就可以获取页面的 html 内容了 (innerHTML 这样的东西),然后检索整个页面上的合法 Link 去一个个打开做递归...

事实上,做爬虫很多时候就是想找到我们指定的一些数据。对页面里面很多乱七八糟的东西是没有兴趣的。可以只写 js 去定位一些元素里面的数据。当然,这会有很多的问题的。一个个打开,这样很慢的,效率是个问题。可是我觉得这的的确确是一个思路啊...

这个控件里面是有 ajax 请求的,每次点击了一个城市,才会激发 ajax 请求获取数据。利用传统的方式,可能就要去找 ajax 的规律,然后利用规律一个个下载。但是我们用前端模拟点击事件,让页面自己按照常规的方式加载新的数据,然后从 dom 里面取到我们所需的内容就可以了

这里我录了个屏,录屏丢了,由于尺寸比较大,就不插在文章里面了。请点击这个观看一段抓取的效果。当然,这仅仅是前端技术越发强大的冰山一角。我相信前端将会变得越发的强大以及越发的重要。


我写的一个 chrome 的 extension: Domain Time Tracker

2011 August-09

Domain Time Tracker 是一个用于记录用户浏览页面时间的插件。其实差不多就是对 FF 下的MeeTimer的山寨。

一直对自己的工作学习效率不是很满意,可是又没有找到什么很有效的办法去有效果改善这种情况(当然,我知道其实对待这种事情没有什么捷径,我的意思其实是找到一个比较适合我的办法...)。在翻网页的时候不经意间发现了一个有关时间管理的博客,褪墨。其中有一些利用软件去更好的管理自己的时间和生活的方法。我感觉对于比较宅,天天对着电脑的我,这些工具显然比较有吸引力。博主介绍的一些软件中就有 FF 下的MeeTimer。不过据我所知,MeeTimer在 FF3.0 之后的版本就不能使用了 (点击MeeTimer,页面上的支持版本就是这个啦~,这就是"据我所知"的出处)。而且我那那段时事件又感觉 chrome 所提供的开发 extension 的机制非常的棒~并且在当时的 chrome app store 里面也没有找到功能可以像MeeTimer那样强大的页面浏览时间追踪插件。于是就萌生了在 chrome 写一个满足自己需求的页面浏览时间追踪插件的想法。

这个扩展功能比较简单,但是我自己用起来还不错。同时,由于 google 要开发者交那 5 美元入会费才能把应用放到 app store。但在交钱之前选择所在地区时发现没有中国大陆同时我也木有信用卡,于是觉得暂时还是先放在自己网站这里吧。以后有了信用卡再去折腾把。

上面是说了我写这个扩展的动机。下面还是立即介绍下这个插件的功能吧,这才是重点。

虽然自己写这个东西写了相当久,但是其功能还是很简单的。主要就是根据域名统计用户在这个页面上停留的多长的时间。这里要强调一下所谓的"停留时间"是**用户把一个页面作为最前端页面,并且 chrome 在所有窗口出去最前面时的状态。并且,当插件发现用户在一个页面上停留时间超过 1 分钟并且对当前页面没有鼠标移动、按键点击等这些动作时,它会认为用户已经不再操纵计算机并停止计时。**也就是说,只有你真真正正的在看这个页面,在这个页面上时不时滑动鼠标,点击按键的时候这个扩展才会把所消耗的时间记录下来。

插件有分组的功能。在软件运行之初,所有的域名被放在一个"ungrouped"的分组之中。用户可以点击主界面上面的"Group Options"去建立一个新的分组。返回主界面后,当鼠标移动到列表里的域名上时会显示一个对域名做分组的编辑窗口哦~一个域名是可以放在多个分组之中的。

我一直觉得一个好的软件是不应该需要人去教才会用的。我也是本着这个理念去做这个小插件的。不求其功能一开始就非常完备,但求其看起来清清爽爽,干干净净,舒舒服服,让人装上它就是到它是干啥的。上面的讲解也主要是希望大家知道我所记录的时间到底是哪些时间~

当然,这个扩展还有很多可扩展的空间,还有很多功能需要改正。

**同步功能:**我目前觉得比较恶心的就是所有的数据只是在本地保存,而我经常在 Ubuntu 和 win7 换来换去导致有两个不同的统计结果。好想把它做的像是 chrome 的同步功能那样,让我可以在两个 chrome 下看到的是一直的统计结果。而且同步的另一个巨大的好处是我可以让我的数据可以保存的非常久远,不会因为我 ubuntu 毫无缘由的崩溃而烟消云散。

**查看更悠久数据:**说到长期统计下去就不得不说目前所能看到的统计时间窗口是非常有限的。最多看看上周的数据,或者看你全局的数据。但事实上,扩展中是统计了每天的数据的,需要我去写更多代码提供几个按钮罢了...

**数据可视化:**如果能把每天看英语那个分组生成个曲线图或者把上个月的数据和这个月的数据用一个柱状图显示,便于看到上个月和这个月的数据对比该多好...是啊,我也想啊,这东东到目前为止就是我在用啦。只是要想办法画图罢了....canvas 很强大,应该可以吧-_-

最后再说一句,感觉这样从小东西着手,循序渐进开发的方式很不错。一开始就要做个很大的东西一方面会遇到很多障碍导致进度过慢,并逐渐丧失信心。另一方面,由于项目较大又耐心不足会导致东西粗制滥造,让自己失望。从一个小东西开始,可以让自己更专注而且有精力尽量做的规整一些,仔细一些。

最后感慨一下~Google 是不是彻底放弃中国了呢,连开发者们都抛弃了。其实我对 Google 没意见,不要酱紫啊 T_T