产品上决定做子图嵌套是在 2021 年 1 月底,回想起来,当时做出这个决定还是比较纠结的。因为,产品上本就不是想做 XMind,也不想被 XMind 牵着走,更不想背包袱(有了 XMind 的功能,也意味着有了它的包袱)。但无奈的是,在面对一些具体场景时,当时没找到更合适的做法。最终还是硬着头皮上了,好在最终结果上看,我们虽然沿着 XMind 的路径,但貌似走出了一条也许比 XMind 更宽广的道路。

小溪鲨鱼

“很幸运,我们所在的这条小溪没有大鲨鱼,所以我们才能够这样活到现在,并且活得很好。才有时间去尝试更多的可能。” —— 来源《在 XMind 工作是怎样的体验?

透过一些公开分享资料,会发现 XMind 的人喜欢用“小溪里没鲨鱼”,比喻在思维导图是个过于细分的领域,没有巨头进来,以至于他们这十几年,活下来了,且活的很好。

从某个角度来说这是对的,毕竟区区思维导图,如果巨头愿意投入足够的人力,是一定能做出来的。

从另一个角度看,这句话也是 XMind 谦虚的表现。因为十多年只做一件事,并把它做到世界第一,那这里面一定是有壁垒的。比如本篇提的子图嵌套,市面上林林总总的思维导图软件那么多,但实现这个功能的,只有寥寥几家。语雀思维图也是做了一年半,才走到这一步。所以这应该称不上是一件简单的事情。

还有,一方面,这次语雀思维图的子图嵌套,深受 XMind 的启发。另一方面,在图编排领域,思维导图是一个非常年轻子领域,作为一家民族企业,能秉持长期主义,持续投入,并这么多年来稳居世界第一,那还是蛮厉害的。向 XMind 致敬

黑屋找门

古来,数学家在证明一个定理时,常常用黑屋找门比喻,自身处境艰难,比如:

“数学家就像身在一间黑屋里的盲人,努力想看清一只黑猫,而那只黑猫也许根本就不存在。”
—— 达尔文

“证明未知的猜想,就像在一个方圆 1 平方公里的黑屋子里找路,没有任何光亮。”
—— 陈秀雄

“解决费马大定理,就是要穿过一个一个的黑屋子,首先我来到一个黑屋子,什么都看不见。我先得去摸摸这个屋子里的所有家具,所有摆设,等摸得烂熟。对这个房间的每一个纹理都清楚的时候,我才能找到它的电灯开关。我打开电灯开关才能知道下一个屋子的门在哪。打开那个门,然后进入下一个屋子然后又开始这个过程。”
—— 安德鲁·怀尔斯

当然,子图嵌套的问题的难度远不及证明数学定理,笔者也没有傻到,要将自己和数学家类比。但在能力和问题难度大致相当的情况下,这种黑屋找门的感受是切实存在的。

在解决未知问题的过程中进错屋子是常态,进对屋子是巧合。由于笔者在解决这个问题的过程中,进错屋子太多,无法尽述。下面仅向大家分享,进对屋子的演绎链条。

黑屋 1:XMind 的方案真的足够好了吗?

XMind 的方案真的足够好了吗?在“暴力”试玩 XMind 后,我们发现 XMind 以下几种情况是不够好的:

显得有点诡异 图形有重叠,明显不够好 应该是 BUG 了
image.png

image.png


image.png
image.png

如果别人的方案不够好,从产品设计上改如何改进?我们总结出一个设计原则:要限制可能产生冲突的组合

那么如何定义冲突的组合呢?在产品设计,阶段大家使用的语言,可能是:“线不能重合”,“布局不能反向”,等。但从技术角度,我们此时离【如何定义冲突】这件屋子,之间还隔着好几个屋子。

黑屋 2:如何定义一个布局?

要讨论什么是思维导图布局的嵌套、布局的冲突,首先得讨论布局是什么。
image.png
思维导图,是一个树形分形(分形通常被定义为:一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状),就像雪花一样。这意味着纵使用户最终画出的图千千万万,但在定义算法时,只需要定义有限的几个。以目前结构举例:
image.png

之前的 7 种结构中,只有两个布局算法,绿色和红色分别这两种布局算法衍生出的结构

在现有布局引擎里,要解释为什么一种布局算法,能衍生出多种布局,还需要引入额外一些概念,下面以标准布局(绿色集合)为例,引出算法方向象限,并逐个每个概念的定义及其作用。

类型 —— Type

image.png
类型是布局中的内核部分,它决定了整棵树的生长方式,上述三幅图分别对应了三种不同的布局算法。定义在节点上,约束其子节点。

方向 —— Direction

image.png
方向确定了算法的生长方向,上面四张图,分别属于同一种布局的四个方向。

象限 —— Quadrant

