仿AntV G6-紧凑树布局实战

1,767 阅读4分钟

很开心实现了紧凑树布局,如有bug,欢迎指教。

参照antv g6官方图例

紧凑树

脑图树

对比:

脑图树,树枝相互之间不会相交,但当树越来越深,树枝会劈的越来越庞大,很占用空间。

紧凑树,在保证树枝不会互相重叠的情况下,做到了空间的充分占用,如果该分叉树枝比较短,空间足够就会紧挨隔壁树枝生长,非常的perfect!!!

实战

如何做到紧凑

“树枝”会看看左邻右枝的生长情况,如果邻枝长的也比较茂盛,那么这两个树枝之间要间隔大一点,避免枝叶相互遮挡,但具体要间隔多少,左右两边的间隔又分别是多少,这个就要看树枝生长的有多茂盛,“树根”再根据“树枝”的茂盛情况,合理的布局“树枝”位置。

关键点:

以上的间隔情况用补位数来说明,左右的间隔用上补位数、下补位数来说明
  • 从未节点往前计算补位数
  • 父节点不需要补位的情况
    • 同一深度下,第一个节点跟最后一个节点属于同一个父节点
    • 子节点数为0
    • 子节点数==1且子节点的补位数之和==0
  • 父节点需要补位的情况
    • 同一深度下,如果节点是第一个节点,父节点只需下补位
    • 同一深度下,如果节点是最后一个节点,父节点只需上补位(节点数+补位总数/2)
    • 同一深度下,如果当前节点是中间节点,其父节点需上、下补位 上、下需补位数 = (节点数+补位总数)/2 ----大致说明
  • 节点真正需要的补位数
    1. 判断该节点上、下是否有满足补位数的“紧挨”且“相连”的“无子节点”且“未被补位占用”的节点存在
    2. 若存在,则该节点不需补位
    3. 若不存在,补位数=需补位数-已存在满足条件的节点数
  • 设置节点位置
    • 父节点的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)
   }
 })
}

UI优化