在页面上渲染一颗N叉树

542 阅读2分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

渲染的示例数据

const root = {
  val: 1,
  children: [
    { val: 2, children: [] },
    {
      val: 3,
      children: [
        { val: 5, children: [] },
        { val: 6, children: [] },
        {
          val: 7,
          children: [
            { val: 11, children: [] },
            { val: 12, children: [] },
          ],
        },
        { val: 8, children: [] },
      ],
    },
    {
      val: 4,
      children: [
        { val: 9, children: [] },
        { val: 10, children: [] },
      ],
    },
  ],
}

结点不重叠

前几天一个朋友问我怎么样能把一棵树渲染出来,保证结点之间不重叠.学了两个多月的算法这还是难不倒我的.用 BFS 遍历树,一层层渲染即可,这样保证每层之间不会重叠,然后每一层之间相互隔开.

interface DataNode {
  val: number
  children: DataNode[]
}
// 渲染结点
interface drawNode {
  (x: number, y: number, val: number, radius?: number): void
}
function renderTree(root: DataNode) {
  const gap = 50
  let children = [root],
    top = 50

  while (children.length) {
    let left = 50
    const tmp: DataNode[] = []
    for (const node of children) {
      drawNode(left, top, node.val)
      tmp.push(...node.children)
      left += gap
    }
    top += gap
    children = tmp
  }
}

查看源码 renderTree1.ts

实际渲染的样子,每个结点之间不会重叠

render1.png

优化渲染效果

只是这样看起来有点丑,果然朋友又加了个要求,想要渲染出来能好看一些,结点的位置能平衡一些,类似 XMind 中的样子:

xmind-tree.png

要渲染成这样,就没办法像上面那样一步到位的完成了,在不知道子孙结点的情况下,无法确定当前结点横坐标的位置.这时候我们可以使用预处理的思想,先将整棵树完整的搜索一遍,获取我们想要的信息,之后再去根据已有信息进行布局.

代码如下:

function renderTree(root: DataNode) {
  const nodes = new Map<DataNode, { val: number; x: number; y: number; wide: number }>()
  const gap = 50
  let top = 50
  const queue: (DataNode | null)[][] = [[root]]

  // 用 BFS 遍历树,按层将结点放入 queue 中
  while (true) {
    const tmp: (DataNode | null)[] = []
    let foundChild = false
    for (const node of queue[queue.length - 1]) {
      if (node && node.children.length) {
        foundChild = true
        tmp.push(...node.children)
      } else {
        // 当结点没有子结点时,填充 null,预留宽度
        tmp.push(null)
      }
    }
    if (!foundChild) break

    queue.push(tmp)
  }

  // 自底向上遍历 queue,先计算子结点的位置以及宽度,然后父结点的宽度等于子结点宽度之和
  for (let i = queue.length - 1; i >= 0; i--) {
    let left = 0
    for (const node of queue[i]) {
      let wide = gap
      if (node) {
        for (const child of node.children) {
          wide += nodes.get(child)?.wide!
        }
        if (wide > gap) wide -= gap

        const x = left + wide / 2,
          y = top * (i + 1)
        nodes.set(node, { x: x, y: y, val: node.val, wide })
        drawNode(x, y, node.val)
      }
      left += wide
    }
  }
}

查看源码 renderTree.ts

最终渲染出来的样子

render2.png

本文源码 github.com/XYShaoKang/…