HT for Web中2D和3D应用都支持树状结构数据的展示,展现效果各异,2D上的树状结构在展现层级关系明显,但是如果数据量大的话,看起来就没那么直观,找到指定的节点比较困难,而3D上的树状结构在展现上配合HT for Web的弹力布局组件会显得比较直观,一眼望去可以把整个树状结构数据看个大概,但是在弹力布局的作用下,其层次结构看得就不是那么清晰了。所以这时候结构清晰的3D树的需求就来了,那么这个3D树具体长成啥样呢,我们来一起目睹下~

要实现这样的效果,该从何下手呢?接下来我们就将这个问题拆解成若干个小问题来解决。

1. 创建一个树状结构

有了解过HT for Web的朋友,对树状结构数据的创建应该都不陌生,在这里我就不做深入的探讨了。树状结构数据的创建很简单,在这里为了让代码更简洁,我封装了三个方法来创建树状结构数据,具体代码如下:

  1. /**
  2. * 创建连线
  3. * @param {ht.DataModel} dataModel - 数据容器
  4. * @param {ht.Node} source - 起点
  5. * @param {ht.Node} target - 终点
  6. */
  7. function createEdge(dataModel, source, target) {
  8. // 创建连线,链接父亲节点及孩子节点
  9. var edge = new ht.Edge();
  10. edge.setSource(source);
  11. edge.setTarget(target);
  12. dataModel.add(edge);
  13. }
  14.  
  15. /**
  16. * 创建节点对象
  17. * @param {ht.DataModel} dataModel - 数据容器
  18. * @param {ht.Node} [parent] - 父亲节点
  19. * @returns {ht.Node} 节点对象
  20. */
  21. function createNode(dataModel, parent) {
  22. var node = new ht.Node();
  23. if (parent) {
  24. // 设置父亲节点
  25. node.setParent(parent);
  26.  
  27. createEdge(dataModel, parent, node);
  28. }
  29. // 添加到数据容器中
  30. dataModel.add(node);
  31. return node;
  32. }
  33.  
  34. /**
  35. * 创建结构树
  36. * @param {ht.DataModel} dataModel - 数据容器
  37. * @param {ht.Node} parent - 父亲节点
  38. * @param {Number} level - 深度
  39. * @param {Array} count - 每层节点个数
  40. * @param {function(ht.Node, Number, Number)} callback - 回调函数(节点对象,节点对应的层级,节点在层级中的编号)
  41. */
  42. function createTreeNodes(dataModel, parent, level, count, callback) {
  43. level--;
  44. var num = (typeof count === 'number' ? count : count[level]);
  45.  
  46. while (num--) {
  47. var node = createNode(dataModel, parent);
  48. // 调用回调函数,用户可以在回调里面设置节点相关属性
  49. callback(node, level, num);
  50. if (level === 0) continue;
  51. // 递归调用创建孩子节点
  52. createTreeNodes(dataModel, node, level, count, callback);
  53. }
  54. }

嘿嘿,代码写得可能有些复杂了,简单的做法就是嵌套几个for循环来创建树状结构数据,在这里我就不多说了,接下来我们来探究第二个问题。

2. 在2D拓扑下模拟3D树状结构每层的半径计算

在3D下的树状结构体最大的问题就在于,每个节点的层次及每层节点围绕其父亲节点的半径计算。现在树状结构数据已经有了,那么接下来就该开始计算半径了,我们从两层树状结构开始推算:

我现在先创建了两层的树状结构,所有的子节点是一字排开,并没有环绕其父亲节点,那么我们该如何去确定这些孩子节点的位置呢?

首先我们得知道,每个末端节点都有一圈属于自己的领域,不然节点与节点之间将会存在重叠的情况,所以在这里,我们假定末端节点的领域半径为25,那么两个相邻节点之间的最短距离将是两倍的节点领域半径,也就是50,而这些末端节点将均匀地围绕在其父亲节点四周,那么相邻两个节点的张角就可以确认出来,有了张角,有了两点间的距离,那么节点绕其父亲节点的最短半径也就能计算出来了,假设张角为a,两点间最小距离为b,那么最小半径r的计算公式为:

r = b / 2 / sin(a / 2);

