在HT for Web中2D和3D應用都支持樹狀結構數據的展現,展示效果各異,2D上的樹狀結構在展示層級關係明顯,可是若是數據量大的話,看起來就沒那麼直觀,找到指定的節點比較困難,而3D上的樹狀結構在展示上配合HT for Web的彈力佈局組件會顯得比較直觀,一眼望去能夠把整個樹狀結構數據看個大概,可是在彈力佈局的做用下,其層次結構看得就不是那麼清晰了。因此這時候結構清晰的3D樹的需求就來了,那麼這個3D樹具體長成啥樣呢,咱們來一塊兒目擊下~javascript
要實現這樣的效果,該從何下手呢?接下來咱們就將這個問題拆解成若干個小問題來解決。html
1. 建立一個樹狀結構java
有了解過HT for Web的朋友,對樹狀結構數據的建立應該都不陌生,在這裏我就不作深刻的探討了。樹狀結構數據的建立很簡單,在這裏爲了讓代碼更簡潔,我封裝了三個方法來建立樹狀結構數據,具體代碼以下:node
/** * 建立連線 * @param {ht.DataModel} dataModel - 數據容器 * @param {ht.Node} source - 起點 * @param {ht.Node} target - 終點 */ function createEdge(dataModel, source, target) { // 建立連線,連接父親節點及孩子節點 var edge = new ht.Edge(); edge.setSource(source); edge.setTarget(target); dataModel.add(edge); } /** * 建立節點對象 * @param {ht.DataModel} dataModel - 數據容器 * @param {ht.Node} [parent] - 父親節點 * @returns {ht.Node} 節點對象 */ function createNode(dataModel, parent) { var node = new ht.Node(); if (parent) { // 設置父親節點 node.setParent(parent); createEdge(dataModel, parent, node); } // 添加到數據容器中 dataModel.add(node); return node; } /** * 建立結構樹 * @param {ht.DataModel} dataModel - 數據容器 * @param {ht.Node} parent - 父親節點 * @param {Number} level - 深度 * @param {Array} count - 每層節點個數 * @param {function(ht.Node, Number, Number)} callback - 回調函數(節點對象,節點對應的層級,節點在層級中的編號) */ function createTreeNodes(dataModel, parent, level, count, callback) { level--; var num = (typeof count === 'number' ? count : count[level]); while (num--) { var node = createNode(dataModel, parent); // 調用回調函數,用戶能夠在回調裏面設置節點相關屬性 callback(node, level, num); if (level === 0) continue; // 遞歸調用建立孩子節點 createTreeNodes(dataModel, node, level, count, callback); } }
嘿嘿,代碼寫得可能有些複雜了,簡單的作法就是嵌套幾個for循環來建立樹狀結構數據,在這裏我就很少說了,接下來咱們來探究第二個問題。數組
在3D下的樹狀結構體最大的問題就在於,每一個節點的層次及每層節點圍繞其父親節點的半徑計算。如今樹狀結構數據已經有了,那麼接下來就該開始計算半徑了,咱們從兩層樹狀結構開始推算:dom
我如今先建立了兩層的樹狀結構,全部的子節點是一字排開,並無環繞其父親節點,那麼咱們該如何去肯定這些孩子節點的位置呢?ide
首先咱們得知道,每一個末端節點都有一圈屬於本身的領域,否則節點與節點之間將會存在重疊的狀況,因此在這裏,咱們假定末端節點的領域半徑爲25,那麼兩個相鄰節點之間的最短距離將是兩倍的節點領域半徑,也就是50,而這些末端節點將均勻地圍繞在其父親節點四周,那麼相鄰兩個節點的張角就能夠確認出來,有了張角,有了兩點間的距離,那麼節點繞其父親節點的最短半徑也就能計算出來了,假設張角爲a,兩點間最小距離爲b,那麼最小半徑r的計算公式爲:函數
r = b / 2 / sin(a / 2); 佈局
那麼接下來我麼就來佈局下這個樹,代碼是這樣寫的:
/** * 佈局樹 * @param {ht.Node} root - 根節點 * @param {Number} [minR] - 末端節點的最小半徑 */ function layout(root, minR) { // 設置默認半徑 minR = (minR == null ? 25 : minR); // 獲取到全部的孩子節點對象數組 var children = root.getChildren().toArray(); // 獲取孩子節點個數 var len = children.length; // 計算張角 var degree = Math.PI * 2 / len; // 根據三角函數計算繞父親節點的半徑 var sin = Math.sin(degree / 2), r = minR / sin; // 獲取父親節點的位置座標 var rootPosition = root.p(); children.forEach(function(child, index) { // 根據三角函數計算每一個節點相對於父親節點的偏移量 var s = Math.sin(degree * index), c = Math.cos(degree * index), x = s * r, y = c * r; // 設置孩子節點的位置座標 child.p(x + rootPosition.x, y + rootPosition.y); }); }
在代碼中,你會發現我將末端半徑默認設置爲25了,如此,咱們經過調用layout()方法就能夠對結構樹進行佈局了,其佈局效果以下:
從效果圖能夠看得出,末端節點的默認半徑並非很理想,佈局出來的效果連線都快看不到了,所以咱們能夠增長末端節點的默認半徑來解決佈局太密的問題,如將默認半徑設置成40的效果圖以下:
如今兩層的樹狀分佈解決了,那麼咱們來看看三層的樹狀分佈該如何處理。
將第二層和第三層當作一個總體,那麼其實三層的樹狀結構跟兩層是同樣的,不一樣的是在處理第二層節點時,應該將其看作一個兩層的樹狀結構來處理,那麼像這種規律的處理用遞歸最好不過了,所以咱們將代碼稍微該着下,在看看效果如何:
不行,節點都重疊在一塊兒了,看來簡單的遞歸是不行的,那麼具體的問題出在哪裏呢?
仔細分析了下,發現父親節點的領域半徑是由其孩子節點的領域半徑決定的,所以在佈局時須要知道自身節點的領域半徑,並且節點的位置取決於父親節點的領域半徑及位置信息,這樣一來就沒法邊計算半徑邊佈局節點位置了。
那麼如今只能將半徑的計算和佈局分開來,作兩步操做了,咱們先來分析下節點半徑的計算:
首先須要明確最關鍵的條件,父親節點的半徑取決於其孩子節點的半徑,這個條件告訴咱們,只能從下往上計算節點半徑,所以咱們設計的遞歸函數必須是先遞歸後計算,廢話很少說,咱們來看下具體的代碼實現:
/** * 就按節點領域半徑 * @param {ht.Node} root - 根節點對象 * @param {Number} minR - 最小半徑 */ function countRadius(root, minR) { minR = (minR == null ? 25 : minR); // 若果是末端節點,則設置其半徑爲最小半徑 if (!root.hasChildren()) { root.a('radius', minR); return; } // 遍歷孩子節點遞歸計算半徑 var children = root.getChildren(); children.each(function(child) { countRadius(child, minR); }); var child0 = root.getChildAt(0); // 獲取孩子節點半徑 var radius = child0.a('radius'); // 計算子節點的1/2張角 var degree = Math.PI / children.size(); // 計算父親節點的半徑 var pRadius = radius / Math.sin(degree); // 設置父親節點的半徑及其孩子節點的佈局張角 root.a('radius', pRadius); root.a('degree', degree * 2); }
OK,半徑的計算解決了,那麼接下來就該解決佈局問題了,佈局樹狀結構數據須要明確:孩子節點的座標位置取決於其父親節點的座標位置,所以佈局的遞歸方式和計算半徑的遞歸方式不一樣,咱們須要先佈局父親節點再遞歸佈局孩子節點,具體看看代碼吧:
/** * 佈局樹 * @param {ht.Node} root - 根節點 */ function layout(root) { // 獲取到全部的孩子節點對象數組 var children = root.getChildren().toArray(); // 獲取孩子節點個數 var len = children.length; // 計算張角 var degree = root.a('degree'); // 根據三角函數計算繞父親節點的半徑 var r = root.a('radius'); // 獲取父親節點的位置座標 var rootPosition = root.p(); children.forEach(function(child, index) { // 根據三角函數計算每一個節點相對於父親節點的偏移量 var s = Math.sin(degree * index), c = Math.cos(degree * index), x = s * r, y = c * r; // 設置孩子節點的位置座標 child.p(x + rootPosition.x, y + rootPosition.y); // 遞歸調用佈局孩子節點 layout(child); }); }
代碼寫完了,接下來就是見證奇蹟的時刻了,咱們來看看效果圖吧:
不對呀,代碼應該是沒問題的呀,爲何顯示出來的效果仍是會重疊呢?不過仔細觀察咱們能夠發現相比上個版本的佈局會好不少,至少此次只是末端節點重疊了,那麼問題出在哪裏呢?
不知道你們有沒有發現,排除節點自身的大小,倒數第二層節點與節點之間的領域是相切的,那麼也就是說節點的半徑不只和其孩子節點的半徑有關,還與其孫子節點的半徑有關,那咱們把計算節點半徑的方法改造下,將孫子節點的半徑也考慮進去再看看效果如何,改造後的代碼以下:
/** * 就按節點領域半徑 * @param {ht.Node} root - 根節點對象 * @param {Number} minR - 最小半徑 */ function countRadius(root, minR) { …… var child0 = root.getChildAt(0); // 獲取孩子節點半徑 var radius = child0.a('radius'); var child00 = child0.getChildAt(0); // 半徑加上孫子節點半徑,避免節點重疊 if (child00) radius += child00.a('radius'); …… }
下面就來看看效果吧~
哈哈,看來咱們分析對了,果真就再也不重疊了,那咱們來看看再多一層節點會是怎麼樣的壯觀場景呢?
哦,NO!這不是我想看到的效果,又重疊了,好討厭。
不要着急,咱們再來仔細分析分析下,在前面,咱們提到過一個名詞——領域半徑,什麼是領域半徑呢?很簡單,就是能夠容納下自身及其全部孩子節點的最小半徑,那麼問題就來了,末端節點的領域半徑爲咱們指定的最小半徑,那麼倒數第二層的領域半徑是多少呢?並非咱們前面計算出來的半徑,而應該加上末端節點自身的領域半徑,由於它們之間存在着包含關係,子節點的領域必須包含於其父親節點的領域中,那咱們在看看上圖,是否是感受末端節點的領域被侵佔了。那麼咱們前面計算出來的半徑表明着什麼呢?前面計算出來的半徑其實表明着孩子節點的佈局半徑,在佈局的時候是經過該半徑來佈局的。
OK,那咱們來總結下,節點的領域半徑是其下每層節點的佈局半徑之和,而佈局半徑須要根據其孩子節點個數及其領域半徑共同決定。
好了,咱們如今知道問題的所在了,那麼咱們的代碼該如何去實現呢?接着往下看:
/** * 就按節點領域半徑及佈局半徑 * @param {ht.Node} root - 根節點對象 * @param {Number} minR - 最小半徑 */ function countRadius(root, minR) { minR = (minR == null ? 25 : minR); // 若果是末端節點,則設置其佈局半徑及領域半徑爲最小半徑 if (!root.hasChildren()) { root.a('radius', minR); root.a('totalRadius', minR); return; } // 遍歷孩子節點遞歸計算半徑 var children = root.getChildren(); children.each(function(child) { countRadius(child, minR); }); var child0 = root.getChildAt(0); // 獲取孩子節點半徑 var radius = child0.a('radius'), totalRadius = child0.a('totalRadius'); // 計算子節點的1/2張角 var degree = Math.PI / children.size(); // 計算父親節點的佈局半徑 var pRadius = totalRadius / Math.sin(degree); // 緩存父親節點的佈局半徑 root.a('radius', pRadius); // 緩存父親節點的領域半徑 root.a('totalRadius', pRadius + totalRadius); // 緩存其孩子節點的佈局張角 root.a('degree', degree * 2); }
在代碼中咱們將節點的領域半徑緩存起來,從下往上一層一層地疊加上去。接下來咱們一塊兒驗證其正確性:
搞定,就是這樣子了,2D拓撲上面的佈局搞定了,那麼接下來該出動3D拓撲啦~
3. 加入z軸座標,呈現3D下的樹狀結構
3D拓撲上面佈局無非就是多加了一個座標系,並且這個座標系只是控制節點的高度而已,並不會影響到節點之間的重疊,因此接下來咱們來改造下咱們的程序,讓其可以在3D上正常佈局。
也不須要太大的改造,咱們只須要修改下佈局器而且將2D拓撲組件改爲3D拓撲組件就能夠了。
/** * 佈局樹 * @param {ht.Node} root - 根節點 */ function layout(root) { // 獲取到全部的孩子節點對象數組 var children = root.getChildren().toArray(); // 獲取孩子節點個數 var len = children.length; // 計算張角 var degree = root.a('degree'); // 根據三角函數計算繞父親節點的半徑 var r = root.a('radius'); // 獲取父親節點的位置座標 var rootPosition = root.p3(); children.forEach(function(child, index) { // 根據三角函數計算每一個節點相對於父親節點的偏移量 var s = Math.sin(degree * index), c = Math.cos(degree * index), x = s * r, z = c * r; // 設置孩子節點的位置座標 child.p3(x + rootPosition[0], rootPosition[1] - 100, z + rootPosition[2]); // 遞歸調用佈局孩子節點 layout(child); }); }
上面是改形成3D佈局後的佈局器代碼,你會發現和2D的佈局器代碼就差一個座標系的的計算,其餘的都同樣,看下在3D上佈局的效果:
恩,有模有樣的了,在文章的開頭,咱們能夠看到每一層的節點都有不一樣的顏色及大小,這些都是比較簡單,在這裏我就不作深刻的講解,具體的代碼實現以下:
var level = 4, size = (level + 1) * 20; var root = createNode(dataModel); root.setName('root'); root.p(100, 100); root.s('shape3d', 'sphere'); root.s('shape3d.color', randomColor()); root.s3(size, size, size); var colors = {}, sizes = {}; createTreeNodes(dataModel, root, level - 1, 5, function(data, level, num) { if (!colors[level]) { colors[level] = randomColor(); sizes[level] = (level + 1) * 20; } size = sizes[level]; data.setName('item-' + level + '-' + num); // 設置節點形狀爲球形 data.s('shape3d', 'sphere'); data.s('shape3d.color', colors[level]); data.s3(size, size, size); });
在這裏引入了一個隨機生成顏色值的方法,對每一層隨機生成一種顏色,並將節點的形狀改爲了球形,讓頁面看起來美觀些(其實很醜)。
提個外話,節點上能夠貼上圖片,還能夠設置文字的朝向,能夠根據用戶的視角動態調整位置,等等一系列的拓展,這些你們均可以去嘗試,相信均可以作出一個很漂亮的3D樹出來。
到此,整個Demo的製做就結束了,今天的篇幅有些長,感謝你們的耐心閱讀,在設計上或則是表達上有什麼建議或意見歡迎你們提出,點擊這裏能夠訪問HT for Web官網上的手冊。