JavaScript is required

点击节点高亮关联节点分组

这个示例构建了一个力导布局网络查看器,使用持久化的 canvas 层矩形来标记节点邻域。图谱初始会预置三个高亮分组,每个已保存的分组都会围绕一个核心节点以及 relation-graph 返回的相关节点,绘制一个圆角半透明轮廓。

使用 Canvas 矩形实现点击驱动的相关节点分组高亮

这个示例构建了什么

这个示例构建了一个力导布局网络查看器,使用持久化的 canvas 层矩形来标记节点邻域。图谱初始会预置三个高亮分组,每个已保存的分组都会围绕一个核心节点以及 relation-graph 返回的相关节点,绘制一个圆角半透明轮廓。

用户可以继续点击更多节点来新增一个高亮邻域,同时对比最多三个分组,并且仍然可以继续与图谱交互,因为这些覆盖层不会捕获指针输入。这个示例的重点在于,高亮是通过一个独立的 SVG 覆盖层来实现的,而不是直接修改节点和连线样式。

数据如何组织

基础图数据是一个内联的 RGJsonData 对象,其中包含 rootId、扁平的 nodes 数组和扁平的 lines 数组。内容是一个以 a 为根节点的通用分支网络,因此这个示例可复用的价值不在于领域建模,而在于叠加在普通 relation-graph 数据集之上的运行时分组模式。

在执行 setJsonData(...) 之前,唯一的预处理步骤是为每条连线对象添加 id: line-${idx}。可见的高亮区域并不编码在 RGJsonData 里;它们存放在单独的 myNodeGroups 状态数组中,其中每一项都会保存生成出来的 groupId、推导得到的 groupNodes,以及一个随机的半透明描边颜色。

这种分离在真实应用里很重要。相同的结构可以叠加在服务依赖图、调查图谱、组织关系图或推荐网络上,此时图数据本身是稳定的,但临时性的邻域强调是由用户操作动态推导出来的。

relation-graph 是如何使用的

页面包裹在 RGProvider 中,MyGraph 则使用 RGHooks.useGraphInstance() 作为主要的运行时 API 入口。这个示例通过 setJsonData(...) 加载数据,通过 setZoom(30) 设置初始缩放,通过 getNodeById(...)getNodeRelatedNodes(...) 推导分组,并在清理阶段调用 stopAutoLayout() 停止持续运行的力导布局。

图谱选项让内置渲染保持简洁:layoutName: 'force'、直线连线、边框连接点、紫色圆形节点,以及默认 60x60 的节点尺寸。这里没有使用编辑 API,没有自定义节点模板,也没有自定义连线渲染器。真正的定制点是 RGSlotOnCanvas,它会为每个已保存分组挂载一个 GroupRectBackground 组件。

GroupRectBackground 使用 RGHooks.useGraphStore(),并根据每个分组节点当前的 xyel_Wel_H 重新计算一个包围矩形。由于这个矩形是根据节点的实时几何信息推导出来的,因此在图谱渲染和移动过程中,覆盖层会始终保持对齐。SCSS 文件只包含空选择器,因此最终行为主要依赖于 relation-graph 选项、全高包裹容器,以及自定义的 SVG 覆盖组件。

关键交互

第一个有意义的交互会在挂载时自动发生。图数据加载完成并设置缩放后,示例会为 d1b4c1 预置分组,因此用户在点击之前就能立即看到邻域覆盖层的效果。

核心用户交互是节点点击。点击节点后,会把该节点 id 传给 createNodeGroup(...),它会向图实例请求被点击的节点及其相关节点,然后在该核心节点尚未被表示的情况下保存一个新的分组。

分组保留数量被有意限制。重复的分组 id 会被忽略;当加入第四个不同分组时,最早保存的那个分组会被移除,因此界面上始终只保留最新的三个覆盖层。

