JavaScript is required

节点邻域分组背景

这个示例加载力导关系图,预置三个邻域分组,并允许用户点击任意节点创建新的邻域高亮。每个高亮都会在画布层绘制为基于实时节点边界的 SVG 区域:单节点组用矩形,多节点组用凸包轮廓。

在力导向图中绘制实时邻域背景区域

此示例构建的内容

这个示例构建了一个全高的 relation graph 查看器,具备力导向运动效果和持久化的邻域高亮。画布初始时就会显示三个已保存的分组,每个分组都会作为半透明的绿色背景,出现在一组相关节点的后方。

用户点击任意节点时,都可以为该节点及其相关节点创建另一个已保存的高亮区域。这里最重要的思路是,分组效果并不会直接重设节点或连线的样式,而是额外添加一个独立的画布层叠加层,并让它跟随节点的实时位置变化。

数据如何组织

图数据以内联 RGJsonData 的形式声明在 initializeGraph() 中。每个节点只保存 idtext,每条连线一开始只是简单的 { from, to } 结构,之后再通过最后一次 .map(...) 处理生成所需的连线 id。

在调用 setJsonData(...) 之前几乎没有任何预处理。唯一的数据变换就是自动生成连线 id,而分组状态完全没有嵌入到数据集中。分组成员是在运行时通过 getNodeById(...)getNodeRelatedNodes(...) 推导出来的,然后以 { groupId, groupNodes } 数组的形式保存在本地 React state 中。

在生产环境的图中,这种结构同样可以表示依赖邻域、相关客户记录、风险簇,或分析人员希望临时对比的局部子图,而无需修改底层图数据。

relation-graph 的使用方式

示例整体包裹在 RGProvider 中,MyGraph 使用 RGHooks.useGraphInstance() 作为主要控制入口。图被配置为力导向布局,设置了 layoutName: 'force'maxLayoutTimes: Number.MAX_SAFE_INTEGER,因此布局运动会持续进行,直到清理阶段将其停止。默认样式通过图选项设置,而不是通过自定义节点模板实现:无边框圆形节点、60 x 60 尺寸、直线连线、边框连接点,以及紫色填充色。

图实例 API 负责整个生命周期。setJsonData(...) 用于加载内联数据集,setZoom(30) 用于建立初始视口,getNodeById(...)getNodeRelatedNodes(...) 用于构建每个已保存的邻域,而 stopAutoLayout() 会在组件卸载时执行。图本身的渲染表面大部分保持默认状态。

主要的自定义发生在 RGSlotOnCanvas。每个已保存分组都由 GroupAreaBackground 渲染,它使用 RGHooks.useGraphStore() 监听 shouldRenderNodes,并根据当前节点坐标和测量得到的节点尺寸重新计算 SVG 几何形状。该组件对于单节点分组会生成简单矩形,对于多节点分组则会生成凸包多边形。本地 SCSS 几乎没有增加额外样式,因此这个示例的重点始终放在图行为而不是界面装饰上。

关键交互

  • 图在数据加载后会立即调用 createNodeGroup('d1')createNodeGroup('b4')createNodeGroup('c1'),预先生成三个可见邻域。
  • 点击节点时会触发 onNodeClick,它会把被点击节点作为分组根节点,并通过 getNodeRelatedNodes(...) 扩展出该保存分组。
  • 如果点击的节点所属分组已经存在,则不会发生任何变化,因此叠加层列表始终保持去重状态。
  • 已保存分组列表最多保留三项。加入第四个不同分组时,最旧的那个会被移除。
  • 背景几何会跟随当前图状态变化,因为该背景组件会在节点渲染更新发生时重新计算路径。

关键代码片段

这个 options 配置块表明,该示例是一个使用圆形节点和直线边的力导向布局查看器。

const graphOptions: RGOptions = {
    debug: false,
    defaultNodeBorderWidth: 0,
    defaultLineShape: RGLineShape.StandardStraight,
    defaultNodeColor: 'rgb(130,102,246)',
    defaultNodeShape: RGNodeShape.circle,
    defaultNodeWidth: 60,
    defaultNodeHeight: 60,
    layout: {
        layoutName: 'force',
        maxLayoutTimes: Number.MAX_SAFE_INTEGER
    },
    defaultJunctionPoint: RGJunctionPoint.border
};

这一步连线处理让源数据集保持简洁,只在加载时补充 id。

