初探树状结构
/** * 创建连线 * @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循环去搞定它。不过你知道吗?其实还有个小窍门可以简化这个步骤,那就是:怎么认准树状结构里每个孩子节点在哪?
默认半径设置
书里面给每个末尾角色设定了个默认的25半径,然后叫了下layout()方法来排排队。可是看图文效果好像不太行,末尾那个地方太挤了,都看不到线在哪儿。你要是觉得这样不行,那就试试把默认的半径变大点看看。
/** * 布局树 * @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); }); }
优化布局效果
f13d9a8c1530b97371a5a1ae44092881
我们把圆圈大小设定为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); }
布局优化与挑战
咱们布置树型数据结构时,小孩子们(子节点)得靠爸爸们(父节点)来摆位子。先搞定爹地们,再挨个儿安顿小孩们。比以前强多,但还是会有些小尾巴(末端节点)搞混了。
/** * 布局树 * @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); }); }
考虑孙子节点影响
我们发现倒数第二层和最底层之间有个相切,这让人想到还得去考虑孙子节点对父亲节点领域的影响呀(这样才能更精准地计算出来)。换了算术之后才明白要把每个节点最后那个点自己的领域直径也加进去!
总结优化策略
简单来说,每个节点的”势力范围”就是它下面所有小节点集合起来的大小,这个区域可以通过简单计算孩子节点数和自己本身的半径结合来判断出来。这样,我们就能找到最棒的树状数据结构!
3D拓扑结构优化
/** * 就按节点领域半径 * @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'); …… }
3D架构在布置时多了高度坐标,不影响重叠问题。我们把程序改了下,让3D空间里的树状结构可以正常显示出来,还能让每个层次的节点有自己的颜色和大小!
拓展与应用
除了基础功能,我们还能为这个树形结构数据添点儿料,比如给它加个图像,调整文字的方向,甚至是移动位置啥的,这样做了以后,3D树形数据会变得更炫更好看哟。
评论0