Web思维导图实现的技术点分析,【跳槽必备】

因为一个节点可能包含文本、图片等多种信息,所以,我们使用一个g元素来作为节点容器,文本就创建一个text节点,需要边框的话就再创建一个rect节点,节点的最终大小就是文本节点的大小再加上内边距,比如我们要渲染一个带边框的只有文本的节点:

import { G, Rect, Text} from '@svgdotjs/svg.js’class Node { constructor(opt = {}) { // … this.group = new G()// 节点容器 this.getSize() this.render() } // 计算节点宽高 getSize() { let textData = this.createTextNode() this.width = textData.width + 20// 左右内边距各10 this.height = textData.height + 10// 上下内边距各5 } // 创建文本节点 createTextNode() { let node = new Text().text(this.nodeData.data.text) let { width, height } = node.bbox()// 获取文本节点的宽高 return { node, width, height } } // 渲染节点 render() { let textData = this.createTextNode() textData.node.x(10).y(5)// 文字节点相对于容器偏移内边距的大小 // 创建一个矩形来作为边框 this.group.rect(this.width, this.height).x(0).y(0) // 文本节点添加到节点容器里 this.group.add(textData.node) // 在画布上定位该节点 this.group.translate(this.left, this.top) // 容器添加到画布上 this.draw.add(this.group) }}

如果还需要渲染图片的话,就需要再创建一个image节点,那么节点的总高度就需要再加上图片的高,节点的总宽就是图片和文字中较宽的那个大小,文字节点的位置计算也需要根据节点的总宽度及文字节点的宽度来计算,需要再渲染其他类型的信息也是一样,总之,所有节点的位置都需要自行计算,还是有点繁琐的。

节点类完整代码请看:Node.js,地址:https://github.com/wanglin2/mind-map/blob/main/simple-mind-map/src/Node.js

逻辑结构图

=========

思维导图有多种结构,我们先看最基础的【逻辑结构图】如何进行布局计算,其他的几种会在下一篇里进行介绍。

逻辑结构图如上图所示,子节点在父节点的右侧,然后父节点相对于子节点总体来说是垂直居中的。

节点定位


这个思路源于笔者在网上看到的,首先根节点我们把它定位到画布中间的位置,然后遍历子节点,那么子节点的left就是根节点的left +根节点的width+它们之间的间距marginX,如下图所示:

然后再遍历每个子节点的子节点(其实就是递归遍历)以同样的方式进行计算left,这样一次遍历完成后所有节点的left值就计算好了。

class Render { // 第一次遍历渲染树 walk(this.renderer.renderTree, null, (cur, parent, isRoot, layerIndex) => { // 先序遍历 // 创建节点实例 let newNode = new Node({ data: cur,// 节点数据 layerIndex// 层级 }) // 节点实例关联到节点数据上 cur._node = newNode // 根节点 if (isRoot) { this.root = newNode // 定位在画布中心位置 newNode.left = (this.mindMap.width - node.width) / 2 newNode.top = (this.mindMap.height - node.height) / 2 } else {// 非根节点 // 互相收集 newNode.parent = parent._node parent._node.addChildren(newNode) // 定位到父节点右侧 newNode.left = parent._node.left + parent._node.width + marginX } }, null, true, 0)}

接下来是top,首先最开始也只有根节点的top是确定的,那么子节点怎么根据父节点的top进行定位呢?

上面说过每个节点是相对于其所有子节点居中显示的,那么如果我们知道所有子节点的总高度,那么第一个子节点的top也就确定了:

firstChildNode.top = (node.top + node.height / 2) - childrenAreaHeight / 2

如图所示:

第一个子节点的top确定了,其他节点只要在前一个节点的top上累加即可。

那么怎么计算childrenAreaHeight呢?首先第一次遍历到一个节点时,我们会给它创建一个Node实例,然后触发计算该节点的大小,所以只有当所有子节点都遍历完回来后我们才能计算总高度,那么显然可以在后序遍历的时候来计算。

但是要计算节点的top只能在下一次遍历渲染树时,为什么不在计算完一个节点的childrenAreaHeight后立即就计算其子节点的top呢?原因很简单,当前节点的top都还没确定,怎么确定其子节点的位置呢?

// 第一次遍历walk(this.renderer.renderTree, null, (cur, parent, isRoot, layerIndex) => { // 先序遍历 // …}, (cur, parent, isRoot, layerIndex) => { // 后序遍历 // 计算该节点所有子节点所占高度之和,包括节点之间的margin、节点整体前后的间距 let len = cur._node.children cur._node.childrenAreaHeight = cur._node.children.reduce((h, node) => { return h + node.height }, 0) + (len + 1) * marginY}, true, 0)

总结一下,在第一轮遍历渲染树时,我们在先序遍历时创建Node实例,然后计算节点的left,在后序遍历时计算每个节点的所有子节点的所占的总高度。

接下来开启第二轮遍历,这轮遍历可以计算所有节点的top,因为此时节点树已经创建成功了,所以可以不用再遍历渲染树,直接遍历节点树:

// 第二次遍历walk(this.root, null, (node, parent, isRoot, layerIndex) => { if (node.children && node.children.length > 0) { // 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半 let top = node.top + node.height / 2 - node.childrenAreaHeight / 2 let totalTop = top + marginY// node.childrenAreaHeight是包括子节点整体前后的间距的 node.children.forEach((cur) => { cur.top = totalTop totalTop += cur.height + marginY// 在上一个节点的top基础上加上间距marginY和该节点的height }) }}, null, true)

事情到这里并没有结束,请看下图:

可以看到对于每个节点来说,位置都是正确的,但是,整体来看就不对了,因为发生了重叠,原因很简单,因为【二级节点1】的子节点太多了,子节点占的总高度已经超出了该节点自身的高。

因为【二级节点】的定位是依据【二级节点】的总高度来计算的,并没有考虑到其子节点,解决方法也很简单,再来一轮遍历,当发现某个节点的子节点所占总高度大于其自身的高度时,就让该节点前后的节点都往外挪一挪。

比如上图,假设子节点所占的高度比节点自身的高度多出了100px,那我们就让【二级节点2】向下移动50px,如果它上面还有节点的话也让它向上移动50px,需要注意的是,这个调整的过程需要一直往父节点上冒泡,比如:

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值