在HT for Web中2D和3D应用都支持树状结构数据的展示,展现效果各异,2D上的树状结构在展现层级关系明显,但是如果数据量大的话,看起来就没那么直观,找到指定的节点比较困难,而3D上的树状结构在展现上配合HT for Web的弹力布局组件会显得比较直观,一眼望去可以把整个树状结构数据看个大概,但是在弹力布局的作用下,其层次结构看得就不是那么清晰了。所以这时候结构清晰的3D树的需求就来了,那么这个3D树具体长成啥样呢,我们来一起目睹下~
要实现这样的效果,该从何下手呢?接下来我们就将这个问题拆解成若干个小问题来解决。
1. 创建一个树状结构
有了解过HT for Web的朋友,对树状结构数据的创建应该都不陌生,在这里我就不做深入的探讨了。树状结构数据的创建很简单,在这里为了让代码更简洁,我封装了三个方法来创建树状结构数据,具体代码如下:
/** * 创建连线 * @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循环来创建树状结构数据,在这里我就不多说了,接下来我们来探究第二个问题。
2. 在2D拓扑下模拟3D树状结构每层的半径计算
在3D下的树状结构体最大的问题就在于,每个节点的层次及每层节点围绕其父亲节点的半径计算。现在树状结构数据已经有了,那么接下来就该开始计算半径了,我们从两层树状结构开始推算:
我现在先创建了两层的树状结构,所有的子节点是一字排开,并没有环绕其父亲节点,那么我们该如何去确定这些孩子节点的位置呢?
首先我们得知道,每个末端节点都有一圈属于自己的领域,不然节点与节点之间将会存在重叠的情况,所以在这里,我们假定末端节点的领域半径为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