其他资源: 1.完整的树状图布局代码
效果展示
目标:将树结构数据绘制成一个树的图,树结构数据大概是这个样子: 实现:传统javascript语言+html的canvas实现 说明一下,其实网络上的关于树结构的数据有很多种类,这里我只选取了一种类型的数据,如果想要直接使用我这里的算法,可以自己尝试调整数据类型或者调整对应的算法。后面我还针对另一种数据类型做了相应的调整,也就是说完整的代码可以应对两种格式的树结构数据。
let data = [
{
"id": "0",
"name": "O",
"fatherId": "-1",
},
{
"id": "1",
"name": "E",
"fatherId": "0",
},
{
"id": "2",
"name": "F",
"fatherId": "0",
},
{
"id": "3",
"name": "N",
"fatherId": "0",
},
{
"id": "4",
"name": "A",
"fatherId": "1",
},
{
"id": "5",
"name": "D",
"fatherId": "1",
}
];
data.push({
"id":String(data.length),
"name":"G",
"fatherId":"3",
})
data.push({
"id":String(data.length),
"name":"M",
"fatherId":"3",
})
data.push({
"id":String(data.length),
"name":"B",
"fatherId":"5",
})
data.push({
"id":String(data.length),
"name":"C",
"fatherId":"5",
})
data.push({
"id":String(data.length),
"name":"H",
"fatherId":"7",
})
data.push({
"id":String(data.length),
"name":"I",
"fatherId":"7",
})
data.push({
"id":String(data.length),
"name":"J",
"fatherId":"7",
})
data.push({
"id":String(data.length),
"name":"K",
"fatherId":"7",
})
data.push({
"id":String(data.length),
"name":"L",
"fatherId":"7",
})
树状图展示:
绘制原则
根据Donald Knuth在论文中的叙述,我们可明白树状图的绘制基本要满足两个原则: 原则1:树的边不应该相互交叉。 原则2:相同深度的所有节点应绘制在同一水平线上。这有助于明确树的结构。
主要难点
在计算规模上,对于一个树的操作肯定是要面对迭代和回溯的,而像这样的对树进行布局的算法,可以说的上是一个NPC问题。 同事我们也注意到,通过所谓“完美”的算法绘制出来的树也是有缺陷的,例如:在面对大规模的数据时,会面临屏幕无法全部显示的现象,同时如果树的高度太高,也会发现容易出现你看了下面忘记上面的现象。这里只是提一下,对于这些问题的解决有兴趣可以自己探索。
算法流程1——FirstWalk
本算法主要依靠了《A Node-Positioning Algorithm for General Trees》这篇文献上的算法。 文献变量说明:
在文献或者代码上可能遇到的变量我尽量会在这个地方说明:
id
name
fatherId
x、y
child
level
prelim
modifier
rightSiblingId
leftsiblingId
leftNeighbor
论文中主要完成了两个递归,即FIRSTWALK和SECONDWALK。 firstwalk主要是完成树中节点的prelim,我们可以基本认为最后的坐标是prelim加上一些modify的偏移: firstwalk中的计算逻辑为
1.当前节点是叶节点无左兄弟,prelim记0 2.当前节点是叶节点有左兄弟,prelim是左兄弟prelim加上宽度,即根据左兄弟定位; 3.当前节点非叶节点无左兄弟,prelim是第一个子节点prelim加上最后一个子节点除以二,即根据子节点定位 4.当前节点非叶节点有左兄弟,prelim记左兄弟相对位置,modifier记prelim相对子节点定位修正值,使用apportion修正子孙节点定位
代码部分如下:
function firstWalk(node){
var space = 40;
var nodeSize = 20;
if(node.children == null)
{
if(node.leftsiblingId!=null)
{
node.prelim = data[node.leftsiblingId].prelim + space + nodeSize;
}
else
{
node.prelim = 0 ;
}
}
else
{
var leftPrelim = 0;
var rightPrelim =0;
var nodeChild = null
nodeChild = node.children[0];
firstWalk(nodeChild);
leftPrelim = node.children[0].prelim;
while(nodeChild.rightSiblingId!=null&&nodeChild.rightSiblingId!=undefined)
{
nodeChild = data[nodeChild.rightSiblingId]
firstWalk(nodeChild);
}
rightPrelim = nodeChild.prelim;
var midPoint = (leftPrelim + rightPrelim)/2
if(node.leftsiblingId!=null)
{
node.prelim = data[node.leftsiblingId].prelim +space +nodeSize;
node.modifier = node.prelim - midPoint;
apportion(node);
}
else
{
node.prelim = midPoint;
}
}
}
算法流程2——SecondWalk
secondwalk只是对初始x值加上所有父亲节点的modifier修正值,算得最终x坐标,y坐标可以根据深度直接算出。具体代码如下:
function secondWalk(modSom,node){
if(node!=null)
{
console.log(node.modifier)
node.x = node.prelim + modSom;
node.y = node.level *100+40;
if(node.children!=null)
{
for(var j=0;j<node.children.length;j++)
{
var modifier = 0
if(node.modifier!=null&&node.modifier!=undefined)
modifier =node.modifier
secondWalk(modSom+modifier,node.children[j])
}
}
}
}
apportion函数
而算法中最麻烦的地方在于apportion函数。 apportion函数在firstwalk中,当前节点既有左兄弟又有子节点时发生的一次位置调整,具体逻辑是当子节点定位x坐标大于左兄弟定位x坐标时,当前节点prelim和modifier均累加偏移量。 如果想要具体了解:大家可以看一下这篇博文,作者将apportion阐述的比较清楚
https://blog.csdn.net/zfy920323/article/details/82779764?spm=1001.2014.3001.5501
apportion函数代码为
function apportion(node){
var leftMost = null;
var neighbor = null;
leftMost = node.children[0];
neighbor = data[leftMost.leftNeighbor];
var CompareDepth = 1;
var leftModSum,rightModSum;
while (leftMost!=null&&neighbor!=null)
{
leftModSum = 0.0;
rightModSum = 0.0;
var ancestorLeftmost = null;
var ancestorNeighbor = null;
ancestorLeftmost = leftMost;
ancestorNeighbor = neighbor;
for (var i=0;i<CompareDepth;i++)
{
ancestorLeftmost = data[ancestorLeftmost.fatherId];
ancestorNeighbor = data[ancestorNeighbor.fatherId];
if(ancestorLeftmost==null||ancestorNeighbor==null)
continue;
rightModSum += ancestorLeftmost.modifier;
leftModSum += ancestorNeighbor.modifier;
}
var moveDistance = (neighbor.prelim + leftModSum + 80) - (leftMost.prelim + rightModSum);
if(moveDistance > 0.0)
{
var tempPtr = null;
tempPtr = node;
var leftsibling = 0;
while(tempPtr!=null &&ancestorNeighbor!=null&& tempPtr.id!=ancestorNeighbor.id)
{
leftsibling = leftsibling + 1;
tempPtr = data[tempPtr.leftsiblingId];
}
if(tempPtr!=null)
{
var portion = moveDistance/leftsibling;
tempPtr = node;
while(tempPtr!=null &&ancestorNeighbor!=null&&tempPtr!=ancestorNeighbor)
{
tempPtr.prelim = tempPtr.prelim + moveDistance;
tempPtr.modifier = tempPtr.modifier + moveDistance;
moveDistance = moveDistance - portion;
tempPtr = data[tempPtr.leftsiblingId];
}
}
else
{
}
}
CompareDepth += 1;
if(leftMost.children==null)
{
leftMost = getLeftMost(node,0,CompareDepth)
}
else
leftMost = leftMost.children[0];
if (leftMost!=null&&leftMost.leftNeighbor!=null)
neighbor = data[leftMost.leftNeighbor]
}
}
通过canvas绘制节点和线
这里主要是通过canvas绘图,如果想用svg绘制也是可以,直接对js中的canvas中响应位置做出调整即可,这里也是直接给出代码
function NodeInit(x,y,size,nodeText,nodeType){
var c = document.getElementById('myCanvas')
var ctx1=c.getContext("2d");
ctx1.fillStyle = "rgb(7,105,173)";
if(nodeType=="Root")
{
ctx1.fillRect(x-size/2,y-size/2,size,size);
ctx1.globalCompositeOperation = "destination-over";
ctx1.font="15px Arial";
ctx1.fillText(nodeText, x+size/2+5, y+size/2-5);
}
else if(nodeType=="Node")
{
ctx1.beginPath();
ctx1.arc(x,y,size,0,Math.PI*2);
ctx1.closePath();
ctx1.fill();
ctx1.font="15px Arial";
ctx1.fillText(nodeText, x+size+5, y+size/2+5);
}
}
function EdgInit(beginX,beginY,endX,endY){
var ctx1 = document.getElementById("myCanvas").getContext("2d");
ctx1.beginPath();
ctx1.moveTo(beginX, beginY);
ctx1.lineTo(endX,endY);
ctx1.lineWidth=1;
ctx1.strokeStyle="black";
ctx1.stroke();
ctx1.closePath();
}
function EdgInit2(beginX,beginY,endX,endY)
{
var horizontalSpace =100 ;
var ctx1 = document.getElementById("myCanvas").getContext("2d");
ctx1.beginPath();
ctx1.moveTo(beginX, beginY);
ctx1.lineTo(beginX,beginY-horizontalSpace/2)
ctx1.lineTo(endX,beginY-horizontalSpace/2)
ctx1.lineTo(endX,endY);
ctx1.lineWidth=1;
ctx1.strokeStyle="black";
ctx1.stroke();
ctx1.closePath();
}
最后,这个算法最后也测试了一组一千个节点的数据,速度和显示上不存在任何问题。但是传统树状图在显示上还是有很多问题,比如造成严重的空间浪费等…所以后来有诸多学者提出了其他新颖的算法,去避免树状图在信息传达上的问题,比如径向树状图,空间的占用率更高。
同时关于本文所实现的树状图的完整算法 https://download.csdn.net/download/qq_41296039/21480959
|