多组关系网布局与样式编辑器
这个示例构建了一个固定画布图谱工作台,用户可插入随机生成的树分组,为每组分配独立布局与视觉样式,并以组为单位进行编辑或删除。示例结合了分组子布局执行、悬浮配置面板、选中态覆盖层、小地图支持,以及共享画布导出与交互设置。
在固定画布上编辑多个图分组
这个示例实现了什么
这个示例构建的不是一张预加载的图表,而是一个轻量级的图工作台。页面展示了一个全屏的 RelationGraph 画布,带有深色网格背景、一个悬浮的白色控制窗口、一个小地图,以及用于当前编辑分组的选中覆盖层。
用户可以在运行时插入新的图分组。每个插入的分组都会生成为一棵随机树,放置在指定的根坐标附近,并拥有各自的节点样式、连线样式和布局算法。插入之后,点击该分组中的任意节点,会将整个分组提升为可编辑选区,并暴露出批量重新布局、批量改样式和删除操作。
这个示例的重点并不只是抽象地展示“混合布局”。它展示的是:如何在保持外层画布固定的同时,把每个子图当作一个可以独立设置样式、独立执行布局的模块来处理。
数据如何组织
源数据最初是由 generateRandomTreeData(...) 生成的树形 JsonNode 结构。它的配置项不多,但表达力足够:depth、childCount 和 hasChildrenProbability 定义了每个生成分组的规模以及分支的稠密程度。
在数据传入 relation-graph 之前,flattenTreeData(...) 会把这棵嵌套树转换成扁平的 nodes 和 lines 数组。在这个扁平化步骤中,每个节点都会在 data 中保留一个 isLeaf 标记,这为后续代码提供了一个附加视图层元数据的位置,而无需改动可见节点字段。
分组管理器随后又增加了一层预处理。appendTreeNodeData(...) 会把同一个 myGroupId 写入新分组中的每个节点,并为每条连线分配统一的 junction-point 样式。这个 myGroupId 会成为后续驱动选中、重新布局、样式变更和删除操作的关键标识。
这个示例还单独维护了一个从 myGroupId 映射到 { groupRootNodeId, layoutOptions } 的运行时表。这个映射使编辑器具备有状态能力:一旦某个分组被放到画布上,代码就可以取回该分组的根节点,并只重新执行该分组选定的布局算法。
在真实产品中,随机树可以替换为任何分组关系数据,例如业务能力岛、服务集群、工作流片段、组织单元,或需要共存在同一工作区中的多个依赖子图。
relation-graph 的使用方式
外层 RelationGraph 运行在 fixed 布局模式下,因此 relation-graph 不会尝试一次性排列整个画布。相反,这个示例把 RGHooks.useGraphInstance() 作为运行时图操作的控制接口,用于执行 addNodes、addLines、updateNode、updateLine、createLayout、setEditingNodes、removeNodes、removeLines、moveToCenter 和 zoomToFit 等操作。
每个分组的布局工作被委托给 MyMixLayoutManager。当插入或编辑某个分组时,管理器只会查找具有目标 myGroupId 的节点,将该分组的根节点移动到保存的坐标,使用 fixedRootNode = true 创建一个非主布局实例,然后只针对这一子集运行所选算法。布局编辑器暴露了 center、force、tree、folder、circle 和 fixed,同时还提供算法特定参数和通用对齐设置。
插槽和编辑辅助组件承载了大量交互设计。RGSlotOnView 承载了 RGMiniView、RGEditingNodeController 和 RGEditingReferenceLine,因此被选中的分组可以获得视口级操作按钮、参考线辅助以及实时小地图,而无需修改主图的标记结构。
悬浮的 DraggableWindow 并不只是一个包装器。在创建模式下,它包含随机树生成器、根位置控制和默认分组样式。在编辑模式下,它会切换为带标签页的分组编辑器。其共享设置面板使用 graph instance 和 graph store 来改变滚轮行为、改变拖拽行为,并通过 prepareForImageGeneration() 和 restoreAfterImageGeneration() 导出当前画布图像。
样式一部分通过 relation-graph 属性处理,另一部分通过 CSS 覆盖实现。SCSS 文件让画布呈现出深色网格化工作区的视觉效果,并强制连线标签使用白色背景,这样即使某个分组使用了自定义颜色或动画连线,标签仍然具有良好的可读性。
关键交互
最主要的交互是分组插入。用户可以设置树生成范围,决定根位置是随机还是手动输入,选择默认的节点、连线和布局设置,然后把一个新分组插入到固定画布中。
选中是基于分组而不是基于节点的。点击某个节点会调用 selectGroupNodes(...),它会解析出完整的 myGroupId 同组节点集合,并把这一组传给 setEditingNodes(...)。这样,视口覆盖层和编辑面板就会以整个分组作为操作单位。
随后,可以对被选中的分组执行批量重新布局或批量改样式。布局标签页会修改该分组保存的布局选项,而节点标签页和连线标签页会分别修改同一分组内所有节点或所有连接线的外观。应用这些变更后,只会对该子集重新执行布局。
视口覆盖层还提供两个直接操作:打开分组编辑器,以及删除当前选中的分组。点击空白画布则会通过清除 checked 状态并清空 editing-node 集合,退出当前编辑上下文。
这里还存在工作区级别的交互。设置覆盖层可以把鼠标滚轮行为切换为滚动、缩放或无动作,把拖拽行为切换为选择、移动或无动作,并把当前画布导出为图片。连线点击并不是此处编辑工作流的一部分;当前处理器只会记录被点击的连线。
关键代码片段
下面这个片段展示了主画布被有意设置为固定模式,因此混合布局行为被下放到每个分组的运行时逻辑中,而不是交给一个全局自动布局。
const graphOptions: RGOptions = {
debug: false,
layout: {
layoutName: 'fixed'
}
};
下面这个片段展示了生成出的树数据在加入 relation-graph 之前,如何先被整理成一个可识别的分组。
const treeJsonData: RGJsonData = flattenTreeData(treeRootNode);
treeJsonData.nodes.forEach((node: JsonNode) => {
node.data = { myGroupId: groupItemsDefaultOptions.myGroupId };
});
treeJsonData.lines.forEach((line: JsonLine) => {
line.fromJunctionPoint = groupItemsDefaultOptions.junctionPoint;
line.toJunctionPoint = groupItemsDefaultOptions.junctionPoint;
});
this.graphInstance.addNodes(treeJsonData.nodes);
this.graphInstance.addLines(treeJsonData.lines);
下面这个片段展示了运行时插入流程:生成一个 UUID,构建树数据,附加进去,然后只对新分组执行布局。
const newGroupId = graphInstance.generateNewUUID(8);
const treeRootNode: JsonNode = generateRandomTreeData(generateRandomTreeDataConfig, newGroupId);
const leftGroupInfo = myLayout.current.appendTreeNodeData(
treeRootNode,
{
myGroupId: newGroupId,
junctionPoint: RGJunctionPoint.lr
}
);
await graphInstance.sleep(500);
await myLayout.current.layoutGroupNodes({
下面这个片段展示了选中分组如何围绕自身根节点重新布局,同时仍与主画布布局保持分离。
const groupRootNode = this.graphInstance.getNodeById(groupRootNodeId);
if (groupRootNode) {
this.graphInstance.updateNodePosition(groupRootNode, rootNodeXy.x, rootNodeXy.y);
const myGroupLayout = this.graphInstance.createLayout<InstanceType<typeof RGLayouts.ForceLayout>>(layoutOptions);
myGroupLayout.isMainLayouer = false;
myGroupLayout.layoutOptions.fixedRootNode = true;
myGroupLayout.placeNodes(groupNodes, groupRootNode);
}
下面这个片段展示了节点点击与空白画布点击,如何分别控制进入和退出分组编辑状态。
const onNodeClick = (node: RGNode, $event: RGUserEvent) => {
console.log('Node clicked:', node.id, node);
myLayout.current.selectGroupNodes(node.data?.myGroupId);
if (editingGroupStyles.myGroupId !== node.data?.myGroupId) {
openEditGroupStylesPanel();
}
};
const onCanvasClick = () => {
graphInstance.clearChecked();
graphInstance.setEditingNodes([]);
下面这个片段展示了叠加在图视口之上的工具层:小地图、分组操作以及参考线辅助。
<RelationGraph
options={graphOptions}
onNodeClick={onNodeClick}
onLineClick={onLineClick}
onCanvasClick={onCanvasClick}
>
<RGSlotOnView>
<RGMiniView />
<RGEditingNodeController>
下面这个片段展示了共享画布设置路径中的导出能力,以及实时交互模式切换。
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();
这个示例的独特之处
根据准备好的对比数据,这个示例最突出的阅读方式是把它看作一种多分组创作模式,而不是一个通用布局演示。它不常见的特征在于:固定的外层画布与运行时插入多个随机树分组相结合,而每个分组都保存了各自的根坐标、布局选项、节点样式和连线样式。
与 batch-operations-on-nodes 相比,这里的选中模型更有明确倾向。那个示例是对任意被选中的节点做批量编辑,而这个示例则通过 myGroupId 把一次节点点击提升为整组选择,然后把被选中的子图当作一个经过设计的模块来处理。
与 gee-node-alignment-guides 相比,参考线在这里属于辅助基础设施,而不是主要教学点。这里更核心的内容是:如何在一个紧凑的编辑界面中完成分组插入、分组重布局、分组改样式和分组删除的完整流程。
与 layout-center 相比,这个示例并不是要展示如何对一份数据应用一种布局,而是强调如何逐步组合多个图孤岛。与 undo-redo-example 相比,它也比一个完整的自由编辑器更聚焦:它并不关注任意边的创建或历史管理,而是关注分组树网络的重复插入与批量编辑。
这使它成为一个特别有力的起点,适合那些需要半结构化图工作区的团队:它比静态演示更动态,但又比完整图编辑器更轻量。
这种模式还适用于哪些场景
这种模式可以复用于架构工作台,团队可以把多个服务集群放到同一块画板上,并分别调整每个集群。它也适用于规划工具,在那里,每个插入的分组都可以表示一个部门、一条项目流,或一张需要拥有自身局部布局的能力地图。
另一个扩展方向是把它作为可复用图模块的可视化组合界面。产品可以允许用户把带样式的子图保存为模板,将其插入到更大的画布中,然后继续通过这里展示的同组选择与重新布局流程来编辑每个插入模块。
它同样适合迁移到教育或仿真类工具中。生成器不必输出随机树,而可以生成场景包、依赖集合或课程专用图片段,同时固定的外层画布仍然作为统一工作区,用于排列、比较并导出这些结果分组。