这个概念衍生自笛卡尔坐标系的四象限,放到布局这里比较晦涩。这里稍微花多点篇幅讲解其含义和作用。从定义上看,这里象限的含义还是空间的划分,是某空间的子空间,结合示例,具体解释如下:
image.png
上图从左到右,分别是一个象限,两个象限,以及四个象限。象限的数量属于节点,在哪个象限展开(选择哪个象限),则由最近一级子节点决定。如何理解这句话?
image.png
以上图举例,A 是含标准布局算法的节点,B、C、D 都是 A 的子节点,但只有 B、C 是其最近一级的子节点。象限的具体作用仅限于 A、B、C 具体表述为:节点 A 的象限数量为 2,B 在象限 A.2,C 在象限 A.1。

一句话描述,在实际编程中,象限的作用:通过影响布局算法的方向,以达到分配子树(图)的生长空间的目的。

黑屋 3:如何定义一个子图?

为便于叙述,先额为拓展一个概念:一个含布局的节点称之为核节点

那么在本文中子图的概念是:若核节点 A 是核节点 B 的子节点,则称节点 A 及其子节点构成的图,是节点 B 的子图。

黑屋 4:基础的嵌套规则是什么?

这个看起来貌似是整个问题的核心,但科学的关键是证伪。有时候,是和不是相比是不值一文的。

子图嵌套是个复杂的事情,为避免日后失控,导致这块成为成本中心。所以基础的嵌套我们追求一个极简的方案,一句话描述:将子图看成矩形黑盒。

根据我们的规则,以下这些图预计会和 XMind 画的不一样。

XMind YMind
image.png image.png
image.png image.png

黑屋 5:如何定义冲突?

这间屋子的结论很简单,但过程却比较曲折。爱思考的朋友,在看结论之前,不妨先自己思考一下。

先说结论:布局方向向量上,含有同向基向量,且不含反向基向量的布局,可以相互嵌套。

比较费解,展开解释一下。这里,我们不妨用一个向量来表示布局的方向,然后把问题归约到线性代数的领域去看。

那么什么是同向基向量?要理解这句话,首先需理解什么是)(严格的定义点击链接去看,不再赘述)。由定义可知,两个线性无关的基可组成一个二维平面空间,在笛卡尔坐标系中,我们不妨找到一组基,分别是沿 x 轴的 (x, 0),以及沿着 y 轴的 (0, y)。

由线性代数的相关的结论可知,任何一个空间中的任意向量,都能用基向量的线性组合表示。举例说明:
问:阿里|语雀思维图子图嵌套背后的技术思考 - 图15 分别与阿里|语雀思维图子图嵌套背后的技术思考 - 图16阿里|语雀思维图子图嵌套背后的技术思考 - 图17阿里|语雀思维图子图嵌套背后的技术思考 - 图18阿里|语雀思维图子图嵌套背后的技术思考 - 图19 是否含有相同的基向量?
image.png
向量 阿里|语雀思维图子图嵌套背后的技术思考 - 图21 由一组基向量 阿里|语雀思维图子图嵌套背后的技术思考 - 图22阿里|语雀思维图子图嵌套背后的技术思考 - 图23 组成。那么由于分别有 阿里|语雀思维图子图嵌套背后的技术思考 - 图24阿里|语雀思维图子图嵌套背后的技术思考 - 图25阿里|语雀思维图子图嵌套背后的技术思考 - 图26阿里|语雀思维图子图嵌套背后的技术思考 - 图27,同向。所以有 阿里|语雀思维图子图嵌套背后的技术思考 - 图28阿里|语雀思维图子图嵌套背后的技术思考 - 图29阿里|语雀思维图子图嵌套背后的技术思考 - 图30阿里|语雀思维图子图嵌套背后的技术思考 - 图31 ,有同向基向量。反之 阿里|语雀思维图子图嵌套背后的技术思考 - 图32阿里|语雀思维图子图嵌套背后的技术思考 - 图33阿里|语雀思维图子图嵌套背后的技术思考 - 图34阿里|语雀思维图子图嵌套背后的技术思考 - 图35 ,没有同向基向量。

解释完同向基向量,回头再看如何理解这条规则。下面以缩进布局标准布局的组合举例:
image.png
结构一和结构二没有同向基向量,所以不能互相嵌套。结构一和结构三,有同向的水平基向量,则可以相互嵌套。

在此规则下,无冲突的图形是:
image.png
上文黑屋 1 中提到的那些,则都存在冲突,到了这里,我们终于可以暂且回到第一个屋子,解释了什么是冲突。

黑屋 6:如何解决冲突?

虽说产品上的目标是避免冲突,但实际研发的过程中,真实的任务其实是解决冲突。这也是整套方案的核心,下面内容略费脑子,坐好扶稳了。

其实从最上层,case by case 去解冲突,其实每个问题看起来并不难,答案甚至是显而易见的。比如:

冲突组合 解决冲突
image.png image.png
image.png image.png
image.png image.png
…… ……

但这里我们从技术视角,这种排列组合是无法枚举,如果按穷举方式去做,未来一定是成本中心,因而我们有必要去构建一套理论,形成一个框架,去处理冲突。

