树布局层级距离与节点距离设置
这个示例在同一固定层级数据上演示 relation-graph 树布局的两种运行时间距策略。用户可对比滑块驱动的 `treeNodeGapH` 与 `treeNodeGapV` 更新,以及逗号分隔的 `levelGaps` 输入;每次变更都会重新布局、居中图谱并适配视口。
使用滑块和逐层间距比较树布局间距
这个示例构建了什么
这个示例构建了一个用于控制间距的从左到右树布局实验场。画布展示了一组固定的层级结构,使用矩形节点和正交连线;同时,一个悬浮工具窗口允许用户在同一张图上切换两种间距策略。
在第一种模式下,用户通过范围输入调整水平方向的层级间距和垂直方向的同级节点间距。在第二种模式下,用户输入一个以逗号分隔的 levelGaps 数组,为不同深度指定不同的间距。这里最重要的并不是示例数据本身,而是在同一个实时的 relation-graph 实例上,并排比较统一间距控制与逐层间距控制。
数据是如何组织的
图数据以内联方式声明为一个 RGJsonData 对象,其中包含 rootId、扁平的 nodes 数组和扁平的 lines 数组。因此,这个层级结构是通过边记录显式表达的,而不是通过嵌套的 children 对象表达。
在首次调用 setJsonData() 之前没有任何预处理。后续唯一一次数据转换发生在第二种控制模式中:文本输入会按逗号拆分、去除首尾空白并转换为数字,然后作为 layout.levelGaps 重新使用。
在真实应用中,同样的数据结构可以表示汇报关系、依赖树、分类层级、审批链,或任何需要分别调整各层深度间距的方向性树结构。
relation-graph 是如何使用的
入口组件使用 RGProvider 包裹整个 demo,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 获取运行中的图实例。基础配置把内置 tree 布局设为 from: 'left'、treeNodeGapH: 400 和 treeNodeGapV: 70;同时在视觉上刻意保持简洁,使用矩形节点、灰色正交连线、左右连接点、右侧展开控制点以及圆角折线拐角。
真正体现示例重点的是实例 API。组件挂载时会调用 setJsonData(),然后执行 moveToCenter() 和 zoomToFit()。当当前模式或间距状态发生变化时,组件会通过 setOptions({ layout }) 重写 layout,运行 doLayout(),然后再次居中并适配视口。在 case 1 中,它会更新 treeNodeGapH 和 treeNodeGapV,同时清空 levelGaps。在 case 2 中,它会应用 levelGaps,并保留 treeNodeGapV 作为同级节点间距。
这里没有自定义节点、连线、画布或视口插槽,也没有图编辑工作流。本地 SCSS 中唯一的样式覆盖是把节点文字颜色设为黑色,这让树保持可读,同时让整体外观仍然接近 relation-graph 的默认效果。悬浮辅助窗口来自共享的 DraggableWindow 组件,它还提供了一个次级的画布设置面板,用于控制滚轮模式、拖拽模式和图片导出。
关键交互
最主要的交互是模式切换。SimpleUISelect 在基于滑块的面板和基于文本输入的面板之间切换,每次切换标签页都会立即触发对应的重新布局逻辑,并作用于已经加载的树。
在第一种模式中,任一范围输入变化都会立刻重新运行树布局,因此用户可以实时比较更宽或更紧凑的层级间距与同级节点间距的效果。在第二种模式中,编辑逗号分隔的数值列表会更新 levelGaps 并再次运行布局,从而在不重建数据集的前提下展示不均匀的层级深度间距。
悬浮窗口本身还可以被拖动、最小化,并切换到设置覆盖层。这个共享覆盖层可以修改 wheelEventAction、修改 dragEventAction,并在截图流程前后分别调用 prepareForImageGeneration() 和 restoreAfterImageGeneration() 来导出当前图像。
关键代码片段
这段 options 配置说明,该 demo 从一个自左向右的树开始,具有明确的默认间距和最少化的样式设置。
const baseOptions: RGOptions = {
debug: false,
layout: {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 400,
treeNodeGapV: 70,
},
defaultExpandHolderPosition: 'right',
defaultNodeShape: RGNodeShape.rect,
这段内联数据说明,该示例使用显式的 nodes 和 lines,而不是嵌套式树数据。
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'a' },
{ id: 'b', text: 'b' },
{ id: 'b1', text: 'b1' },
{ id: 'b1-1', text: 'b1-1' },
// ...
],
这段重新布局流程对应第一种间距策略:更新 treeNodeGapH 与 treeNodeGapV,清空 levelGaps,然后重新运行当前布局。
const layoutOptions: RGLayoutOptions = {
layoutName: 'tree',
from: 'left',
treeNodeGapH: rangeHorizontal,
treeNodeGapV: rangeVertical,
levelGaps: []
};
graphInstance.setOptions({ layout: layoutOptions });
await graphInstance.doLayout();
第二个面板展示了该示例如何把一个文本输入框转换为逐层间距数组。
<input className="w-full border border-gray-900 p-1"
value={levelGaps.join(',')}
placeholder="Please enter content"
onChange={(e) => {
setLevelGaps(e.target.value.split(',').map(item => Number(item.trim())));
}}
></input>
这段共享设置代码说明,这个悬浮辅助窗口还会通过图实例 API 控制画布行为与图片导出。
const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();
这个示例的独特之处
对比式的数据展示让这个示例具有辨识度,因为它把两种树间距策略打包进了同一个紧凑 demo,并且都作用于同一份静态层级结构。它不是只暴露一组布局控制,而是让用户比较由滑块驱动的 treeNodeGapH 和 treeNodeGapV 更新,与解析后的逐层 levelGaps 之间的差别;每次变化之后都会执行 setOptions({ layout })、doLayout()、重新居中以及视口适配。
与 tree-data 相比,这个示例关注的不是树数据如何加载。它更核心的启发在于:无需改变数据集形态,就可以对一棵已经加载的树进行交互式重设间距。与 ever-changing-tree 和 layout-folder2 相比,它的范围要窄得多:它不会演变成一个广义的样式或方向切换实验场,而是保持朴素的视觉处理,让用户注意力集中在间距本身。
它也和 center-layout-options 很接近,但这里把那种双模式的间距对比模式迁移到了具有方向性的 tree 布局中。这让同级节点间距与逐层间距之间的区别更直观,因为页面直接在同一棵从左到右的树上暴露了 treeNodeGapH、treeNodeGapV 和 levelGaps。
这种模式还适用于哪些场景
- 在为组织树、依赖树或审批图添加领域特定节点模板之前,先构建一个内部布局调优面板。
- 在基于树的产品中,向团队讲解何时应使用全局统一间距规则,何时应使用按深度区分的间距规则。
- 创建技术演示时,需要基于 React state 立即重新布局,而不在每次控件变化时重建图数据。
- 为只读层级结构增加一个轻量的图形工作台,让用户在测试布局参数时同时调整视口行为并导出快照。