非层级树可视化布局算法

404 阅读6分钟

基本介绍

本文介绍的是一种非层级树布局算法,我们这里的树布局算法并不是像在数据结构中学到的那种二叉树的概念,利用二叉树的结构和值,不考虑节点的大小。

图片.png

我们这里的节点可以是不同大小,不同形状的节点,在这样的条件下,我们的布局算法能够更聪明地利用空间,最大化密度和对称性。所以本文介绍的这种布局算法更具有通用性
图片.png
首先,我们需要先了解什么是层级布局算法,和我们介绍的非层级算法有什么区别,我们先看一张对比图

图片.png
层级树布局的特点是对于深度相同的树节点会被放在同一层上,得到的布局结构会有很明显的层级结构特点,但是某一层级的某个节点的高度非常高,这样对于下一层所有的节点都必须要适应这个节点的高度,这样就会导致在垂直方向有着很大的空间浪费,而非层级布局就是为了解决这样的问题
但同时又会带来新的性能问题,层级树布局因为有层级的概念,同一层级的节点的y方向的坐标都是相同的,所以层级树布局能在线性时间完成,而非层级布局则需要很多计算,很难在线性时间内完成。在2013年之前,提出的很多非层级布局的算法都不能在线性时间内完成或者不能完整证明。
在2013年 A.J van der Ploeg 提出的论文 Drawing Non-layered TIdy Trees in Linear Time,在线性时间完成非层级树布局并且给出了完整的证明。

图片.png 本文会介绍其算法思想,并在文章末尾会给出基于Javascript实现的算法

算法思想

第 1 步:初始化,为每个节点添加x=0,y=父节点y坐标+父节点的高度还有一些辅助的属性,以树的后序遍历算法完成初始化。

图片.png
例如左边这棵树的结构,我们初始化之后的结果如右图所示,初始化之后蓝色节点所有的子节点的高度是相同的,x坐标都为0,所以子节点都是重合在一起的,所以我们下一步就需要去移动节点,消除重叠 第 2 步:如果节点没有左兄弟节点,则将其向右移动直到不发生重叠。

图片.png
我们可以看到节点8虽然发生了重叠但是它没有左兄弟节点所以不需要移动,而节点9,有左兄弟节点8所以向右移动到不发生重叠
第 3 步:计算父节点的相对位置。在子节点8,9移动完后,我们就可以根据子节点的位置计算出父节点7的x。node7.x = (node8.x + node8.width+node9.width)/2 虽然计算出了节点7的x坐标,但是我们发现节点7,8,9还是重叠在一起,但是现在节点7,8,9之间的相对位置是确定了的,我们去整体移动就可以了。
第 4 步:合并轮廓。在整体移动之前,我们还需要了解一个轮廓的概念,这也是算法非常巧妙的一个思想。
还是上面那张图,我们确定好节点7的相对位置后,节点7,8,9就可以看成一个整体,轮廓可以分为左轮廓和右轮廓两种。对于节点7,8,9来说左轮廓就是节点7,8而右轮廓为7,9,8。

class IYL(lowY, index, next) {
  this.lowY = lowY
  this.index = index
  this.next = next
}

利用链表管理轮廓。
第 5 步:整体移动。对于节点7而言,有左兄弟节点3,所以需要向右移动相除重叠,节点7的子节点也需要整体一起移动。
如何紧凑,快速,正确移动到合适的位置,是重点与难点,要保持整体布局算法在线性时间内完成,就需要对于每个节点的操作都在常数时间内完成。这也是在此之间其它算法不能保证线性时间的问题所在。

图片.png
根据我们前面的介绍,我们通过树的后序遍历来计算节点的位置,所以对于节点7的左兄弟节点已经是确定好位置了,而节点7,8,9需要整体移动。我们维护已经确定好位置的节点的右轮廓,和要移动的整体的左轮廓。
如上图,左轮廓是7,8.而右轮廓是3,6,4,5。我们就需要根据两个轮廓从上往下,依次对比如3->7,6->7,6->8,计算出保证左轮廓一定在右轮廓右边的最小移动距离

图片.png 图片.png
移动完成后,更新已经确定好位置的右轮廓。所以完成移动后右轮廓从3,6,5,5变为了7,9,8,4,5
直到完成所有的节点位置计算。
这里只介绍了节点算法的思想,具体的代码分析,可以根据文章末尾提供的代码Debug分析。

算法过程模拟

以这个树为例,通过一步一步模拟整个算法的计算过程,帮助理解算法。

图片.png
首先通过初始化所有节点x=0,y=height。我们通过递归地初始化,计算的height已经是节点最终的y坐标。
对于上面的例子后序遍历这棵树的结果为1,2,5,4,6,3,8,9,7这也是我们处理节点的顺序 节点1没有左兄弟所有x=0,不需要移动。 节点2起初x=0向右移动x=node1.width。 节点5没有左兄弟节点,暂时x=0,不用担心后序会由父节点向右整体移动。 节点4由于只有一个子节点所有确定节点5的坐标后就可以确定节点4的坐标了 x=node5.x+(node5.width-node4.width)/2。目前我们得到了这样的效果。

图片.png图片.png图片.png
节点6是叶子节点所以相对左兄弟节点4移动就可以了x=node4.x+node4.width 节点3也是同理,子节点4,6已经确定,就可以计算出节点3的坐标,计算过程和上面类似。
这里节点3因为有左兄弟节点2,所以需要整体移动节点3,4,5,6
同理计算节点7,8,9。然后整体移动后,根据节点1,2,3,7确定根节点0的位置完成最终布局。