ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JointJS 系谱图(Genogram)自动布局实战:基于 DirectedGraph 与夫妻容器的多代族谱构建指南

JointJS 系谱图(Genogram)自动布局实战:基于 DirectedGraph 与夫妻容器的多代族谱构建指南 前端UI组件【免费下载链接】jointA proven SVG-based JavaScript diagramming library powering exceptional UIs项目地址https://gitcode.com/gh_mirrors/jo/joint点击查看免费下载系谱图Genogram是医学、心理学与社会工作中广泛使用的扩展家族树它在基础血缘关系之上用标准化符号编码性别、死亡、收养、双胞胎等信息。本指南以开源仓库中 examples/genogram-ts 示例为蓝本讲解如何用 JointJS 与joint/layout-directed-graphdagre将一份扁平的 JSON 家族数据自动排布成层次清晰、零人工定位的多代族谱。读完本文你将掌握夫妻容器抽象、五阶段交叉最小化、两种连线路由风格、链接到链接link-to-link连接以及自定义 HighlighterView 等完整实战方案。一、Genogram 是什么超越血缘的标准化符号系统Genogram 与普通家谱的关键区别在于编码了额外的社会与医学信息。本示例在 shapes.ts 中用三类元素形状承载性别信息男性以圆角矩形表示浅蓝填充colors.maleFill女性以椭圆表示浅粉填充colors.femaleFill性别未知以菱形/多边形表示灰色填充colors.unknownFill。同时通过高亮器HighlighterView叠加两类符号参见 highlighters.ts已故dod字段非空在形状上叠加一个内切 X 形十字收养adopted字段为true在形状左右两侧绘制方括号。此外**配偶连线mate link**由共享子女推导而来同卵双胞胎之间用虚线连接。数据中的multiple字段标记多胞胎组identical字段指向同卵双胞胎的另一方。二、技术选型与项目结构示例基于以下技术栈见 package.jsonjoint/core— JointJS 图编辑核心库joint/layout-directed-graph— 基于 dagre 的分层自动布局包Vite— 开发服务器与打包器yarn devTypeScript— 全量类型化源码。项目结构如下见 README.mdexamples/genogram-ts/ src/ main.ts 入口paper 初始化、数据集加载、渲染调度 shapes.ts 元素与连线形状定义MalePerson、FemalePerson、UnknownPerson highlighters.ts 自定义 dia.HighlighterView已故十字、收养括号 layout/ index.ts Genogram 布局算法DirectedGraph 夫妻容器 minimize-crossings.ts 五阶段交叉最小化供 customOrder 使用 utils.ts 元素创建、血统高亮、家族树图 data.ts 数据类型与解析PersonNode、亲子/配偶链接 theme.ts 集中式尺寸、颜色、z-index 默认值与连线样式覆盖 styles.css Paper 与悬停样式 families/ 数据集 JSON 文件 scripts/ test-layout.cts 无浏览器 Node.js 布局测试其中 main.ts 是编排核心它一次性创建dia.Graph与dia.Paperasync: true、frozen: true提升渲染性能通过import.meta.glob预加载families/*.json数据集用下拉框切换数据集、按钮切换连线风格后重新执行layoutGenogram()并调用paper.transformToFitContent()自适应视口。三、数据模型扁平 JSON 与自动关系推导每个数据集是扁平的 JSON 数组一个 person 对象一个元素见 README.md 与 data.ts{ id: 1, name: Jane Doe, sex: F, mother: 2, father: 3, dob: 1990-01-15, dod: 2020-06-01, adopted: true, multiple: 1, identical: 4 }字段类型说明idnumber唯一标识namestring显示名称sexstringM、F或?mothernumber?母亲 IDfathernumber?父亲 IDdobstring?出生日期YYYY-MM-DDdodstring?死亡日期YYYY-MM-DD未知时写?adoptedboolean?是否收养multiplenumber?多胞胎组 ID兄弟姐妹共享identicalnumber?同卵双胞胎的另一方 ID关键设计是关系全部自动推导无需手写边亲子链接遍历每个 person只要mother/father是number且存在于人员集合中就生成{ parentId, childId }见 data.ts 的getParentChildLinks配偶链接如果两个不同的人同时是某个孩子的mother和father则他们是配偶通过father|mother组合键去重生成MateLink见getMateLinksdata.ts。仓库内置四份数据集src/families虚构的多代 Thompson 家族含双胞胎、同卵三胞胎、收养、夭折等多种情形、英国王室George V 至今、带角色标签的关系图Father、Cousin 等以及约 1000 人、7 代的benchmark.json压力数据集。四、核心难点Genogram 为何不能直接做 DAG 布局与普通有向图相比Genogram 的布局有五个独特约束详见 TUTORIAL.md夫妻必须同 rank 且并排亲子连线应从夫妻中点出发双胞胎/多胞胎共享同一分叉点同卵双胞胎之间是连线连到连线配偶连线是水平双向的天然破坏 DAG 假设。解决策略是一条五步流水线将每对夫妻替换为一个夫妻容器节点参与布局用带customOrder 回调的 DirectedGraph 布局最小化交叉后处理把容器拆回两个真实人员、让连线经过夫妻中点、补上配偶连线用高亮器叠加已故/收养符号。五、夫妻容器Couple Containersdagre 按节点逐一布局。若把夫妻拆成两个独立节点两人可能落在不同 rank 或相距很远。因此 layout/index.ts 为每对配偶创建一个不可见的宽矩形宽度足以容纳两个符号并排const extraWidth linkStyle orthogonal ? sizes.symbolWidth : 0; const container new shapes.standard.Rectangle({ size: { width: sizes.symbolWidth * 2 sizes.coupleGap extraWidth, height: sizes.symbolHeight }, });正交orthogonal模式下容器额外加宽一个symbolWidth用来容纳向外偏移的姓名标签。随后用personIdToContainer记录人员元素 → 容器映射layoutId()把每个人员的布局 ID 解析为其容器 IDlayout/index.ts。所有亲子连线在布局阶段都重定向到容器未成家的独身人员solo则直接参与布局。六、边去重Edge Deduplication当父母同处一个容器时一个孩子会产生两条映射到同一条容器→孩子布局边的亲子连线dagre 无法正确处理重复边。布局前按srcLayout→tgtLayout键去重layout/index.tsconst edgeKey ${srcLayout}→${tgtLayout}; const isDuplicate layoutEdgeSet.has(edgeKey); layoutEdgeSet.add(edgeKey); if (isDuplicate) { (link as any)._layoutDuplicate true; }只有首次出现的边参与布局重复边在布局完成后重新加回图graph.addCells(duplicateLinks)。七、DirectedGraph 布局与五阶段交叉最小化布局调用如下layout/index.tsDirectedGraph.layout(graph, { rankDir: TB, nodeSep: sizes.symbolGap, rankSep: sizes.levelGap, customOrder: (glGraph, jointGraph, defaultOrder) minimizeCrossings( glGraph, jointGraph, defaultOrder, { parentChildLinks, layoutId, personById, identicalGroupOf, nodeMultipleGroup } ), });rankDir: TB— 自上而下的层级世代向下流动nodeSep: sizes.symbolGap— 节点间水平间距默认 20rankSep: sizes.levelGap— 世代间垂直间距默认 70customOrder— 用自定义多阶段排序取代 dagre 内置排序。customOrder是 dagre 暴露的排序钩子布局引擎先计算 rank再调用该回调确定每层节点的水平顺序。本示例的五阶段实现位于 minimize-crossings.ts。Phase 1以默认启发式结果作为种子先用 dagre 自己的排序生成基线测量总交叉数并保存最优状态defaultOrder(glGraph); let bestCrossings totalCrossings(); let bestOrder saveOrder();Phase 2多轮重心barycenter细化重心barycenter一个节点的重心是其相邻 rank 上所有邻居位置的平均值——例如某个孩子有三个父母位于位置 1、3、5则其重心为 (135)/3 3。按重心值排序各 rank 节点相连节点会被拉近从而减少边交叉。这是分层图绘制最经典的启发式之一源自 Sugiyama、Tagawa 与 Toda1981。实现中交替执行自上而下看父母与自下而上看子女两轮扫描最多 24 轮MAX_BARYCENTER_ITERATIONS保留最佳结果交叉数为 0 时提前终止for (let iter 0; iter 24; iter) { for (const rank of ranks) reorderByBarycenter(rank, up); for (let i ranks.length - 1; i 0; i--) reorderByBarycenter(ranks[i], down); const crossings totalCrossings(); if (crossings bestCrossings) { bestCrossings crossings; bestOrder saveOrder(); } if (crossings 0) break; }每次重心排序的平局tie由出生日期与同卵双胞胎组 ID打破保证兄弟姐妹按自然顺序排列minimize-crossings.ts。Phase 3贪心节点重排对每个节点尝试其在 rank 内的每一个位置选取使全局总交叉数最小的位置以跳出重心扫描无法解决的局部最小值for (let i 0; i nodes.length; i) { nodes.splice(i, 1); // 先移除节点 for (let j 0; j nodes.length; j) { nodes.splice(j, 0, nodeId); // 尝试每个插入位置 applyOrder(nodes); const cost totalCrossings(); // 记录最优位置... nodes.splice(j, 1); } nodes.splice(bestPos, 0, nodeId); // 插入最优位置 }该阶段复杂度为 O(n² × 位置数)因此宽度超过 50 个节点的 rank 会被跳过MAX_RANK_WIDTH_FOR_RELOCATION避免大数据集上的性能问题。Phase 4容器级交叉消解dagre 在带 dummy 节点的图上做交叉最小化长边被拆成短段这可能在容器展开成两个配偶后遗漏视觉交叉。本阶段用computeContainerCrossings()——只统计真实边容器到容器的交叉检查——配合基于真实边邻接的重心扫描修正真实节点用真实边邻居dummy 节点用 dagre 图邻居保住连线路由质量。关键洞察是每个扫描方向自上而下、自下而上之后都要单独检查容器交叉并保存最优因为自下而上的扫描可能推翻自上而下扫描刚修好的结果minimize-crossings.ts。Phase 5双胞胎/多胞胎相邻约束交叉最小化可能把同一多胞胎组的成员拆散。本阶段把组成员聚拢到最左成员的位置同卵双胞胎保持相邻组内按同卵组优先、再按出生日期排序members.sort((a, b) { // Identical group first, then birth date }); filtered.splice(insertAt, 0, ...members);八、夫妻定位Couple Positioning布局完成后每个容器都有坐标。接下来把容器拆回两个人员元素根据各自父母容器的 X 坐标决定谁站左边从而避免连线不必要地交叉layout/index.tsconst fromParentX getParentX(fromId); const toParentX getParentX(toId); const [leftEl, rightEl] fromParentX toParentX ? [fromEl, toEl] : [toEl, fromEl]; const inset linkStyle orthogonal ? sizes.symbolWidth / 2 : 0; leftEl.position(pos.x inset, pos.y); rightEl.position(pos.x inset sizes.symbolWidth gap, pos.y);正交模式下人员相对容器边缘向内缩进以居中姓名标签也向外偏移避免重叠if (linkStyle orthogonal) { leftEl.attr(name, { textAnchor: end, x: calc(w / 2 - ${sizes.nameMargin}) }); rightEl.attr(name, { textAnchor: start, x: calc(w / 2 ${sizes.nameMargin}) }); }九、连线重连与路由经过夫妻中点亲子连线在视觉上必须从夫妻两者的中点出发而不是从某一个配偶出发。布局后把每条连线重连回真实人员元素并添加 verticeslayout/index.ts路由几何随连线风格而异。Fan扇型默认——水平到夫妻中点垂直向下再水平到孩子const midX (sourceCenter.x partnerCenter.x) / 2; const midY (sourceCenter.y partnerCenter.y) / 2; const halfwayY (midY targetCenter.y) / 2; link.vertices([ { x: midX, y: midY }, // 夫妻中点 { x: midX, y: halfwayY }, // 垂直向下 { x: targetCenter.x, y: halfwayY } // 转向孩子 ]);Orthogonal正交——从父/母垂直向下、水平到夫妻中点、再垂直向下、水平到孩子const thirdY midY (targetCenter.y - midY) / 3; const twoThirdsY midY 2 * (targetCenter.y - midY) / 3; link.vertices([ { x: sourceCenter.x, y: thirdY }, // 从父/母垂直向下 { x: midX, y: thirdY }, // 水平到中点 { x: midX, y: twoThirdsY }, // 再次垂直向下 { x: targetCenter.x, y: twoThirdsY } // 水平到孩子 ]);注意目标锚点使用anchor: { name: top, args: { useModelGeometry: true } }使连线从符号顶部接入且重连时优先使用useModelGeometry保证几何一致。十、双胞胎/多胞胎共享分叉点双胞胎与三胞胎共享一个分叉点——各子连线不各自转向而是在组的平均 X 坐标处汇合layout/index.tsconst avgX uniqueIds.reduce((sum, id) { return sum (graph.getCell(id) as dia.Element).getCenter().x; }, 0) / uniqueIds.length;Fan 风格下用forkX取代targetCenter.xlink.vertices([ { x: midX, y: midY }, { x: midX, y: halfwayY }, { x: forkX, y: halfwayY } // 共享分叉点 ]);分组键为源容器ID|multiple 值twinGroupKey同父同母且multiple相同的子女共享分叉点。十一、同卵双胞胎连接链接到链接Link-to-Link同卵双胞胎用一条水平虚线连接各自的亲子连线。这用到 JointJS 的链接到链接连接能力配合connectionRatio锚点layout/index.tsconst ratioA computeAnchorRatio(linkA, ANCHOR_VERTICAL_OFFSET); const ratioB computeAnchorRatio(linkB, ANCHOR_VERTICAL_OFFSET); new IdenticalLinkShape({ source: { id: linkA.id, anchor: { name: connectionRatio, args: { ratio: ratioA } } }, target: { id: linkB.id, anchor: { name: connectionRatio, args: { ratio: ratioB } } }, });computeAnchorRatio沿连线路径从孩子端反向回溯找到距孩子指定垂直偏移ANCHOR_VERTICAL_OFFSET levelGap / 4正交模式为/8的点换算成 0–1 的路径比例。连线的几何样式定义在 shapes.ts虚线strokeDasharray: 4 2粉色描边。十二、配偶连线Mate Links最后添加配偶连线是两人之间的水平连接。它必须在布局之后添加因为它是双向的会破坏布局引擎的 DAG 假设layout/index.tsconst mateJointLinks mateLinks.map((ml) { return new MateLinkShape({ source: { id: String(ml.from), anchor: { name: center, args: { useModelGeometry: true } } }, target: { id: String(ml.to), anchor: { name: center, args: { useModelGeometry: true } } }, }); }); graph.addCells(mateJointLinks);MateLink使用较粗strokeWidth: 3的粉色描边且不带箭头targetMarker: null。十三、符号高亮器自定义 dia.HighlighterView已故十字与收养括号作为自定义dia.HighlighterView子类实现与形状定义解耦。每个高亮器用tagName path在preinitialize()中设置静态属性仅在highlight()中计算动态的d属性highlighters.tsclass DeceasedHighlighter extends dia.HighlighterView { preinitialize() { this.tagName path; this.attributes { stroke: colors.dark, strokeWidth: 2, strokeLinecap: round, fill: none, }; } protected highlight(elementView: dia.ElementViewdia.Element) { const { width, height } elementView.model.size(); const p crossPadding; const d M ${p} ${p} ${width - p} ${height - p} M ${width - p} ${p} ${p} ${height - p}; this.el.setAttribute(d, d); } }高亮器在图渲染完成后施加。已故十字用z: 2使其渲染在年龄标签之下收养括号无需特殊 z 顺序DeceasedHighlighter.add(cellView, body, deceased-cross, { z: 2 }); AdoptedHighlighter.add(cellView, body, adopted-brackets);年龄与姓名的 SVG 标签、textWrap换行宽度calc(w 2*nameWrapOverlap)、maxLineCount、省略号等都在 shapes.ts 的commonAttrs()中统一定义年龄由 utils.ts 的computeAge依据dob/dod计算生年-卒年取自dod未去世时显示*。十四、交互悬停血统高亮悬停某个人员时会高亮其直系祖先与后代。实现位于 utils.ts 的setupLineageHighlighting血缘遍历使用单独的familyTree图buildFamilyTree仅含人员节点与亲子边通过 JointJS 的getPredecessors/getSuccessors高效查询祖先与后代悬停元素用内置highlighters.stroke添加粗描边layer: dia.Paper.Layers.BACK相关连线用 z 偏移置顶无关元素变暗。z-index 默认值集中在 theme.ts 的defaultZIndex中export const defaultZIndex { person: 1, parentChildLink: 2, mateLink: 3, identicalLink: 3, focusedOffset: 10, };每种连线类型有自己的默认 z置顶时加focusedOffset10以保持相对堆叠顺序例如配偶连线始终在亲子连线之上mouseleave时从已知默认值恢复 z 而非读取当前值避免脏状态问题。元素用 opacity 变暗而连线用变浅的描边颜色——因为带 opacity 的重叠线段在交叉处会显得更暗产生视觉伪影.joint-element { transition: opacity 0.3s ease; } .joint-element.dimmed { opacity: 0.1; } .joint-link [joint-selectorline] { transition: stroke 0.3s ease; } .joint-link.dimmed [joint-selectorline] { stroke: #e2e8dd; }十五、两种连线风格对比UI 中的按钮可切换两种连线路由风格Fan扇型默认连线先水平到夫妻中点、再垂直向下、再水平到孩子形成紧凑的T 形接头外观Orthogonal正交连线先从每个父母垂直向下再经过水平横杆路由形成层级分明、更传统的树状外观。连线风格会连锁影响布局的多个部分影响点FanOrthogonal容器宽度symbolWidth*2 coupleGap再加一个symbolWidth夫妻定位从容器边缘对齐相对容器向内缩进symbolWidth/2姓名标签居中左伴侣右对齐、右伴侣左对齐并外移路由顶点水平→垂直→水平垂直→水平→垂直→水平coupleGap/levelGap20 / 7030 / 100姓名换行上限nameMaxLineCount24十六、主题架构集中式尺寸与样式覆盖布局尺寸与颜色集中在 theme.ts。基础sizes对象不可变按连线风格区分的覆盖单独定义在linkStyleOverridesexport const linkStyleOverrides { fan: {}, orthogonal: { coupleGap: 30, levelGap: 100, nameMaxLineCount: 4 }, } as const satisfies Recordstring, Partialtypeof sizes;渲染时把当前风格的覆盖与基础尺寸合并见 main.tsconst layoutSizes { ...sizes, ...linkStyleOverrides[linkStyle] };这样切换风格时不会污染基础主题对象。sizes的核心尺寸包括符号宽高50×50、已故十字内缩4、收养括号间距6、夫妻间距20、节点间距20、代际间距70、画布内边距50、姓名换行外延5、正交标签边距6等。十七、无浏览器布局测试scripts/test-layout.cts 是一个 Node.js 脚本复用浏览器版完全相同的layoutGenogram()无重复实现在无浏览器环境验证布局零交叉# 在 examples/genogram-ts 目录下 yarn test-layout # 默认跑 thompson.json yarn test-layout --datathompson.json # 指定数据集它解析--data参数用标准 Rectangle 形状替代自定义 SVG 形状Node 环境无需 SVG markup跑完布局后按 Y 坐标分组打印各代人员的水平位置并做容器级视觉交叉检查把同一夫妻的两条边按夫妻中点归一、按容器键去重后用(srcX - srcX) * (tgtX - tgtX) 0判定交叉发现交叉即打印并process.exit(1)失败。这正是 Phase 4 中只有容器到容器边才可能视觉交叉思想的直接落地。十八、数据设计考量跨家族婚姻不同根家族的孩子相互通婚在平面布局中会造成不可避免的边交叉。为最小化交叉见 TUTORIAL.md 末尾尽量让联姻发生在相邻家族之间避免同一代出现一连串跨家族婚姻嫁入家族但树上没有父母的配偶外部配偶作为 solo 节点放置不引入额外家族分支。十九、运行指南# 仓库根目录安装依赖后yarn workspace 方式进入示例目录 cd examples/genogram-ts yarn install yarn dev打开终端显示的地址默认http://localhost:5173用下拉框切换 Thompson 家族、英国王室、关系图等数据集用按钮切换 Fan/Orthogonal 连线风格观察布局差异benchmark.json约 1000 人、7 代用于验证五阶段交叉最小化在大型数据集上的性能表现Phase 3 会跳过宽度超过 50 的 rank 以控制开销。完整的分步讲解见 TUTORIAL.md布局流水线全图与各阶段细节可对照 layout/index.ts 与 minimize-crossings.ts 阅读。赞分享前端UI组件【免费下载链接】jointA proven SVG-based JavaScript diagramming library powering exceptional UIs项目地址https://gitcode.com/gh_mirrors/jo/joint点击查看免费下载相关推荐dbskill 问题单元模板深度解析从 YAML frontmatter 到 QST 内容单元的实战指南dbskill 问题单元模板深度解析从 YAML frontmatter 到 QST 内容单元的实战指南 导读 dbs content system http前端UI组件mxGraph自定义布局算法基于力导向的网络关系图实现mxGraph自定义布局算法基于力导向的网络关系图实现 引言 在数据可视化领域网络关系图Network Graph是展示实体间关联的强大工具。传统布局算UI组件前端图形学AI 菜谱知识图谱生成器实战基于 Kimi 大模型的智能信息抽取与 Neo4j 图谱构建Datawhale all-in-ragAI 菜谱知识图谱生成器实战基于 Kimi 大模型的智能信息抽取与 Neo4j 图谱构建Datawhale all in rag 本篇技术指南围绕 Dat教程人工智能大模型RAG上一篇Redis桌面管理革命Another Redis Desktop Manager完整实战指南下一篇MikroORM 索引与唯一约束完整指南从 Index/Unique 装饰器到类型安全的 using 查询提示创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表