基於HTML5實現3D熱圖Heatmap應用

Heatmap熱圖經過衆多數據點信息,匯聚成直觀可視化顏色效果,熱圖已普遍被應用於氣象預報、醫療成像、機房溫度監控等行業,甚至應用於競技體育領域的數據分析。html

http://www.hightopo.com/guide/guide/plugin/forcelayout/examples/example_heatmap2d.htmlnode

591709-20151013225907116-4147716.png

http://www.hightopo.com/guide/guide/plugin/forcelayout/examples/example_heatmap3d.htmlgit

591709-20151013225453397-126998.png

已有衆多文章分享了生成Heatmap熱圖原理,可參考《How to make heat maps》和《How to make heat maps in Flex》,本文將介紹基於HTML5技術的實現方式,主要基於Cavans和WebGL這兩種HTML5的2D和3D技術的應用,先上最終例子實現的界面效果和操做視頻:github

IMG_10361.jpg

http://v.youku.com/v_show/id_XNzc5ODYxNjY4.html?firsttime=0web

實現Heatmap的開源js庫比較出名的就是 heatmapjs ,該框架發展了2年多,做者Patrick Wied最近決定在保持開源的基礎上,提供有償的商業支持服務,這是好事,地球上絕大部分開源項目做者搞個barely可用的初級版本後,就多年不見更新了,而真正能實際上線使用的產品哪有不須要持續完善、加強可擴展性以及提供特殊定製服務的,考慮到做者這兩年已無償投了這麼多(Over the last 2 years, I devoted more than 500 hours of work to improving heatmap.js to make it a truly great library. ),但願此文也能幫做者在國內起點宣傳做用。算法

heatmapjs 採用的Canvas的2D繪製方式實現,這種基於CPU的繪製方式對於幾百幾千的點還湊合,但若是須要實時運算成千上萬節點效果的,仍是得依靠併發性更強大的GPU方式,採用HTML5的話只能是WebGL方案,還好Florian Boesch在《High Performance JS heatmaps》博客中提供了基於WebGL實現的heatmap方式,並將其開源在https://github.com/pyalot/webgl-heatmap 上,這兩個開源庫質量都還不錯,一個基於Canvas實現,一個基於WebGL實現,後者性能高點,但須要支持WebGL的瀏覽器,heatmapjs 的文檔例子比較全面,但二者接口都很是簡單易學,代碼也都就幾百行,你徹底能夠根據項目狀況選擇甚至進行代碼改造優化。canvas

回到咱們要實現的例子,我將採用heatmapjs在內存中實時運算出熱圖,結合hightopo(http://www.hightopo.com/)的HT for Web的3D引擎,以一堆節點連線關係的3D的網絡拓撲圖,其中節點表明熱源,其越接近地面則地面溫度越高,這樣每一個節點的xz面座標信息做爲要傳入給heatmapjs的點xy二維座標信息,三維節點的elevation也就是y軸信息,則做爲離地面的距離信息,距離越大轉成要傳入heatmapjs的value值越小,最後啓動HT for Web的三維拓撲自動佈局彈力算法,這樣可直觀的觀察圖元節點在不一樣的空間位置動態變化時地板的溫度熱圖變化效果。瀏覽器

代碼核心就在重載forceLayout.onRelaxed函數,在每次自動佈局過程將全部熱源節點的信息構建成heatmap須要的數據,同時經過ht.Default.setImage(‘hm-bottom’, heatmap._renderer.canvas);將熱圖的canvas註冊成HT的圖片,而floor的地板圖元綁定了註冊的’hm-bottom’圖片,這樣就實現了內存繪製canvas,而後經過HT for Web的3D引擎將Cavnas做爲貼圖信息動態呈現到3D場景的效果。網絡

整個實現代碼以下不到百行,你也能夠採用https://github.com/pyalot/webgl-heatmap 的WebGL方式來實現,這樣就是3D到2D再到3D的有趣過程,這就是HTML5技術可無縫融合各類方案的魅力!併發

MAX = 500;
WIDTH = 1024;
HEIGHT = 512;        
function init() {                           
    dataModel = new ht.DataModel();            
    g3d = new ht.graph3d.Graph3dView(dataModel);                            
    g3d.getMoveMode = function(e){ return 'xyz'; };                        
    view = g3d.getView();            
    view.className = 'main';
    document.body.appendChild(view);    
    window.addEventListener('resize', function (e) { g3d.invalidate(); }, false);            
    heatmap = h337.create({ width: WIDTH, height: HEIGHT });                                   
    ht.Default.setImage('hm-bottom', heatmap._renderer.canvas);            
    var floor = new ht.Node();
    floor.s3(WIDTH, 1, HEIGHT);
    floor.s({
        '3d.selectable': false,
        'layoutable': false,
        'all.visible': false,
        'top.visible': true,
        'top.image': 'hm-bottom',
        'top.reverse.flip': true,
        'bottom.visible': true,
        'bottom.transparent': true,
        'bottom.opacity': 0.5,
        'bottom.reverse.flip': true                
    });
    dataModel.add(floor);            
    var root = createNode();                   
    for (var i = 0; i < 3; i++) {
        var iNode = createNode();                       
        createEdge(root, iNode);
        for (var j = 0; j < 3; j++) {
            var jNode = createNode();                            
            createEdge(iNode, jNode);                                                         
        }
    }                   
    forceLayout = new ht.layout.Force3dLayout(g3d);  
    forceLayout.start();
    forceLayout.onRelaxed = function(){
        var points = [];
        dataModel.each(function(data){
            if(data instanceof ht.Node && data !== floor){
                var p3 = data.p3();
                if(p3[1] > MAX){
                    p3[1] = MAX;
                    data.setElevation(MAX);
                }
                else if(p3[1] < -MAX){
                    p3[1] = -MAX;
                    data.setElevation(-MAX);
                }                        
                points.push({
                    x: p3[0] + WIDTH/2,
                    y: p3[2] + HEIGHT/2,
                    value: MAX - Math.abs(p3[1])
                });
            }
        });
        heatmap.setData({data: points, min: 0, max: MAX});
    };
}
function createNode(){
    var node = new ht.Node();             
    node.s({
        'shape3d': 'sphere',
        'shape3d.color': '#E74C3C',
        'shape3d.opacity': 0.8,
        'shape3d.transparent': true,
        'shape3d.reverse.cull': true
    });
    node.s3(20, 20, 20);
    dataModel.add(node);
    return node;
}
function createEdge(sourceNode, targetNode){
    var edge = new ht.Edge(sourceNode, targetNode);
    edge.s({
        'edge.width': 3,
        'edge.offset': 10,                
        'shape3d': 'cylinder',
        'shape3d.opacity': 0.7,
        'shape3d.transparent': true,
        'shape3d.reverse.cull': true
    });
    dataModel.add(edge);
    return edge;
}
相關文章
相關標籤/搜索