点击节点高亮关联节点分组
这个示例构建了一个力导布局网络查看器,使用持久化的 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(),并根据每个分组节点当前的 x、y、el_W 和 el_H 重新计算一个包围矩形。由于这个矩形是根据节点的实时几何信息推导出来的,因此在图谱渲染和移动过程中,覆盖层会始终保持对齐。SCSS 文件只包含空选择器,因此最终行为主要依赖于 relation-graph 选项、全高包裹容器,以及自定义的 SVG 覆盖组件。
关键交互
第一个有意义的交互会在挂载时自动发生。图数据加载完成并设置缩放后,示例会为 d1、b4 和 c1 预置分组,因此用户在点击之前就能立即看到邻域覆盖层的效果。
核心用户交互是节点点击。点击节点后,会把该节点 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-options 和 layout-force 相比,力导求解器在这里是支撑基础设施,而不是页面主题。这个示例真正可复用的经验,是如何在一个实时图上实现点击驱动的邻域标注,包括默认预置分组、去重状态管理,以及最多保留三个覆盖层的滚动历史。
稀有性数据还强调了一种不常见的组合:紫色的 60 像素圆形节点、持续运行的力导运动、点击驱动的相关节点分组、随机的半透明轮廓颜色,以及根据当前节点几何信息重新计算的矩形边界。这些选择共同让这个示例成为一个聚焦于轻量级多邻域对比的参考,而不是用于力导布局调优或图编辑。
这种模式还能用于哪里
这种模式非常适合服务拓扑查看器:用户点击一个服务后,希望同时保留多个影响邻域的可视化结果,而不需要给整个图谱重新着色。
它也适用于欺诈审查、调查分析和安全研判工具,此时分析人员需要并排比较几个相关节点簇,同时保留原始节点和边的样式不变。
当需求是提供一种会跟随实时布局变化的临时局部强调效果,并且视觉上比填充区域或完整的选区管理系统更轻量时,这种方法同样适用于知识图谱、推荐图谱和内部依赖关系图。