画一棵普通的组织架构树,现成的库一大把。但「族谱树」有两个普通树形结构搞不定的硬骨头: 夫妻要水平并排,任意两人之间要能算出口语称谓。这篇讲讲追远是怎么处理的。
一、为什么不用 D3 / G6 的现成布局
最直觉的做法是抓个图布局库。但 D3 的 d3.tree() 和 AntV G6 的 CompactBox 都假设
一个节点只有一个父、子节点垂直挂在下方。族谱不是这样:
- 夫妻是同一代、需要水平并排的两个节点,而不是父子关系;
- 孩子挂在「夫妻这一对」下方居中,而不是挂在某一个人下方;
- 一个人可能有多段婚姻,每段婚姻各自带孩子。
硬套通用树布局,结果就是配偶被挤成上下级、连线打架。所以追远的布局单位不是「人」,而是
FamilyUnit = 丈夫 + 妻子 + 子女数组。布局算法是自研的。
二、两趟扫描:自底向上量宽,自顶向下落座
核心是经典的两趟(two-pass)思路,但作用在 FamilyUnit 上:
第一趟 · 自底向上计算子树宽度
从叶子往根递归。每个 FamilyUnit 的宽度 = max(夫妻并排所需宽度, 所有子树宽度之和)。
叶子节点宽度就是一张卡片;越往上,宽度是孩子们撑开的总和。这一趟只算尺寸,不定坐标。
function measure(unit) {
const selfW = coupleWidth(unit) // 夫妻并排宽度
if (unit.children.length === 0)
return unit.width = selfW
let childrenW = sum(unit.children.map(measure)) // 递归
+ gap * (unit.children.length - 1)
return unit.width = Math.max(selfW, childrenW)
}
第二趟 · 自顶向下分配坐标
从根往下。父节点拿到自己的 x 区间后,把孩子们按各自宽度水平排开、整体在父节点下方居中, y 由代数(generation)决定。夫妻则在自己这一格里左右并排。一趟下来每个节点的 (x, y) 就定死了。
关键不变量:父节点 x 始终落在「孩子区间的中点」。这保证了连线永远从夫妻中间垂下来、再分叉到每个孩子,视觉上对称。
三、渲染:SVG + 视口虚拟化
族谱动辄几百上千人,全量渲染 DOM 会卡。追远的处理:
- SVG 渲染,每个节点一个
<g>分组,夫妻连线用虚线、亲子连线用折线 / 贝塞尔曲线; - 视口虚拟化:只渲染「可见区域 + 200px 缓冲区」内的节点,平移时动态增删;
- 缩放范围 0.1x ~ 3x,以鼠标 / 双指捏合中心为缩放原点;PC 用 wheel + 拖拽,移动端用 Hammer.js 手势;
- 懒加载:初始只展开 4 代,点击节点的加号按需加载子树。
四、称谓推算:BFS + 增强六符号编码
「我和这个人是什么关系?」——这是族谱的灵魂功能,也是最容易做错的地方。追远不走数据库递归, 而是把单个家族的关系图整个加载进内存,用 BFS 求两人之间的路径,再把路径翻译成口语称谓。
路径编码:六个符号
从 A 走到 B 的每一步,用一个字母表示「往哪个方向走」:
| 符号 | 含义 | 符号 | 含义 |
|---|---|---|---|
F | 经父亲上行 | S | 到儿子 |
M | 经母亲上行 | D | 到女儿 |
H | 到丈夫 | W | 到妻子 |
于是「爷爷」是 FF(父之父),「外婆」是 MM(母之母),「堂哥」是
FFS…(经祖父再下来的同辈男性),「表妹」则经 M 上行——
编码里有没有经过母系(M),正好就是堂亲 / 表亲的分水岭。
查表为主,规则兜底
- 查表法:100+ 条常见路径直接映射到口语称谓,覆盖 10 代以上直系、旁系、姻亲;
- 规则推导兜底:查不到的长路径,用辈分差 + 性别 + 父 / 母系规则现推;
- 刻意用口语称谓——爷爷 / 奶奶 / 外公 / 外婆,而不是书面的祖父 / 祖母,因为这是给普通家庭用的,不是给宗族祠堂用的。
五、离线优先:一份 schema,两端互通
追远没有服务器、不联网、不要账号。数据全在用户自己的设备上:
- PC 桌面版:Vue 3 + Electron 外壳 + better-sqlite3,主进程跑 SQLite,通过 IPC 暴露给渲染层;
- Android 版:Kotlin + Jetpack Compose + 原生 SQLiteOpenHelper;
- 关键点:两端复用同一份
schema.sql,所以一个.zybak整库备份(含照片)能在手机和电脑之间直接来回导。
正因为要双端互通,性别字段在底层始终存 1 / 2 不动——产品理念里的「不分男女」是在显示层做的,碰都不碰数据格式。工程上的克制,换来的是十年后这份 .db 还能打开。
六、踩过的坑,一句话总结
- 别套通用树布局——配偶并排是族谱的第一性,布局单位必须是「家庭」不是「人」。
- 称谓别用数据库递归——单家族关系图小,全量进内存 BFS 又快又好维护。
- 离线优先的代价要前置——schema 一旦双端共用,任何「显示需求」都不能反向改数据格式。