那么接下来我么就来布局下这个树,代码是这样写的:

  1. /**
  2. * 布局树
  3. * @param {ht.Node} root - 根节点
  4. * @param {Number} [minR] - 末端节点的最小半径
  5. */
  6. function layout(root, minR) {
  7. // 设置默认半径
  8. minR = (minR == null ? 25 : minR);
  9. // 获取到所有的孩子节点对象数组
  10. var children = root.getChildren().toArray();
  11. // 获取孩子节点个数
  12. var len = children.length;
  13. // 计算张角
  14. var degree = Math.PI * 2 / len;
  15. // 根据三角函数计算绕父亲节点的半径
  16. var sin = Math.sin(degree / 2),
  17. r = minR / sin;
  18. // 获取父亲节点的位置坐标
  19. var rootPosition = root.p();
  20.  
  21. children.forEach(function(child, index) {
  22. // 根据三角函数计算每个节点相对于父亲节点的偏移量
  23. var s = Math.sin(degree * index),
  24. c = Math.cos(degree * index),
  25. x = s * r,
  26. y = c * r;
  27.  
  28. // 设置孩子节点的位置坐标
  29. child.p(x + rootPosition.x, y + rootPosition.y);
  30. });
  31. }

在代码中,你会发现我将末端半径默认设置为25了,如此,我们通过调用layout()方法就可以对结构树进行布局了,其布局效果如下:

从效果图可以看得出,末端节点的默认半径并不是很理想,布局出来的效果连线都快看不到了,因此我们可以增加末端节点的默认半径来解决布局太密的问题,如将默认半径设置成40的效果图如下:

现在两层的树状分布解决了,那么我们来看看三层的树状分布该如何处理。

将第二层和第三层看成一个整体,那么其实三层的树状结构跟两层是一样的,不同的是在处理第二层节点时,应该将其看做一个两层的树状结构来处理,那么像这种规律的处理用递归最好不过了,因此我们将代码稍微该着下,在看看效果如何:

不行,节点都重叠在一起了,看来简单的递归是不行的,那么具体的问题出在哪里呢?

仔细分析了下,发现父亲节点的领域半径是由其孩子节点的领域半径决定的,因此在布局时需要知道自身节点的领域半径,而且节点的位置取决于父亲节点的领域半径及位置信息,这样一来就无法边计算半径边布局节点位置了。

那么现在只能将半径的计算和布局分开来,做两步操作了,我们先来分析下节点半径的计算:

首先需要明确最关键的条件,父亲节点的半径取决于其孩子节点的半径,这个条件告诉我们,只能从下往上计算节点半径,因此我们设计的递归函数必须是先递归后计算,废话不多说,我们来看下具体的代码实现:

  1. /**
  2. * 就按节点领域半径
  3. * @param {ht.Node} root - 根节点对象
  4. * @param {Number} minR - 最小半径
  5. */
  6. function countRadius(root, minR) {
  7. minR = (minR == null ? 25 : minR);
  8.  
  9. // 若果是末端节点,则设置其半径为最小半径
  10. if (!root.hasChildren()) {
  11. root.a('radius', minR);
  12. return;
  13. }
  14.  
  15. // 遍历孩子节点递归计算半径
  16. var children = root.getChildren();
  17. children.each(function(child) {
  18. countRadius(child, minR);
  19. });
  20.  
  21. var child0 = root.getChildAt(0);
  22. // 获取孩子节点半径
  23. var radius = child0.a('radius');
  24.  
  25. // 计算子节点的1/2张角
  26. var degree = Math.PI / children.size();
  27. // 计算父亲节点的半径
  28. var pRadius = radius / Math.sin(degree);
  29.  
  30. // 设置父亲节点的半径及其孩子节点的布局张角
  31. root.a('radius', pRadius);
  32. root.a('degree', degree * 2);
  33. }

OK,半径的计算解决了,那么接下来就该解决布局问题了,布局树状结构数据需要明确:孩子节点的坐标位置取决于其父亲节点的坐标位置,因此布局的递归方式和计算半径的递归方式不同,我们需要先布局父亲节点再递归布局孩子节点,具体看看代码吧:

  1. /**
  2. * 布局树
  3. * @param {ht.Node} root - 根节点
  4. */
  5. function layout(root) {
  6. // 获取到所有的孩子节点对象数组
  7. var children = root.getChildren().toArray();
  8. // 获取孩子节点个数
  9. var len = children.length;
  10. // 计算张角
  11. var degree = root.a('degree');
  12. // 根据三角函数计算绕父亲节点的半径
  13. var r = root.a('radius');
  14. // 获取父亲节点的位置坐标
  15. var rootPosition = root.p();
  16.  
  17. children.forEach(function(child, index) {
  18. // 根据三角函数计算每个节点相对于父亲节点的偏移量
  19. var s = Math.sin(degree * index),
  20. c = Math.cos(degree * index),
  21. x = s * r,
  22. y = c * r;
  23.  
  24. // 设置孩子节点的位置坐标
  25. child.p(x + rootPosition.x, y + rootPosition.y);
  26.  
  27. // 递归调用布局孩子节点
  28. layout(child);
  29. });
  30. }

