需求描述

关系图分层级展示,做一个类似树结构的展示界面,每一层级节点按照权重计算坐标位置,父节点的位置放在在下层子节点中间。

需求分析

  • 关系图不是真正的树结构,所以目标节点只有在其下一层的才是计算的‘子节点’,如果兄弟节点有共同的下层‘子节点’,按照从左到右的顺序优先排列(也就是说,若节点A和节点B是同一层的兄弟节点,他们有共同的下层节点C,那么就把C作为A的‘子节点’,在计算B节点坐标的时候就不在使用C节点作为参照了),这样做的目的是为了避免两个兄弟节点具有相同的下层‘子节点’导致渲染节点重合的问题。
  • 确定分层:关系层次是确定的,所以可以根据分类数组遍历创建一个收集每一层节点的数组。
  • 节点排序:根据分层的节点,从上到下依次排出节点的顺序,为了计算节点坐标做准备。
  • 计算所有叶子节点横坐标:根据所有叶子节点的数量平分区间。遍历分层数组,层从上到下,节点从左到右,如果当前节点是第一个叶子节点,设置权重比例为 w = 1,如果当前节点有子节点就去遍历其子节点,如果其子节点是叶子节点就设置权重 w = w + 1, 按照遍历顺序每一个叶子节点权重 + 1。如果一共有n 个叶子节点那么就把空间平均分成
    n + 1份。这个(n+1)要记录下来,等分空间的每一份宽度就是 boxWidth / (n + 1),用叶子节点的权重 * 每一份的宽度就是当前叶子节点的横坐标了。
  • 计算除了叶子节点之外的节点横坐标:在上一步基础上,倒序遍历分类数组,比如只有3层,从第三层开始遍历,计算第二层节点横坐标,只要将第二层某一节点的下层所有子节点(第三层的叶子节点)中两头的节点权重相加除以2作为当前节点在本层的权重,其他节点类似求权重即可。
  • 设置节点坐标:纵坐标可以设置成固定值,横坐标按照节点权重乘以每一份宽度即可 w * boxWidth / (n + 1)

问题解决

  • 按分类排序节点
// 分类数组 before
categories: [
  {name: '分类1'},
  {name: '分类2'},
  {name: '分类3'}
],
    
// 分类数组 after,增加了children,其中就是node节点
cateArr: [
    name: '分类1',
    children: [...]
  },
  {
    name: '分类2',
    children: [...]
  },
  {
    name: '分类3',
    children: [...]
  }
],
      
// children 中的 node 节点
{
  "id": "1",
  "category": 0,
  "name": "节点名称1",
  "value": 10
}
/**
 * @Description: 根据分层遍历计算每个节点的子节点数
 * @param {*} links 节点关系数组(排序后)
 * @param {*} arr 分类数组
 * @param {*} lastIdx 数组最后一个index, 最后一层就是子节点,不用参与遍历
 * @return {*}
 */
sortNodes(links, arr, lastIdx) {
  const cArr = cloneDeep(arr)
  let vm = this
  cArr.forEach((item, idx) => {
    if (idx !== lastIdx) {
      let prev = []
      let nodes = item.children
      nodes.forEach((node) => {
        let id = node.id
        let link = vm.getLinkIds(links, id, prev, item.pIds)
        node.ids = link.ids
        node.cIds = link.cIds
        prev = link.prev
      })
    }
  })
  this.setLeafWeight(cArr)
}
  • 叶子节点设置权重
/**
 * @Description: 设置叶子节点的权重
 * 每个叶子节点权重为1,其余节点权重为其下层所有关联节点权重的中间值
 * @param {*} arr 分类数组
 * @return {*}
 */
setLeafWeight(arr) {
  let vm = this
  const cArr = cloneDeep(arr)
  let len = cArr.length - 1
  let len2 = len - 1 // 倒数第二层
  let w = 1 // 节点权重初始值
  vm.nodeMap.clear()

  // 从上到下计算所有叶子节点权重
  cArr.forEach((item, idx) => {
    if (idx !== len) {
      const nodes = item.children
      nodes.forEach((node) => {
        let ids = node.ids
        if (!ids.length) {
          vm.nodeMap.set(node.id, w)
          w++
        }
        if (idx === len2 && ids.length) {
          ids.forEach((id) => {
            vm.nodeMap.set(id, w)
            w++
          })
        }
      })
    }
  })

  this.leafGrad = vm.nodeMap.size + 1
  this.setRestWeight(cArr, len2)
},
  • 非叶子节点设置权重
/**
 * @Description: 设置除叶子节点之外的节点权重
 * @param {*} arr 分类数组
 * @param {*} len 分类数组倒数第二层index
 * @return {*}
 */
setRestWeight(arr, len) {
  let map = this.nodeMap
  for (let i = len; i > -1; i--) {
    let item = arr[i].children
    Array.isArray(item) &&
      item.forEach((node) => {
        if (!map.has(node.id)) {
          if (node.ids.length === 1) { // 只有一个字节的的直接取子节点值
            let mid = map.get(node.ids[0])
            map.set(node.id, mid)
          } else {
            let [start, end] = node.cIds
            let mid = (map.get(start) + map.get(end)) / 2
            map.set(node.id, mid)
          }
        }
      })
  }
},
  • 获取节点坐标值
/**
 * @Description: 获取节点坐标值
 * @param {*} cArr 大分类
 * @param {*} len 分类数组长度
 * @return {*}
 */
getNodePos(cArr, len) {
  const boxDom = document.getElementById('myChart')
  const boxWidth = boxDom.clientWidth - 100
  const ySize = Math.ceil(boxDom.clientHeight / (len + 1))
  const xGrad = boxWidth / this.leafGrad // 叶子节点分割区间宽度

  let allNodes = []
  cArr.forEach((c, idx) => {
    let children = c.children
    if (Array.isArray(children) && children.length) {
      let nodes = cloneDeep(children)
      const height = ySize * (idx + 1)
      nodes.forEach((node) => {
        node.x = this.nodeMap.get(node.id) * xGrad
        node.y = height
      })
      allNodes.push(...nodes)
    }
  })

  return allNodes
},

注意

  • 关系数组中数据排序会影响节点的坐标,所以通过遍历分类数组,根据每一个节点的id在关系数组中按照当前节点的指向target来排序。