概念补充

在正式进入之前,还需要补充一些概念。

如果觉得下文某些概念不清晰、缺漏、或是未使用业界标准用词。望指出,望斧正。

首先,在本次的方案中,对于一种布局来说,它的支持的方向和占满一个全空间所需象限数(后文简写:满象限数)是既定的。详解如下:

布局类型 布局方向 满象限数
标准布局(Standard) 4 [ image.png, 下.png, 左.png, 右.png ] 2 [ 左右.png, 上下.png ]
缩进布局(Intend) 4 [右下.png, 右下.png, 左上.png, 右上.png ] 4 [ image.png ]

定义:实际可用象限数等于满象限数的布局,称之满布局。
根据该定义,进行一次拆解,能得到以下 3 个满布局:

编号 满布局 象限空间
m.1 左右.png image.png
m.2 上下.png image.png
m.3 image.png image.png

定义:将子节点约束在某个象限下,我们称之为象限约束。
定义:满布局含象限约束得到的布局,称之缺布局。
进一步展开,有以下缺布局:

编号 对应满布局 布局 方向 约束象限
q.1 m.1 右.png 1
q.2 m.1 左.png 2
q.3 m.2 下.png 1
q.4 m.2 上.png 2
q.5 m.3 右下.png 1
q.6 m.3 左下.png 2
q.7 m.3 左上.png 3
q.8 m.3 右上.png 4

解决冲突

完成储备概念后,我们回来本节的主题,如何处理冲突?
先尝试将冲突大致分类:

A:同类布局,相同满布局 B:同类布局,不同满布局 C:不同类布局
image.png image.png image.png

经过审慎思考(此处省略 500 字),我们得出结论:

  1. 对于含有相同方向的布局(比如:场景 A),只需要修改象限约束,得到无冲突的缺布局,即可解决冲突。
  2. 若不含同向方向向量(比如:场景 B),需要强制转为同一满布局,后序问题则归约到第一种情况
  3. 冲突和布局类型无关,仅和方向有关,可以归约到上述两种情况。
  4. 由于当下象限集和目标象限集,是一个多对多的关系,所以需要一个算法保证,象限规约的合理性和稳定性。

问题域

由之前的讨论可得,这里的抽象问题域是:将一个象限集,以某种规则映射到另一个象限集。
举例说明:

定义域 值域 期望映射结果
image.png
象限集 [ 2 ]
image.png
象限集 [ 1 ]
2 → 1
image.png
象限集 [ 1, 2 ]
image.png
象限集 [ 1 ]
1 → 1
2 → 1

根据实际产品设计,象限的归约还需要遵循就近原则,举例说明:

定义域 值域 期望映射结果
image.png
[ 3 ]
image.pngimage.png
[1, 4]
3 → 1

归约算法

算法比较简单,但比较关键,因为现阶段(未来可能也是),所有涉及象限归约的场景,都会落到这个算法里。核心点是:

一、确保最邻近。
二、可泛化。满足多对多的所有情况
三、周期性。确保定义域是任何值,都能映射到值域

具体流程,打个比方是:顺时针看 1 眼,逆时针看 2 眼,顺时针看 3 眼,…… 逆时针看 n+1 眼
举例说明:

当前象限 目标象限 第一步 第二步 第三步 命中结束
image.png image.png image.png image.png image.png

具体实现,详细解释起来有点琐碎,直接上代码了,如下:

  1. /**
  2. * 象限归约
  3. * @param {Array} domain - 当前象限定义域
  4. * @param {Array} range - 目标象限值域
  5. * @param {Number} l - 目标集合总长度
  6. */
  7. export function quadrantReduce(domain, range, l) {
  8. const rst = {};
  9. let source, target, c;
  10. for (let i = 0; i < domain.length; i++) {
  11. source = domain[i];
  12. target = source;
  13. c = 1;
  14. while (!range.includes(target)) {
  15. if (c % 2 === 1) {
  16. target += c;
  17. if (target > l) target %= l;
  18. } else {
  19. target -= c;
  20. if (target < 1) target = l + target % l;
  21. }
  22. c++;
  23. }
  24. rst[source] = target;
  25. }
  26. return rst;
  27. }

至此,我们阐释了这次子图嵌套方案的主要概念和思路。

总结

对于一张思维导图而言,结构无疑是很重要的。

这次更新从技术角度,挑战前所未有。从产品角度,也开启一扇门,一来,之前想做的一些内部场景(如:项管场景,需要标准布局嵌套鱼骨图、时间轴布局等),后续也能开始做了。二来,相信子图嵌套也能带来更大的灵活度,给用户的创作,更大的发挥空间。

最后

技术是性感的,求是求真的过程是痛苦的,更是愉悦的。希望所有技术人都能在忙碌的工作中,从技术里,在求真求是的过程里,在自己的内心中,找到最纯粹的、最本源的一份快乐。