很开心实现了紧凑树布局,如有bug,欢迎指教。
参照antv g6官方图例
紧凑树
脑图树
对比:
脑图树,树枝相互之间不会相交,但当树越来越深,树枝会劈的越来越庞大,很占用空间。
紧凑树,在保证树枝不会互相重叠的情况下,做到了空间的充分占用,如果该分叉树枝比较短,空间足够就会紧挨隔壁树枝生长,非常的perfect!!!
实战
如何做到紧凑
“树枝”会看看左邻右枝的生长情况,如果邻枝长的也比较茂盛,那么这两个树枝之间要间隔大一点,避免枝叶相互遮挡,但具体要间隔多少,左右两边的间隔又分别是多少,这个就要看树枝生长的有多茂盛,“树根”再根据“树枝”的茂盛情况,合理的布局“树枝”位置。
关键点:
以上的间隔情况用补位数来说明,左右的间隔用上补位数、下补位数来说明
- 从未节点往前计算补位数
- 父节点不需要补位的情况
- 同一深度下,第一个节点跟最后一个节点属于同一个父节点
- 子节点数为0
- 子节点数==1且子节点的补位数之和==0
- 父节点需要补位的情况
- 同一深度下,如果节点是第一个节点,父节点只需下补位
- 同一深度下,如果节点是最后一个节点,父节点只需上补位(节点数+补位总数/2)
- 同一深度下,如果当前节点是中间节点,其父节点需上、下补位 上、下需补位数 = (节点数+补位总数)/2 ----大致说明
- 节点真正需要的补位数
- 判断该节点上、下是否有满足补位数的“紧挨”且“相连”的“无子节点”且“未被补位占用”的节点存在
- 若存在,则该节点不需补位
- 若不存在,补位数=需补位数-已存在满足条件的节点数
- 设置节点位置
- 父节点的y位于“子节点”纵向布局的中心
- 虚拟节点总数 = 子节点补位数总数 + 子节点数
- 子节点整体纵向高度为= 子节点高度 * 虚拟节点总数
- 子节点与中心位之间的差距 = (节点的索引index + 当前节点前的补位总数) - 中心位置
--- 注意:不要遗漏当前节点前的补位总数 - 子节点的y坐标 = 子节点与中心位之间的差距 * 总高度/总数 + 父节点的y
效果图
节点信息:补位总数-上补位数-下补位数-子节点数
代码讲解
树节点结构
{
id: '', // 节点ID
parentId:'', // 父节点ID
ITEM_WIDTH: 120, // 节点的宽
ITEM_HEIGHT: 90, // 节点的高
SPLIT_WIDTH: 100, // 节点左右的间距
SPLIT_HEIGHT: 50, // 节点上下的间距
childNodeNum: 0, // 子节点数
deep: 1, // 深度
bwNum: 0, // 补位数
prevBw: 0, // 上补位数
nextBw: 0 // 下补位数
}
计算各个节点deep、childNodeNum
calChildNodeInfo (parent) {
const { children } = parent
if (!children.length) {
return
}
let childNodeNum = 0
children.forEach(child => {
if (child.parentId === node.id) {
childNodeNum++;
const deep = parent.deep + 1
child.deep = deep
// deepObject存放各个deep的节点列表
const deepList = this.deepObject[deep] || []
deepList.push(child)
this.deepObject[deep] = deepList
this.calChildNodeInfo(child)
}
})
parent.childNodeNum = num;
}
(核心)计算各个节点的补位数情况
calChildNodeBwInfo () {
// 从未节点往前计算补位数
const keys = Object.keys(this.deepObject).sort((a, b) => {
return a - b
})
const maxDeep = keys[keys.length - 1]
for (let i = maxDeep; i > 1; i--) {
const list = this.deepObject[i] // 当前深度i的树节点list
const firstNode = list[0] // 深度i下的第一个节点
const lastNode = list[list.length - 1] // 深度i下的最后一个节点
// 判断如果深度i下,第一个节点跟最后一个节点属于同一个父节点,则父节点不需要补位
if (firstNode.parentId === lastNode.parentId) {
continue
}
const parentIds = {}
list.forEach((item) => {
// 判断如果父节点已经计算过,则return
if (!parentIds[item.parentId]) {
const parentIndex = this.getIndexInDeep(i - 1, item.parentId)
if (parentIndex < 0) {
return
}
// 深度i下的item节点对应父节点
const parentNode = this.deepObject[i - 1][parentIndex]
// 父节点的直接子节点的补位数总数
const childNodesbwNum = this.getChildNodesbwNum(parentNode)
parentIds[item.parentId] = true
// 以下情况,父节点不需要补位
if (parentNode.childNodeNum === 1 && childNodesbwNum === 0 || parentNode.childNodeNum === 0) {
return
}
let tempBwNum = 0
let hasNextNum = 0
let hasPrevNum = 0
/*
计算节点需要的补位数
判断该节点上、下是否有满足补位数的“紧挨”且“相连”的“无子节点”且“未被补位占用”的节点存在;
若存在,则该节点不需补位,否则的话,补位数=需补位数-已存在满足条件的节点数
*/
// 深度i下,当前节点是第一个节点,父节点只需下补位
if (item.id === firstNode.id) {
// 补位数处理
if (parentNode.childNodeNum === 1) {
tempBwNum = childNodesbwNum
} else {
tempBwNum = (parentNode.childNodeNum - 1 + childNodesbwNum) / 2
}
// 获取父节点"下面"满足补位数且符合条件的节点数
hasNextNum = this.calHasNext(parentNode, Math.ceil(tempBwNum), parentIndex)
if (hasNextNum < tempBwNum) {
parentNode.bwNum = tempBwNum - hasNextNum
parentNode.nextBw = tempBwNum - hasNextNum
}
return
}
// 深度i下,当前节点是最后一个节点,父节点只需上补位
if (item.id === lastNode.id) {
if (parentNode.childNodeNum === 1) {
tempBwNum = childNodesbwNum
} else {
tempBwNum = (parentNode.childNodeNum - 1 + childNodesbwNum) / 2
}
// 获取父节点"上面"满足补位数且符合条件的节点数
hasPrevNum = this.calHasPrev(parentNode, Math.ceil(tempBwNum), parentIndex)
if (hasPrevNum < tempBwNum) {
parentNode.bwNum = tempBwNum - hasPrevNum
parentNode.prevBw = tempBwNum - hasPrevNum
}
return
}
// 深度i下,当前节点是中间节点,其父节点需上、下补位
let prevBw = 0
let nextBw = 0
tempBwNum = parentNode.childNodeNum - 1 + childNodesbwNum
hasPrevNum = this.calHasPrev(parentNode, Math.ceil(tempBwNum / 2), parentIndex)
hasNextNum = this.calHasNext(parentNode, Math.ceil(tempBwNum / 2), parentIndex)
if (hasPrevNum < tempBwNum / 2) {
prevBw = tempBwNum / 2 - hasPrevNum
}
// 如果当前节点跟最后一个节点属于同一个父节点,则父节点不需要下补位
if (item.parentId !== lastNode.parentId) {
if (hasNextNum < tempBwNum / 2) {
nextBw = tempBwNum / 2 - hasNextNum
}
}
parentNode.bwNum = prevBw + nextBw
parentNode.prevBw = prevBw
parentNode.nextBw = nextBw
}
})
}
}
// 工具方法
calHasPrev (node, num, index) {
const deep = node.deep
const deepList = this.deepObject[deep]
let firstIndex = index - num
firstIndex = firstIndex < 0 ? 0 : firstIndex
const tempArr = deepList.slice(firstIndex, index)
const filterArr = []
for (let i = tempArr.length - 1; i > -1; i--) {
const item = tempArr[i]
if (item.childNodeNum !== 0 || item.hasUseBw) {
break
}
item.hasUseBw = true // 设置已经被补位占用,防止重复计算
filterArr.push(item)
}
return filterArr.length
},
calHasNext (node, num, index) {
const deep = node.deep
const deepList = this.deepObject[deep]
const tempArr = deepList.slice(index + 1, index + num + 1)
const filterArr = []
for (let i = 0; i < tempArr.length; i++) {
const item = tempArr[i]
if (item.childNodeNum !== 0 || item.hasUseBw) {
break
}
item.hasUseBw = true
filterArr.push(item)
}
return filterArr.length
}
(核心)设置节点位置(x,y)
setNodePosition(parent) {
const childNum = parent.childNodeNum
if (childNum === 0) return
const startX = parent.x + parent.ITEM_WIDTH + parent.SPLIT_WIDTH
const childNodesbwNum = this.getChildNodesbwNum(parent)
const bwNum = childNum + childNodesbwNum
let tempDisBw = 0 // 节点前的节点补位总数
parent.children.forEach((item, i) => {
if (item.parentId === parent.data.nodeId) {
item.x = startX
const disY = (parent.ITEM_HEIGHT + parent.SPLIT_HEIGHT) * bwNum
const itemDisY = disY / bwNum
const middle = (bwNum - 1) / 2
tempDisBw = item.prevBw + tempDisBw
const disIndex = (i + tempDisBw) - middle
const tempY = disIndex * itemDisY + parent.y
if (childNum === 1) {
item.y = parent.y
} else {
item.y = tempY
}
/*
设置子节点的y坐标
父节点的y在子节点纵向布局的中心
子节点整体纵向高度为= (parent.ITEM_HEIGHT + parent.SPLIT_HEIGHT) * (子节点补位数之和 + 子节点数)
子节点的y坐标 = (中心位middle之间的差距disIndex) * itemDisY + parent.y
差距disIndex = (节点的索引i + 当前节点前的补位总数) - 中心位置
*/
tempDisBw += item.nextBw
this.setNodePosition(item)
}
})
}