双向树线条标签调节
这个示例构建了一个以根节点为中心的双向树,其中一侧分支朝向根节点生长,另一侧分支则背离根节点生长。图形可以在水平和垂直树形预设之间切换,同时保持相同的数据集和相同的双侧结构。
双向树线条标签调优
这个示例构建了什么
这个示例构建了一个以根节点为中心的双向树,其中一侧分支朝向根节点生长,另一侧分支则背离根节点生长。图形可以在水平和垂直树形预设之间切换,同时保持相同的数据集和相同的双侧结构。
用户会看到由正交线连接的矩形节点,其中父侧分支使用黄色,子侧分支使用绿色。最重要的行为是,树两侧都可以分别独立调整各自的线条标签几何参数,因此即使方向发生变化,标签位置仍然保持可读。
数据是如何组织的
数据是一个静态内联的 RGJsonData 对象,包含一个 rootId、一个扁平的 nodes 数组,以及一个扁平的 lines 数组。有些连线指向根节点,有些连线则从根节点向外指,这让布局具有足够的方向性,从而围绕中心节点生成一棵双向树。
在调用 setJsonData 之前没有做任何预处理。真正有意义的转换发生在布局之后:组件会读取 node.lot.level,将负数层级视为反向一侧的分支,然后在运行时为不同侧分别应用颜色和线条标签设置。
在实际应用中,同样的结构可以表示上游与下游依赖、管理者与下属、所有权流入与流出,或者任何需要将一个焦点节点放在中间而不是使用单一顶层根节点的层级结构。
relation-graph 是如何使用的
这个示例使用 RGProvider 提供 hook 上下文,并通过运行时配置来渲染 RelationGraph 画布,而不是依赖一个很大的声明式 options 属性。RGHooks.useGraphInstance() 是主要控制入口:组件在初始化期间调用 setOptions()、setJsonData()、moveToCenter() 和 zoomToFit(),然后再使用 getNodes()、getLines()、updateNode() 和 updateLine() 做布局后的重新样式化。
布局始终使用 layoutName: 'tree',但会在 from: 'left' 和 from: 'top' 之间切换。这个方向切换也会改变间距和默认连接点,在水平模式下使用 RGJunctionPoint.lr,在垂直模式下使用 RGJunctionPoint.tb。两种预设下的视觉基线保持稳定:矩形节点、无节点边框,以及用于连接线的 RGLineShape.SimpleOrthogonal。
图中没有使用自定义节点插槽或自定义线条插槽。相反,它依赖内置渲染,并通过两种方式定制结果:一是通过运行时更新线条几何,二是通过 SCSS 覆盖标签外观。.rg-line-label 规则会把内置标签变成紧凑的白色小标签,它的边框和文字颜色会继承当前线条颜色,从而让两组分支在视觉上与各自的连接线保持一致。
浮动控制窗口是一个本地子组件,本身并不是图形功能的一部分,但它对示例的组织方式很重要。它承载了方向选择器、两个 MyLinesOptions 面板,以及一个共享的 canvas-settings 浮层,这个浮层可以改变滚轮与拖拽行为,并把当前图形导出为图片。
关键交互
方向选择器可以在水平和垂直树形预设之间切换。这个变化会重建图配置,重新加载同一份 JSON 数据,重新应用分支样式,并将视口重新居中。
两个 MyLinesOptions 面板会就地更新当前图形。每一侧都可以修改 textAnchor、placeText、textOffsetX、textOffsetY 和 polyLineStartDistance,而无需重建数据集,因此这个演示表现得像一个专注于标签调优的实验场。
浮动窗口本身可以拖动和最小化。它的设置浮层可以把滚轮行为在滚动与缩放之间切换,把画布拖拽行为在框选与移动之间切换,并通过共享的截图辅助工具导出图形画布。
节点点击和连线点击处理器只会把对象打印到控制台,因此它们不会实质性改变示例的行为。
关键代码片段
这段代码说明,方向切换会在加载同一份 JSON 数据之前,先改变布局方向、间距、连接点默认值,以及图形的基础渲染选项。
if (activeTabName === 'h') {
layoutOptions = {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 150,
treeNodeGapV: 20
};
defaultJunctionPoint = RGJunctionPoint.lr;
} else {
layoutOptions = {
layoutName: 'tree',
from: 'top',
treeNodeGapH: 20,
treeNodeGapV: 150
};
defaultJunctionPoint = RGJunctionPoint.tb;
}
这段代码证明,分支分组来源于布局元数据,随后再利用这些分组为两侧分别重新着色,并应用不同的标签几何设置。
for (const node of graphInstance.getNodes()) {
if (node.lot && node.lot.level !== undefined && node.lot.level < 0) {
graphInstance.updateNode(node, { color: '#ca8a04' });
leftNodes.push(node);
} else {
graphInstance.updateNode(node, { color: '#3f9802' });
}
}
const leftNodeIds: string[] = leftNodes.map(n => n.id);
for (const line of graphInstance.getLines()) {
if (leftNodeIds.includes(line.from) || leftNodeIds.includes(line.to)) {
graphInstance.updateLine(line, {
color: '#ca8a04',
textAnchor: parentsLineOptions.textAnchor ? parentsLineOptions.textAnchor : (activeTabName === 'v' ? 'center' : 'start'),
placeText: parentsLineOptions.placeText
});
}
}
这段代码展示了,线条标签控制是以可复用的 UI 状态暴露出来的,而不是为每条边硬编码数值。
<SimpleUISelect
data={[
{ value: '', text: 'Auto' },
{ value: 'start', text: 'start' },
{ value: 'middle', text: 'middle' },
{ value: 'end', text: 'end' }
]}
currentValue={lineOptions.textAnchor}
onChange={(newValue: string) => {
lineOptionsUpdater({
...lineOptions,
textAnchor: newValue
});
}}
/>
这段代码展示了如何把内置线条标签重新样式化为带边框的小标签,而不是普通的 SVG 文本。
.rg-line-peel {
.rg-line-label {
background-color: #fff;
color: var(--rg-line-color);
border: 1px solid var(--rg-line-color);
font-size: 10px;
}
}
这个示例的独特之处
与关系紧密的 bothway-tree 示例相比,这个变体更聚焦于线条标签几何,而不是箭头方向或分支形状变化。对比数据始终表明,它最强的差异点在于:布局完成后,可以通过两个独立的 MyLinesOptions 面板分别重设父侧分支和子侧分支的样式。
与 layout-tree 和 io-tree-layout 相比,这个示例更关注在必须保留内置标签可见性的情况下,如何让双向树保持可读,而不是一般性的重新布局机制。双向树、正交连接线、运行时方向切换、通过 node.lot.level 做布局后分支识别,以及针对不同分支的标签控制,这些组合起来构成了它可复用的核心。
它也比 line 示例更聚焦。它不是预先罗列多种边样式,而是展示如何在布局之后保持标准的 relation-graph 连线,并就地修改它们;当分支分组只能在布局引擎运行后才能确定时,这种方式很有用。
这种模式还能应用在哪里
这种模式很适合用于上游和下游依赖查看器、供应链树、所有权结构,或者审批流程等场景,在这些场景中,同一个焦点实体需要在同一张画布中同时拥有入向和出向分支。
当线条标签承载操作含义时,这种模式也很有用,例如百分比、角色、阶段或关系类型,并且这些标签在同一层级结构的两侧需要不同的偏移量或锚点设置。
当上游与下游状态应当根据布局结果推导出来,而不是在输入数据集中以显式标记重复保存时,这种布局后分组技术尤其适用。