樹結構佈局
前言
本文講解如何實現圖形化樹結構佈局。
佈局規則:
- 根節點始終處於畫布中間
- 同級節點不能相互重疊
- 父節點永遠處於子節點的水平中間位置
準備工作
以上圖爲例進行樹結構佈局設計,我們以每個節點的中心位置作爲節點座標。
width
:畫布寬度
nodeWidth
: 節點的寬度
nodeHeight
: 節點的高度,
levelHeight
: 節點間的垂直間距(即每層間的垂直距離)
distance
: 節點間水平間距(即同層節點間的距離)
初始化樹形節點座標
/*
*nodeProps:屬性節點的相關配置屬性
*node:當前節點信息
*parentNode:父節點信息
*
*/
const generatePonint = (nodeProps, node, parentNode) => {
let nodeWidth = nodeProps.nodeWidth; //節點寬度
let nodeHeight = nodeProps.nodeHeight; //節點高度
let levelHeight = nodeProps.levelHeight; //節點與父節點間的間距
let distance = nodeProps.distance; //節點與兄弟節點間的間距
let parentX = nodeProps.width / 2; //父節點橫座標
let parentY = -levelHeight - nodeHeight / 2; //父節點縱座標
let nodeIndex = 0; //節點位於兄弟節點間的位置
let nodeNum = 1; //兄弟節點個數
if (parentNode) {
nodeIndex = parentNode.children.findIndex(item => item.id == node.id);
nodeNum = parentNode.children.length;
parentX = parentNode.x;
parentY = parentNode.y;
}
node.y = parentY + nodeHeight + levelHeight;
node.x = parentX - (nodeNum * nodeWidth + (nodeNum - 1) * distance) / 2 + nodeIndex * (nodeWidth + distance) + nodeWidth / 2;
if (node.children && node.children.length > 0) {
let childLength = node.children.length;
for (let i = 0; i < childLength; i++) {
generatePonint(nodeProps, node.children[i], node);
}
}
return node;
}
同層節點所佔寬度=節點數 * 節點寬度+(節點數-1)*節點水平間距
當前節點所在x軸位置=節點下標 * (節點寬度+節點水平間距) +節點寬度的一半
因爲下標是從0開始計算的所以少算了一個節點位置,其中點即是
nodeWidth / 2
節點相較於左邊線的偏移量=父節點x軸位置-同層節點所佔寬度/2
節點X軸座標=節點偏移量+當前節點x軸所在位置
節點Y軸座標=父節點Y座標+節點高度+節點垂直間距
節點偏移
初始化節點只是理想化的效果,此時只是將節點分層級給出了一個座標,並不能保證節點間是否重複。
因爲所有節點都是基於父節點的位置進行座標計算的,而父節點的水平間距是固定的,那麼當層級越多,相鄰節點就會出現相互覆蓋現象。
公共方法
//獲取當前節點的父節點
const getParentNode = (node, root) => {
//如果當前節點爲根節點直接返回null
if (node.id === root.id) {
return null;
}
if (root.children && root.children.length > 0) {
for (let child of root.children) {
if (child.id === node.id) {
return root;
} else {
let parentNode = getParentNode(node, child);
if (parentNode) {
return parentNode;
}
}
}
}
return null;
}
//獲取相同深度的節點列表
const getDepthNode = (depth, root, list) => {
if (root.depth === depth) {
list.push(root);
return list;
}
if (root.children && root.children.length > 0) {
for (let node of root.children) {
getDepthNode(depth, node, list)
}
}
return list
}
//獲取直系二代祖先節點
const getSameAncestorsNode = (node, nextNode, root) => {
let parentNode = getParentNode(node, root);
let nextParentNode = getParentNode(nextNode, root);
if (parentNode.id == nextParentNode.id) {
return node;
} else {
return getSameAncestorsNode(parentNode, nextParentNode, root);
}
}
//獲取祖宗節點
const getAncestorsNode = (node, root) => {
let parentNode = getParentNode(node, root);
if (parentNode) {
if (parentNode.id == root.id) {
return node;
} else {
let ancestorsNode = getAncestorsNode(parentNode, root);
if (ancestorsNode) {
return ancestorsNode;
}
}
}
}
//獲取兄弟節點
const getBrotherNode = (node, root) => {
let parentNode = getParentNode(node, root);
if (parentNode) {
return parentNode.children;
}
}
可以將樹結構當成血緣譜,相同的祖先節點既是不同的子節點其祖先爲同一人,在樹結構中所有的子節點都是根節點的子孫。
偏移計算
//節點偏移
const offsetPoint = (nodeProps, node, root) => {
let brotherList = getDepthNode(node.depth, root, []);
let nodeDiffer = nodeProps.nodeWidth + nodeProps.distance;
let nodeIndex = brotherList.findIndex(item => item.id === node.id);
let nextNode = brotherList[nodeIndex + 1];
if (nextNode) {
let nextParentNode = getParentNode(nextNode, root);
let parentNode = getParentNode(node, root);
//判斷重疊,只需判斷不同父節點的相鄰2個節點是否在固定節點寬度內
if (nextParentNode && parentNode && nextParentNode.id != parentNode.id && nextNode.x - node.x < nodeDiffer) {
let offsetValue = node.x - nextNode.x + nodeDiffer;//相鄰2個節點需要的偏移量
//查找2重複節點的共同祖先,並在共同祖先下查找當前節點共同祖先的第二代節點
let sameAncestorsNode = getSameAncestorsNode(node, nextNode, root);
if (sameAncestorsNode) {
//將當前節點共同祖先的第二代節點層次偏移,第二代節點及其左邊的節點向左偏移,其他的向右偏移
//例如當前節點的第二代祖先層級爲3,同層座標爲1,那麼層級爲3的0,1節點左移,其他節點右移
offsetNodeAndChildPoint(sameAncestorsNode, root, offsetValue);
//因爲上面已經把底層節點全部移動了,而當前祖先節點位置居中,所以,當前節點的所有父級暫時不動其他父節點對應移動
offsetParentPoint(sameAncestorsNode, root, offsetValue);
//因爲右邊的節點全部右移了,導致位置不居中,所以當前節點及其左邊的節點向左移動(包含子節點)
//offsetParenAndChildtPoint(sameAncestorsNode, root, offsetValue);
}
}
}
// return root;
}
//子節點偏移
const offsetChildPoint = (node, root, offsetValue, pos) => {
if (node.children && node.children.length > 0) {
for (let child of node.children) {
if (pos == "left") {
child.x = child.x - offsetValue / 2;
offsetChildPoint(child, root, offsetValue, pos)
} else {
child.x = child.x + offsetValue / 2;
offsetChildPoint(child, root, offsetValue, pos)
}
}
}
}
//節點及其子節點偏移
const offsetNodeAndChildPoint = (node, root, offsetValue) => {
let brotherList = getDepthNode(node.depth, root, []);
let nodeIndex = brotherList.findIndex(item => item.id === node.id);
for (let i = 0; i < brotherList.length; i++) {
if (nodeIndex == brotherList.length - 1) {
if (i < nodeIndex) {
brotherList[i].x = brotherList[i].x - offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'left');
} else {
brotherList[i].x = brotherList[i].x + offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'right');
}
} else {
if (i <= nodeIndex) {
brotherList[i].x = brotherList[i].x - offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'left');
} else {
brotherList[i].x = brotherList[i].x + offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'right');
}
}
}
}
//節點父節點偏移
const offsetParentPoint = (node, root, offsetValue) => {
let parentNode = getParentNode(node, root);
let brotherList = getDepthNode(parentNode.depth, root, []);
let nodeIndex = brotherList.findIndex(item => item.id === parentNode.id);
for (let i = 0; i < brotherList.length; i++) {
if (i < nodeIndex) {
brotherList[i].x = brotherList[i].x - offsetValue / 2;
} else if (i > nodeIndex) {
brotherList[i].x = brotherList[i].x + offsetValue / 2;
}
}
let grandfather = getParentNode(parentNode, root);
if (grandfather) {
offsetParentPoint(parentNode, root, offsetValue)
}
}
//節點父節點偏移
const offsetParenAndChildtPoint = (node, root, offsetValue) => {
let parentNode = getParentNode(node, root);
if (parentNode.id == root.id) {
return;
}
let brotherList = getDepthNode(parentNode.depth, root, []);
let nodeIndex = brotherList.findIndex(item => item.id === parentNode.id);
if(nodeIndex!=0){
return;
}
for (let i = 0; i < brotherList.length; i++) {
if (nodeIndex == brotherList.length - 1) {
if (i < nodeIndex) {
brotherList[i].x = brotherList[i].x - offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'left');
} else {
brotherList[i].x = brotherList[i].x + offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'right');
}
} else {
if (i <= nodeIndex) {
brotherList[i].x = brotherList[i].x - offsetValue / 2;
offsetChildPoint(brotherList[i], root, offsetValue, 'left');
}
}
}
let grandfather = getParentNode(parentNode, root);
if (grandfather) {
offsetParenAndChildtPoint(parentNode, root, offsetValue)
}
}
偏移原理
-
只有存在同級的下一個兄弟節點纔可能出現覆蓋效果。
-
只用不同父元素的相鄰節點纔會出現覆蓋效果
-
我們這裏採用從中間向左右分別偏移,即重複的節點,左側的節點向左偏移一半位置,右側的節點向右偏移一半位置,這樣既可達到相鄰2節點不重複。
-
-
如果只是對當前節點進行偏移,那麼它可能會和同級的節點重複,所以對重複的左側節點,其子節點和兄弟節點都將左移;對重複右側節點,其子節點和兄弟節點都將右移。
-
此時我們會發現父節點因爲未移動所以已經不再中間位置了,那我們父節點必須也做到相應的左移和右移。
-
因爲根節點始終在頁面的水平中間位置,並不會移動,此時可以發現偏移節點的公共祖先其實並不需要移動位置。所以在公共祖先下,只需將左側重複元素的直系父節點向左偏移,右側重複元素的直系父元素向右偏移。
-
節點的偏移必定帶動同級節點的偏移,即左側重複節點向左偏移時,其左側的所有節點都需向左偏移,右側重複節點向右偏移時,其右側的所有節點都將向右偏移
-
-
移動邏輯:
- 從二代祖先節點開始,移動同級節點及其子節點。
- 因爲祖先節點下的所有層級節點都偏移了,那麼除祖先節點不變外,其他與祖先節點平級的所有的節點及其父節點都應該進行相應的偏移,以確保同一層級非同一祖先的節點位置正確,直到根節點。
- 假如祖先節點層位於該層第一個位置,那麼其右側的節點均向右移動了,而祖先節點的位置沒變動,所以祖先節點及其左側的節點均應向左移動。
畫布寬高自適應
//獲取最大深度
const getMaxDepth = (root) => {
if (root) {
let maxDepth = 1;
if (root.children && root.children.length > 0) {
for (let node of root.children) {
maxDepth = Math.max(maxDepth, getMaxDepth(node) + 1);
let nodeIndex = root.children.findIndex(item => item.id == node.id);
if (root.children[nodeIndex + 1]) {
maxDepth = Math.max(maxDepth, getMaxDepth(root.children[nodeIndex + 1]) + 1);
}
}
}
return maxDepth;
}
}
//獲取最大橫座標節點
const getMaxPointX = (root, maxPointX = 0) => {
if (root) {
maxPointX = Math.max(maxPointX, root.x);
if (root.children && root.children.length > 0) {
for (let node of root.children) {
maxPointX = getMaxPointX(node, maxPointX)
}
}
}
return maxPointX;
}
//獲取最小橫座標節點
const getMinPointX = (root, minPointX = 0) => {
if (root) {
minPointX = Math.min(minPointX, root.x);
if (root.children && root.children.length > 0) {
for (let node of root.children) {
minPointX = getMinPointX(node, minPointX)
}
}
}
return minPointX;
}
//獲取畫布最大寬度
const getMaxCanvasWidth = (root, nodeProps) => {
let rootX = root.x;
let maxPointX = getMaxPointX(root, rootX);
let minPointX = getMinPointX(root, rootX);
let minX = Math.abs(minPointX - rootX - nodeProps.nodeWidth / 2);
let maxCanvasWidth = Math.max(minX, maxPointX - rootX + nodeProps.nodeWidth / 2);
return Math.ceil(maxCanvasWidth * 2);
}
const getMaxCanvasHeight = (root, nodeProps) => {
let maxDepth = getMaxDepth(root);
let maxHeight = nodeProps.nodeHeight * maxDepth + nodeProps.levelHeight * (maxDepth - 1);
return Math.ceil(maxHeight);
}
使用方法
//爲節點添加x,y座標
dealData(canvas, treeData, nodeSetting, scale) {
const width = canvas.width;
const height = canvas.height;
let nodeProps = {
width: width,
nodeWidth: nodeSetting.nodeWidth * scale,
nodeHeight: nodeSetting.nodeHeight * scale,
levelHeight: nodeSetting.levelHeight * scale,
distance: nodeSetting.distance * scale
}
TreeUtil.generatePonint(nodeProps, treeData, null);
TreeUtil.offsetRoot(nodeProps, treeData, treeData);
let maxCanvasWidth = TreeUtil.getMaxCanvasWidth(treeData, nodeProps);
if (maxCanvasWidth > width) {
canvas.width = maxCanvasWidth;
this.dealData(canvas, treeData, nodeSetting, scale);
}
let maxCanvasHeight = TreeUtil.getMaxCanvasHeight(treeData, nodeProps);
if (maxCanvasHeight > height) {
canvas.height = maxCanvasHeight;
this.dealData(canvas, treeData, nodeSetting, scale);
}
}
canvas:畫布對象
treeData:樹形數據
nodeSetting:節點相關配置信息
scale:縮放比例,1/像素比