这些覆盖层不会阻塞图谱交互,因为 SVG 容器使用了 pointerEvents: 'none'。这使得这些矩形成为一个视觉标注层,而不是一个会争夺交互的覆盖层。

关键代码片段

这个片段展示了默认图谱配置:内置的力导布局、连接到边框的直线连线,以及 60 像素的圆形节点。

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
};

这个片段说明,数据集是以命令式方式加载的,初始缩放会在加载后设置,并且会立即预置三个示例分组。

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,
            mainColor: generateRandomPastelColor()
        }];
        return nextGroups.length > 3 ? nextGroups.slice(1) : nextGroups;
    });
};

这个片段说明,自定义覆盖层是通过 relation-graph 的 canvas 插槽来渲染的,而不是通过修改节点或连线样式来实现。

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

这个片段展示了覆盖矩形如何从实时节点边界推导出来,并额外增加留白,使轮廓能够随着图谱移动而同步跟随。

const groupBounds = useMemo(() => {
    if (!group.groupNodes || group.groupNodes.length === 0) return null;
    let minX = Infinity, minY = Infinity;
    let maxX = -Infinity, maxY = -Infinity;

    group.groupNodes.forEach(node => {
        minX = Math.min(minX, node.x);
        minY = Math.min(minY, node.y);
        maxX = Math.max(maxX, node.x + (node.el_W || 0));
        maxY = Math.max(maxY, node.y + (node.el_H || 0));
    });

    const padding = 10;

这个片段说明,这个矩形是一个不填充、不可交互的 SVG 轮廓,而不是一个填充的区域。

<svg
    className="my-group-svg-bg"
    style={{
        position: 'absolute',
        left: 0,
        top: 0,
        width: '20px',
        height: '20px',
        pointerEvents: 'none',
        overflow: 'visible'
    }}
>
    <rect
        x={groupBounds.x}
        y={groupBounds.y}
        width={groupBounds.width}
        height={groupBounds.height}
        rx="10"
        ry="10"
        fill="none"
        stroke={group.mainColor}
        strokeWidth="10"
    />
</svg>

这个示例的独特之处

对比数据表明,这个示例并不只是另一个力导布局演示。它最突出的模式,是把 RGSlotOnCanvas、运行时相关节点查询,以及专门用于包裹每个已保存邻域的 GroupRectBackground 组件结合在一起,并用一个感知布局变化的 SVG 矩形来呈现。

nodes-group-area-background 相比,数据加载流程和分组逻辑非常相似,但几何形态和视觉重量不同。这个示例始终只渲染一个带留白的圆角轮廓矩形,并使用 fill="none",而对应的兄弟示例则更强调填充区域覆盖和更复杂的区域几何。因此,当图内部内容必须保持清晰可见时,这个示例更适合作为起点。

layout-force-optionslayout-force 相比,力导求解器在这里是支撑基础设施,而不是页面主题。这个示例真正可复用的经验,是如何在一个实时图上实现点击驱动的邻域标注,包括默认预置分组、去重状态管理,以及最多保留三个覆盖层的滚动历史。

稀有性数据还强调了一种不常见的组合:紫色的 60 像素圆形节点、持续运行的力导运动、点击驱动的相关节点分组、随机的半透明轮廓颜色,以及根据当前节点几何信息重新计算的矩形边界。这些选择共同让这个示例成为一个聚焦于轻量级多邻域对比的参考,而不是用于力导布局调优或图编辑。

这种模式还能用于哪里

这种模式非常适合服务拓扑查看器:用户点击一个服务后,希望同时保留多个影响邻域的可视化结果,而不需要给整个图谱重新着色。

它也适用于欺诈审查、调查分析和安全研判工具,此时分析人员需要并排比较几个相关节点簇,同时保留原始节点和边的样式不变。

当需求是提供一种会跟随实时布局变化的临时局部强调效果,并且视觉上比填充区域或完整的选区管理系统更轻量时,这种方法同样适用于知识图谱、推荐图谱和内部依赖关系图。