代码写完了,接下来就是见证奇迹的时刻了,我们来看看效果图吧:

不对呀,代码应该是没问题的呀,为什么显示出来的效果还是会重叠呢?不过仔细观察我们可以发现相比上个版本的布局会好很多,至少这次只是末端节点重叠了,那么问题出在哪里呢?

不知道大家有没有发现,排除节点自身的大小,倒数第二层节点与节点之间的领域是相切的,那么也就是说节点的半径不仅和其孩子节点的半径有关,还与其孙子节点的半径有关,那我们把计算节点半径的方法改造下,将孙子节点的半径也考虑进去再看看效果如何,改造后的代码如下:

  1. /**
  2. * 就按节点领域半径
  3. * @param {ht.Node} root - 根节点对象
  4. * @param {Number} minR - 最小半径
  5. */
  6. function countRadius(root, minR) {
  7. ……
  8.  
  9. var child0 = root.getChildAt(0);
  10. // 获取孩子节点半径
  11. var radius = child0.a('radius');
  12.  
  13. var child00 = child0.getChildAt(0);
  14. // 半径加上孙子节点半径,避免节点重叠
  15. if (child00) radius += child00.a('radius');
  16.  
  17. ……
  18. }

下面就来看看效果吧~

哈哈,看来我们分析对了,果然就不再重叠了,那我们来看看再多一层节点会是怎么样的壮观场景呢?

哦,NO!这不是我想看到的效果,又重叠了,好讨厌。

不要着急,我们再来仔细分析分析下,在前面,我们提到过一个名词——领域半径,什么是领域半径呢?很简单,就是可以容纳下自身及其所有孩子节点的最小半径,那么问题就来了,末端节点的领域半径为我们指定的最小半径,那么倒数第二层的领域半径是多少呢?并不是我们前面计算出来的半径,而应该加上末端节点自身的领域半径,因为它们之间存在着包含关系,子节点的领域必须包含于其父亲节点的领域中,那我们在看看上图,是不是感觉末端节点的领域被侵占了。那么我们前面计算出来的半径代表着什么呢?前面计算出来的半径其实代表着孩子节点的布局半径,在布局的时候是通过该半径来布局的。

OK,那我们来总结下,节点的领域半径是其下每层节点的布局半径之和,而布局半径需要根据其孩子节点个数及其领域半径共同决定。

好了,我们现在知道问题的所在了,那么我们的代码该如何去实现呢?接着往下看:

  1. /**
  2. * 就按节点领域半径及布局半径
  3. * @param {ht.Node} root - 根节点对象
  4. * @param {Number} minR - 最小半径
  5. */
  6. function countRadius(root, minR) {
  7. minR = (minR == null ? 25 : minR);
  8.  
  9. // 若果是末端节点,则设置其布局半径及领域半径为最小半径
  10. if (!root.hasChildren()) {
  11. root.a('radius', minR);
  12. root.a('totalRadius', minR);
  13. return;
  14. }
  15.  
  16. // 遍历孩子节点递归计算半径
  17. var children = root.getChildren();
  18. children.each(function(child) {
  19. countRadius(child, minR);
  20. });
  21.  
  22. var child0 = root.getChildAt(0);
  23. // 获取孩子节点半径
  24. var radius = child0.a('radius'),
  25. totalRadius = child0.a('totalRadius');
  26.  
  27. // 计算子节点的1/2张角
  28. var degree = Math.PI / children.size();
  29. // 计算父亲节点的布局半径
  30. var pRadius = totalRadius / Math.sin(degree);
  31.  
  32. // 缓存父亲节点的布局半径
  33. root.a('radius', pRadius);
  34. // 缓存父亲节点的领域半径
  35. root.a('totalRadius', pRadius + totalRadius);
  36. // 缓存其孩子节点的布局张角
  37. root.a('degree', degree * 2);
  38. }

在代码中我们将节点的领域半径缓存起来,从下往上一层一层地叠加上去。接下来我们一起验证其正确性:

搞定,就是这样子了,2D拓扑上面的布局搞定了,那么接下来该出动3D拓扑啦~

