基于HTML5實(shí)現(xiàn)的Heatmap熱圖3D應(yīng)用
Heatmap熱圖通過眾多數(shù)據(jù)點(diǎn)信息,匯聚成直觀可視化顏色效果,熱圖已廣泛被應(yīng)用于氣象預(yù)報(bào)、醫(yī)療成像、機(jī)房溫度監(jiān)控等行業(yè),甚至應(yīng)用于競技體育領(lǐng)域的數(shù)據(jù)分析。
已有眾多文章分享了生成Heatmap熱圖原理,可參考《How to make heat maps》和《How to make heat maps in Flex》,本文將介紹基于HTML5技術(shù)的實(shí)現(xiàn)方式,主要基于Cavans和WebGL這兩種HTML5的2D和3D技術(shù)的應(yīng)用,先上最終例子實(shí)現(xiàn)的界面效果和操作視頻:
實(shí)現(xiàn)Heatmap的開源js庫比較出名的就是 heatmapjs ,該框架發(fā)展了2年多,作者Patrick Wied最近決定在保持開源的基礎(chǔ)上,提供有償?shù)纳虡I(yè)支持服務(wù),這是好事,地球上絕大部分開源項(xiàng)目作者搞個(gè)barely可用的初級(jí)版本后,就多年不見更新了,而真正能實(shí)際上線使用的產(chǎn)品哪有不需要持續(xù)完善、增強(qiáng)可擴(kuò)展性以及提供特殊定制服務(wù)的,考慮到作者這兩年已無償投了這么多(Over the last 2 years, I devoted more than 500 hours of work to improving heatmap.js to make it a truly great library. ),希望此文也能幫作者在國內(nèi)起點(diǎn)宣傳作用。
heatmapjs 采用的Canvas的2D繪制方式實(shí)現(xiàn),這種基于CPU的繪制方式對(duì)于幾百幾千的點(diǎn)還湊合,但如果需要實(shí)時(shí)運(yùn)算成千上萬節(jié)點(diǎn)效果的,還是得依靠并發(fā)性更強(qiáng)大的GPU方式,采用HTML5的話只能是WebGL方案,還好Florian Boesch在《High Performance JS heatmaps》博客中提供了基于WebGL實(shí)現(xiàn)的heatmap方式,并將其開源在https://github.com/pyalot/webgl-heatmap 上,這兩個(gè)開源庫質(zhì)量都還不錯(cuò),一個(gè)基于Canvas實(shí)現(xiàn),一個(gè)基于WebGL實(shí)現(xiàn),后者性能高點(diǎn),但需要支持WebGL的瀏覽器,heatmapjs 的文檔例子比較全面,但兩者接口都非常簡單易學(xué),代碼也都就幾百行,你完全可以根據(jù)項(xiàng)目情況選擇甚至進(jìn)行代碼改造優(yōu)化。
回到我們要實(shí)現(xiàn)的例子,我將采用heatmapjs在內(nèi)存中實(shí)時(shí)運(yùn)算出熱圖,結(jié)合hightopo的HT for Web的3D引擎,以一堆節(jié)點(diǎn)連線關(guān)系的3D的網(wǎng)絡(luò)拓?fù)鋱D,其中節(jié)點(diǎn)代表熱源,其越接近地面則地面溫度越高,這樣每個(gè)節(jié)點(diǎn)的xz面坐標(biāo)信息作為要傳入給heatmapjs的點(diǎn)xy二維坐標(biāo)信息,三維節(jié)點(diǎn)的elevation也就是y軸信息,則作為離地面的距離信息,距離越大轉(zhuǎn)成要傳入heatmapjs的value值越小,***啟動(dòng)HT for Web的三維拓?fù)渥詣?dòng)布局彈力算法,這樣可直觀的觀察圖元節(jié)點(diǎn)在不同的空間位置動(dòng)態(tài)變化時(shí)地板的溫度熱圖變化效果。
代碼核心就在重載forceLayout.onRelaxed函數(shù),在每次自動(dòng)布局過程將所有熱源節(jié)點(diǎn)的信息構(gòu)建成heatmap需要的數(shù)據(jù),同時(shí)通過ht.Default.setImage(‘hm-bottom’, heatmap._renderer.canvas);將熱圖的canvas注冊(cè)成HT的圖片,而floor的地板圖元綁定了注冊(cè)的’hm-bottom’圖片,這樣就實(shí)現(xiàn)了內(nèi)存繪制canvas,然后通過HT for Web的3D引擎將Cavnas作為貼圖信息動(dòng)態(tài)呈現(xiàn)到3D場景的效果。
整個(gè)實(shí)現(xiàn)代碼如下不到百行,你也可以采用https://github.com/pyalot/webgl-heatmap 的WebGL方式來實(shí)現(xiàn),這樣就是3D到2D再到3D的有趣過程,這就是HTML5技術(shù)可無縫融合各種方案的魅力!
- 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;
- }