人物关系图交互操作
这个示例构建了一个力导向的人物关系图,它的行为更像一个轻量级编辑工作区,而不是只读的网络视图。画布以一份预先准备好的角色关系数据集为起点,将每个人渲染为圆形头像节点,并在选中节点上叠加一个以节点为中心的径向菜单,以及一个固定在侧边的节点编辑面板。
以节点为中心操作的人物关系图
这个示例构建了什么
这个示例构建了一个力导向的人物关系图,它的行为更像一个轻量级编辑工作区,而不是只读的网络视图。画布以一份预先准备好的角色关系数据集为起点,将每个人渲染为圆形头像节点,并在选中节点上叠加一个以节点为中心的径向菜单,以及一个固定在侧边的节点编辑面板。
用户可以点击节点打开上下文操作,在原位重命名节点、复制节点、添加新的子节点、启动交互式连线创建,或打开一个详情卡片,实时编辑颜色和头像 URL。最重要的一点是,这个覆盖层 UI 并不是脱离图状态独立存在的:它锚定在被选中的节点上,并通过 relation-graph 实例 API 驱动真实的图变更。
数据是如何组织的
数据集在 mock-data-api.ts 中被模拟为异步 RGJsonData。它定义了 rootId: "N13"、一组人物节点,以及带标签的关系线,例如 friend、relative、lover、collusion、corruption 和 report。每个节点都包含 id、text、color、borderColor,以及一个 data 对象,其中包含 icon、sexType 和 isGoodMan 等字段。每条线都使用 from、to、text、color、fontColor 和 data.type 值。
图加载前没有任何预处理。fetchJsonData() 会在一个短暂的超时后返回内联对象,而 initializeGraph() 会将该结果直接传给 setJsonData(...)。在真实应用中,同样的数据结构可以表示人物、客户、部门、设备、欺诈实体或案件参与方,而 data.icon 和这些关系标签则可以替换为特定领域的元数据。
这个示例还受益于同一端点之间的重复连线,这也是 multiLineDistance 很重要的原因。例如,模拟数据中包含同一组角色之间的多种关系,因此图中需要让平行线之间保持可见间距,而不是塌缩成一条难以辨认的路径。
relation-graph 的使用方式
这个 demo 包裹在 RGProvider 中,然后 MyGraph 通过 RGHooks.useGraphInstance() 读取实时图实例。图配置项设置了一个力导向布局,包含 maxLayoutTimes: 50、直线连线、圆形默认节点、沿路径绘制的线文本、multiLineDistance: 20、2px 节点边框,以及兜底节点颜色 #e85f84。
加载流程直接使用 relation-graph 实例 API。在模拟请求返回后,组件依次调用 loading()、setJsonData(...)、clearLoading()、moveToCenter() 和 zoomToFit()。两秒后,代码会以编程方式打开节点 N3,让编辑入口直接可见,而不需要用户先自行发现它。
渲染层依赖两个 slot API。RGSlotOnNode 会替换默认的节点主体,改为头像圆形和放在下方的标签。RGSlotOnView 则会在画布上方渲染浮动的径向菜单和固定侧边卡片,这让示例能够把图原生布局与自定义 React UI 覆盖层结合起来。
变更工作流同样保持在 relation-graph 内部。菜单使用 generateNewNodeId()、addNodes(...)、addLines(...) 和 startAutoLayout() 来复制节点或追加随机子节点。它还使用 updateNode(...) 和 updateNodeData(...) 将实时编辑结果写回当前选中节点,并通过 startCreatingLinePlot(...) 从当前节点进入交互式边创建模式。
共享的 DraggableWindow 组件为图外再加了一层辅助工作区。它使用 RGHooks.useGraphStore() 读取当前交互模式,并使用 setOptions(...) 在运行时切换滚轮行为和画布拖拽行为。它还演示了如何使用 prepareForImageGeneration()、getOptions() 和 restoreAfterImageGeneration() 导出图画布截图。
样式通过本地 SCSS 做了大量定制。画布使用平铺背景图,被选中的节点会显示颜色光晕和标签胶囊,被选中的线会反转标签颜色,展开按钮被重新着色为蓝色,而径向菜单则会借助自定义 SVG 象限动画进入,并在菜单圆环下方放置一个文本输入框。
关键交互
点击节点会打开径向菜单,并将该节点标记为当前编辑目标。菜单并不是固定在页面上的;每当触发缩放、节点拖拽、画布拖拽或画布拖拽结束事件时,它都会重新定位,因此能够持续跟随当前选中节点在视图坐标中的位置。
点击空白画布会清除 relation-graph 的 checked 状态,并关闭两个覆盖层。这个重置行为很重要,因为否则在用户把注意力从当前选中节点移开后,菜单或信息卡片可能仍然悬浮在界面上。
四个径向操作分别驱动不同的运行时行为。信息操作会切换固定详情卡片。复制操作会复制当前节点,包括它的颜色和 data 负载。添加子节点操作会创建一到八个新的相连节点。连线操作会进入 relation-graph 的交互式 line-plot 模式,并在用户最终连到另一个节点时创建一条新的带标签边。
这里有两条实时文本编辑路径。径向菜单下方的输入框会在用户输入时直接写入选中节点的 text 字段,而侧边卡片则通过 updateNode(...) 和 updateNodeData(...) 编辑文本、填充色、边框色和头像 URL。
可拖拽的辅助窗口增加了一组与整个图相关、但独立于节点编辑的控制项。用户可以移动或最小化该窗口、打开设置面板、在滚动、缩放和禁用之间切换滚轮交互、在框选、移动和禁用之间切换拖拽行为,并将当前图导出为图片。
关键代码片段
这段配置建立了力导向布局以及视觉默认值,使头像关系网络具备良好的可读性。
const graphOptions: RGOptions = {
debug: false,
defaultLineShape: RGLineShape.StandardStraight,
defaultNodeShape: RGNodeShape.circle,
defaultLineTextOnPath: true,
multiLineDistance: 20,
layout: {
layoutName: 'force',
maxLayoutTimes: 50
},
defaultNodeBorderWidth: 2,
defaultNodeColor: '#e85f84'
};
这段初始化流程说明,该示例会把模拟的 RGJsonData 直接加载进 relation-graph,然后再调整图视口焦点。
const initializeGraph = async () => {
const myJsonData: RGJsonData = await fetchJsonData();
graphInstance.loading();
await graphInstance.setJsonData(myJsonData);
graphInstance.clearLoading();
graphInstance.moveToCenter();
graphInstance.zoomToFit();
};
这个函数证明菜单是锚定在图空间中的,而不是简单地放在上一次点击位置附近。
const updateNodeMenuPosition = () => {
if (showNodeMenu && currentNode) {
const rgNode = graphInstance.getNodeById(currentNode.id);
if (rgNode) {
const viewCoordinate = graphInstance.getViewXyByCanvasXy({
x: rgNode.x + rgNode.el_W / 2,
y: rgNode.y + rgNode.el_H / 2
});
setNodeMenuPanel({
x: viewCoordinate.x - nodeMenuPanel.width / 2,
y: viewCoordinate.y - nodeMenuPanel.height / 2,
width: nodeMenuPanel.width,
height: nodeMenuPanel.height
});
}
}
};
这段变更逻辑展示了菜单如何在原地扩展图,并立即重新启动力导向布局。
const randomChildrenCount = Math.ceil(Math.random() * 8);
const newNodes: JsonNode[] = [];
const newLines: JsonLine[] = [];
for (let i = 0; i < randomChildrenCount; i++) {
const newNodeId = graphInstance.generateNewNodeId();
newNodes.push({
id: newNodeId,
text: 'New Node',
x: currentNode.x + 200,
y: currentNode.y + (Math.random() * 200 - 100)
});
newLines.push({ id: 'line-to-' + newNodeId, from: currentNode.id, to: newNodeId });
}
这组 slot 组合是这里的核心 UI 模式:自定义节点渲染,加上由选中状态驱动的视图级覆盖层。
<RGSlotOnNode>
{({ node }: RGNodeSlotProps) => (
<div className="w-12 h-12 flex place-items-center justify-center">
<div className="my-node-avatar" style={{ backgroundImage: `url(${node.data?.icon})` }} />
<div className="my-node-name absolute transform translate-y-[35px]">{node.text}</div>
</div>
)}
</RGSlotOnNode>
<RGSlotOnView>
{showNodeMenu && currentNode && (
<MyNodeMenus
这个示例的独特之处
与附近的 effect-and-control 这类示例相比,这个 demo 的重心从全局图控制转向了节点局部操作。这里可复用的经验不是视口管理或全局调优,而是如何让选中节点成为重命名、复制、图扩展、边创建和实时属性编辑的发起点。
与 custom-node-quick-actions 相比,它不止是在做覆盖层定位,而是把覆盖层变成了真正的编辑界面。径向菜单直接绑定具体变更,而固定侧边卡片则把同一次选中进一步延展为一个更丰富的头像 URL 与颜色属性编辑器。
与 amount-summarizer 相比,它不像一个完整的画布创作工具,更像是一个针对已加载网络的原位编辑器。对于那些已经拥有关系数据、只需要在图上叠加上下文编辑能力,而不是从空白画布开始建模的团队来说,这是一个很好的起点。
这些对比也说明它支持一种较少见的功能组合:基于头像的关系展示、编辑后的力导向重新布局、跟随节点的径向操作菜单、实时节点属性编辑,以及交互式边创建,并且都集中在同一个示例中。当需求是把结构化图操作直接挂接到选中节点上时,这种组合让这个 demo 比只演示菜单或样式的参考示例更有价值。
这种模式还适用于哪里
这种模式也很适合客户关系探索工具,分析人员需要在不离开图画布的情况下检查某个实体、调整显示属性,并添加新的连线或相关实体。
它同样适用于调查与案件管理图谱,在这类场景中,选中的嫌疑人、账户、设备或文档需要一个局部操作菜单,用于关联证据、复制节点或追加新发现的相关实体。
另一个很好的扩展方向是组织结构或项目依赖关系映射。选中的团队、服务或工作项可以打开一个以节点为中心的操作环,用于添加依赖项、编辑元数据或发起新的连接,而侧边卡片则负责处理更丰富的属性更新。