3. 加入z轴坐标,呈现3D下的树状结构

3D拓扑上面布局无非就是多加了一个坐标系,而且这个坐标系只是控制节点的高度而已,并不会影响到节点之间的重叠,所以接下来我们来改造下我们的程序,让其能够在3D上正常布局。

也不需要太大的改造,我们只需要修改下布局器并且将2D拓扑组件改成3D拓扑组件就可以了。

  1. /**
  2. * 布局树
  3. * @param {ht.Node} root - 根节点
  4. */
  5. function layout(root) {
  6. // 获取到所有的孩子节点对象数组
  7. var children = root.getChildren().toArray();
  8. // 获取孩子节点个数
  9. var len = children.length;
  10. // 计算张角
  11. var degree = root.a('degree');
  12. // 根据三角函数计算绕父亲节点的半径
  13. var r = root.a('radius');
  14. // 获取父亲节点的位置坐标
  15. var rootPosition = root.p3();
  16.  
  17. children.forEach(function(child, index) {
  18. // 根据三角函数计算每个节点相对于父亲节点的偏移量
  19. var s = Math.sin(degree * index),
  20. c = Math.cos(degree * index),
  21. x = s * r,
  22. z = c * r;
  23.  
  24. // 设置孩子节点的位置坐标
  25. child.p3(x + rootPosition[0], rootPosition[1] - 100, z + rootPosition[2]);
  26.  
  27. // 递归调用布局孩子节点
  28. layout(child);
  29. });
  30. }

上面是改造成3D布局后的布局器代码,你会发现和2D的布局器代码就差一个坐标系的的计算,其他的都一样,看下在3D上布局的效果:

恩,有模有样的了,在文章的开头,我们可以看到每一层的节点都有不同的颜色及大小,这些都是比较简单,在这里我就不做深入的讲解,具体的代码实现如下:

  1. var level = 4,
  2. size = (level + 1) * 20;
  3.  
  4. var root = createNode(dataModel);
  5. root.setName('root');
  6. root.p(100, 100);
  7.  
  8. root.s('shape3d', 'sphere');
  9. root.s('shape3d.color', randomColor());
  10. root.s3(size, size, size);
  11.  
  12. var colors = {},
  13. sizes = {};
  14. createTreeNodes(dataModel, root, level - 1, 5, function(data, level, num) {
  15. if (!colors[level]) {
  16. colors[level] = randomColor();
  17. sizes[level] = (level + 1) * 20;
  18. }
  19.  
  20. size = sizes[level];
  21.  
  22. data.setName('item-' + level + '-' + num);
  23. // 设置节点形状为球形
  24. data.s('shape3d', 'sphere');
  25. data.s('shape3d.color', colors[level]);
  26. data.s3(size, size, size);
  27. });

在这里引入了一个随机生成颜色值的方法,对每一层随机生成一种颜色,并将节点的形状改成了球形,让页面看起来美观些(其实很丑)。

提个外话,节点上可以贴上图片,还可以设置文字的朝向,可以根据用户的视角动态调整位置,等等一系列的拓展,这些大家都可以去尝试,相信都可以做出一个很漂亮的3D树出来。

到此,整个Demo的制作就结束了,今天的篇幅有些长,感谢大家的耐心阅读,在设计上或则是表达上有什么建议或意见欢迎大家提出,点击这里可以访问HT for Web官网上的手册

