Heatmap熱圖通過眾多資料點資訊,匯聚成直觀視覺化顏色效果,熱圖已廣泛被應用於氣象預報、醫療成像、機房溫度監控等行業,甚至應用於競技體育領域的資料分析。
已有眾多文章分享了生成Heatmap熱圖原理,可參考《How to make heat maps》和《How to make heat maps in Flex》,本文將介紹基於HTML5技術的實現方式,主要基於Cavans和WebGL這兩種HTML5的2D和3D技術的應用,先上最終例子實現的介面效果和操作視訊:
<iframe src="http://player.youku.com/embed/XNzc5ODYxNjY4" frameborder="0" width="510" height="498"></iframe>
實現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 的文件例子比較全面,但兩者介面都非常簡單易學,程式碼也都就幾百行,你完全可以根據專案情況選擇甚至進行程式碼改造優化。
回到我們要實現的例子,我將採用heatmapjs在記憶體中實時運算出熱圖,結合hightopo的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; }