JavaScript is required

人物关系图交互操作

这个示例构建了一个力导向的人物关系图,它的行为更像一个轻量级编辑工作区,而不是只读的网络视图。画布以一份预先准备好的角色关系数据集为起点,将每个人渲染为圆形头像节点,并在选中节点上叠加一个以节点为中心的径向菜单,以及一个固定在侧边的节点编辑面板。

以节点为中心操作的人物关系图

这个示例构建了什么

这个示例构建了一个力导向的人物关系图,它的行为更像一个轻量级编辑工作区,而不是只读的网络视图。画布以一份预先准备好的角色关系数据集为起点,将每个人渲染为圆形头像节点,并在选中节点上叠加一个以节点为中心的径向菜单,以及一个固定在侧边的节点编辑面板。

用户可以点击节点打开上下文操作,在原位重命名节点、复制节点、添加新的子节点、启动交互式连线创建,或打开一个详情卡片,实时编辑颜色和头像 URL。最重要的一点是,这个覆盖层 UI 并不是脱离图状态独立存在的:它锚定在被选中的节点上,并通过 relation-graph 实例 API 驱动真实的图变更。

数据是如何组织的

数据集在 mock-data-api.ts 中被模拟为异步 RGJsonData。它定义了 rootId: "N13"、一组人物节点,以及带标签的关系线,例如 friendrelativelovercollusioncorruptionreport。每个节点都包含 idtextcolorborderColor,以及一个 data 对象,其中包含 iconsexTypeisGoodMan 等字段。每条线都使用 fromtotextcolorfontColordata.type 值。

图加载前没有任何预处理。fetchJsonData() 会在一个短暂的超时后返回内联对象,而 initializeGraph() 会将该结果直接传给 setJsonData(...)。在真实应用中,同样的数据结构可以表示人物、客户、部门、设备、欺诈实体或案件参与方,而 data.icon 和这些关系标签则可以替换为特定领域的元数据。

这个示例还受益于同一端点之间的重复连线,这也是 multiLineDistance 很重要的原因。例如,模拟数据中包含同一组角色之间的多种关系,因此图中需要让平行线之间保持可见间距,而不是塌缩成一条难以辨认的路径。

relation-graph 的使用方式

这个 demo 包裹在 RGProvider 中,然后 MyGraph 通过 RGHooks.useGraphInstance() 读取实时图实例。图配置项设置了一个力导向布局,包含 maxLayoutTimes: 50、直线连线、圆形默认节点、沿路径绘制的线文本、multiLineDistance: 202px 节点边框,以及兜底节点颜色 #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 比只演示菜单或样式的参考示例更有价值。

这种模式还适用于哪里

这种模式也很适合客户关系探索工具,分析人员需要在不离开图画布的情况下检查某个实体、调整显示属性,并添加新的连线或相关实体。

它同样适用于调查与案件管理图谱,在这类场景中,选中的嫌疑人、账户、设备或文档需要一个局部操作菜单,用于关联证据、复制节点或追加新发现的相关实体。

另一个很好的扩展方向是组织结构或项目依赖关系映射。选中的团队、服务或工作项可以打开一个以节点为中心的操作环,用于添加依赖项、编辑元数据或发起新的连接,而侧边卡片则负责处理更丰富的属性更新。