基于HTML5的3D网络拓扑树呈现的更多相关文章

  1. 基于HTML5的3D网络拓扑自动布局

    上篇将HT for Web的3D拓扑弹力布局的算法运行在Web Workers后台(http://www.hightopo.com/blog/70.html),这篇我们将进一步折腾,将算法运行到真正的 ...

  2. 基于HTML5实现3D热图Heatmap应用

    Heatmap热图通过众多数据点信息,汇聚成直观可视化颜色效果,热图已广泛被应用于气象预报.医疗成像.机房温度监控等行业,甚至应用于竞技体育领域的数据分析. http://www.hightopo.c ...

  3. 基于html5制作3D拳击游戏源码下载

    今天给大家分享一款基于HTML5实现的3d拳王游戏源码.这款实例适用浏览器:360.FireFox.Chrome.Safari.Opera.傲游.搜狗.世界之窗. 不支持IE8及以下浏览器. 在线预览 ...

  4. 基于 HTML5 的 3D 工业互联网展示方案

    前言 通用电气(GE).IBM.英特尔等公司主推的“工业互联网”正在经历“产品-数据分析平台-应用-生态”的演进.这主要得益于 Predix 数据分析平台对工业互联网应用的整合能力.Predix 就像 ...

  5. 基于HTML5实现3D监控应用流动效果

    http://www.hightopo.com/guide/guide/core/lighting/examples/example_flowing.html 流动效果在3D领域有着广泛的应用场景,如 ...

  6. 基于HTML5气3D仿真培训系统

    根据最近的上线HTML5的燃气3D培训仿真系统.曾经的老系统是採用基于C++和OpenGL的OpenSceneGraph引擎设计的,OSG引擎性能和渲染效果各方面还是不错的,但由于这次新产品需求要求能 ...

  7. 基于 HTML5 的 3D 工控隧道案例

    隧道的项目我目前是第一次接触,感觉做起来的效果还蛮赞的,所以给大家分享一下.这个隧道项目的主要内容包括:照明.风机.车道指示灯.交通信号灯.情报板.消防.火灾报警.车行横洞.风向仪.COVI.微波车检 ...

  8. 基于HTML5技术的电力3D监控应用(二)

    上篇介绍了我们电力项目的基本情况,我们选用HTML5技术还是顶着很大压力,毕竟HTML5技术性能行不行,浏览器兼容性会不会有问题,这些在项目选型阶段还是充满疑惑,项目做到现在终于快收尾了我们才敢松口气 ...

  9. 基于 HTML5 Canvas 的 3D 渲染引擎构建机架式服务器

    前言 今天找到了 HT 的官网里的 Demo 网站( http://www.hightopo.com/demos/index.html ),看的我眼花缭乱,目不暇接. 而且 HT 的用户手册,将例子和 ...

随机推荐

  1. Jexus web server V5.4.5 已经发布

    Jexus 是运行于 Linux/FreeBSD 平台的一款以支持 ASP.NET 为主要特色的,同时非常重视安全性和稳定性的高性能 WEB 服务器.最新版 5.4.5 已经发布,官方网站是:www. ...

  2. 可在广域网部署运行的QQ高仿版 -- GG叽叽V3.0,完善基础功能(源码)

    (前段时间封闭式开发完了一个项目,最近才有时间继续更新GG的后续版本,对那些关注GG的朋友来说,真的是很抱歉.)GG的前面几个版本开发了一些比较高级的功能,像视频聊天.远程桌面.文件传送.远程磁盘等, ...

  3. 写了一个简单的NodeJS实现的进程间通信的例子

    1. cluster介绍 大家都知道nodejs是一个单进程单线程的服务器引擎,不管有多么的强大硬件,只能利用到单个CPU进行计算.所以,有人开发了第三方的cluster,让node可以利用多核CPU ...

  4. 原创教程:《metasploit新手指南》介绍及下载

    原创教程:<metasploit新手指南>介绍及下载 1.1 作者简介 这份教程并不是“玄魂工作室”原创,但是我还是要力推给大家.相比那些一连几年都在问“我怎么才能入门”的人而言,我们更欣 ...

  5. 为Angularjs ngOptions加上index解决方案

    今天在Angularjs交流群中有位童学问道如何为Angular select的ngOptions像Angularjs的ngRepeat一样加上一个索引$index. 其实对于这个问题来说Angula ...

  6. 两段for循环代码的区别

    第一段: #include<iostream>using namespace std;int main(){ int a; int x; for(a=0,x=0;a<=1&& ...

  7. PosePlus的第一次突破

    动作问题一定要解决 PosePlus是解决这个问题的库 (之前叫做CleanData.Ani,后来我想过,之前的设计各个库之间的依赖太重了) 现在把他们之间的依赖剥开.   PosePlus解决几个问 ...

  8. Unity3D核心类型一览

    Unity3D核心类型一览 本文记录了Unity3D的最基本的核心类型.包括Object.GameObject.Component.Transform.Behaviour.Renderer.Colli ...

  9. 一个老菜鸟所理解的UX及产品流

    从事前端开发到目前为止已经有4年多的时间了,从一个小菜鸟一路依靠自学,到目前总算一个老菜鸟了.当然了,从事前端的工作,是免不了要对产品以及用户体验有些许了解的.最近谈论起这方面的内容,就按照自己的想法 ...

  10. iOS Crash常规跟踪方法及Bugly集成运用

    当app出现崩溃, 研发阶段一般可以通过以下方式来跟踪crash信息 #1.模拟器运行, 查看xcode错误日志 #2.真机调试, 查看xcode错误日志 #3.真机运行, 查看device系统日志 ...