lines: [
    { from: 'a', to: 'b' }, { from: 'b', to: 'b1' },
    // ...
    { from: 'e2', to: 'e2-8' }, { from: 'e2', to: 'e2-9' }
].map((line, idx) => ({ ...line, id: `line-${idx}` }))

这个初始化流程会加载图、设置缩放级别,并预先填入三个已保存的叠加层。

await graphInstance.setJsonData(myJsonData);
graphInstance.setZoom(30);

createNodeGroup('d1');
createNodeGroup('b4');
createNodeGroup('c1');

这个处理函数就是分组逻辑的核心:它会解析被点击的节点、将其与相关节点扩展成一组,并且只保留最新的三个唯一分组。

const createNodeGroup = (nodeId: string) => {
    const groupCoreNode = graphInstance.getNodeById(nodeId);
    if (!groupCoreNode) return;
    const relatedNodes = graphInstance.getNodeRelatedNodes(groupCoreNode);
    const groupNodes = [groupCoreNode, ...relatedNodes];
    const groupId = 'my-group-' + groupCoreNode.id;

    setMyNodeGroups(prev => {
        if (prev.some(g => g.groupId === groupId)) return prev;
        const nextGroups = [...prev, { groupId, groupNodes }];
        return nextGroups.length > 3 ? nextGroups.slice(1) : nextGroups;
    });
};

这个 slot 接线方式让背景层与主图渲染器保持分离。

<RelationGraph
    options={graphOptions}
    onNodeClick={onNodeClick}
>
    <RGSlotOnCanvas>
        {myNodeGroups.map(thisGroup => (
            <GroupAreaBackground key={thisGroup.groupId} group={thisGroup} />
        ))}
    </RGSlotOnCanvas>
</RelationGraph>

这个路径生成函数会在单节点时切换为矩形,在多节点分组时切换为凸包轮廓。

if (nodes.length === 1) {
    const { x, y, width, height } = nodes[0];
    return `M${x},${y} h${width} v${height} h${-width} Z`;
}

const hullPoints = convexHull(points);
if (hullPoints.length < 3) return '';
return `M${hullPoints[0].x},${hullPoints[0].y} ${hullPoints.slice(1).map(p => `L${p.x},${p.y}`).join(' ')} Z`;

这个 hook 会在 relation-graph 报告节点渲染更新时,根据当前节点边界重新计算叠加层。

const {shouldRenderNodes} = RGHooks.useGraphStore();
const groupArea = useMemo(() => {
    return generateSVGPath(group.groupNodes.map((node: RGNode) => ({
        x: node.x,
        y: node.y,
        width: node.el_W || 60,
        height: node.el_H || 40
    })));
}, [shouldRenderNodes]);

这个示例的独特之处

nodes-group-rect-background 相比,这个示例关注的是区域轮廓几何,而不是一个共享的边界框。对比数据表明,它最主要的区别在于:它会根据节点角点把分组节点转成填充区域,并为单节点提供矩形回退,而不是在整个邻域外绘制一个带内边距的圆角矩形。

layout-force-options 相比,力导向求解器在这里是支撑性基础设施,而不是演示主题。这里真正独特的要点,是一个持续运动的力导向图如何承载由相关节点邻域推导出的区域叠加层。

built-in-slots 相比,这并不是一个通用图层对比示例。它使用 RGSlotOnCanvas 只完成一个非常具体的任务:渲染会跟踪当前节点位置的 SVG 几何图形。这里罕见的组合才是重点:预置分组、点击后按相关节点分组、三组去重保留,以及在力导向图上方实时绘制凸包叠加层。

这一模式还适用于哪些场景

这一模式非常适合那些需要临时强调邻域、但又不希望修改节点或连线样式的图视图。示例场景包括服务依赖排查、欺诈环探索、攻击路径审查,以及关系审计类工作流。

对于需要使用簇形标注、而不是逐节点高亮的知识图谱或网络分析工具,这同样是一种实用模式。一个已保存叠加层可以表示“与该实体相关的记录”“受此事件影响的系统”或“参与这条依赖链的节点”,而底层图仍然可以持续运动。

最后,当团队需要基于运行时几何而不是数据集内存储信息来生成图注释时,这种方法也很有用。可复用的核心思路是:将分组状态保存在图数据之外,并根据画布层中节点的当前边界推导出可见区域。