JavaScript is required

节点悬停高亮关联线条

这个示例渲染一棵自上而下树图,仅高亮悬停节点及其直接连接连线。它演示 `RGSlotOnNode` 悬停事件、通过 `getLines()` 进行运行时邻接查询、临时节点与连线 class 更新、SCSS 动画线样式,以及共享悬浮工具面板的画布设置与图片导出。

节点悬停时高亮相关连线

此示例构建了什么

这个示例构建了一个全高树形查看器,当指针移到某个节点上时,会强调该节点的直接关系。屏幕上展示的是一个自上而下的小型层级结构,包含以字母标记的条目、圆角紫色节点标签,以及正交连接线,画布上方还叠加了一个悬浮辅助窗口。

最主要的可见效果是临时关系高亮。悬停某个节点时,会为该节点添加明显的光晕,并仅为直接连接到它的连线添加动画,而树中的其余部分保持基础样式。指针移开后会立即清除这部分临时状态。

关键点在于,这个示例不会重建图谱,也不会替换内置的边渲染器。它使用 slot 级别的悬停处理器,再结合运行时图实例更新,来重设已经加载好的节点和连线样式。

数据是如何组织的

图数据直接在 initializeGraph 中以内联方式声明为一个 RGJsonData 对象,其中包含一个 rootId、一个扁平的 nodes 数组,以及一个扁平的 lines 数组。每个节点都有 id、显示文本 text,以及一个很小的 data.icon 负载。每条连线都有自己的 idfromto、标签文本,以及额外的 data.myLineId 字段。

setJsonData() 之前没有预处理步骤。数据直接在组件中组装,只加载一次,之后所有行为都由 getNodes()getLines() 返回的运行时图对象驱动。邻居查找发生在悬停时,通过检查某条线的 fromto 是否匹配当前活动节点 id 来完成。

在生产环境图谱中,这种结构同样可以表示汇报关系、依赖树、工作流分支、分类层级,或任何其他有向父子结构,适用于用户需要快速查看一跳上下文而不是做深度遍历的场景。

relation-graph 是如何使用的

index.tsxRGProvider 包裹了这个示例,这样主图组件和共享辅助窗口都可以通过 hooks 读取同一个图上下文。在 MyGraph 内部,RGHooks.useGraphInstance() 被用来通过 setJsonData() 加载数据、通过 zoomToFit() 让视口自适应、通过 getNodes()getLines() 检查当前图对象,并通过 updateNode()updateLine() 应用临时视觉变化。

图配置将这个示例固定为一个向下展开的树。配置使用了 layoutName: 'tree'from: 'top',并在横向和纵向都设置了 150 像素间距,以便被高亮的连接线有足够空间清晰展示。它还将内置连线渲染器切换为 RGLineShape.SimpleOrthogonal 的圆角正交线段,把这些连线锚定在顶部和底部的连接点,并将内置工具栏放在右下角。

节点外观被有意压缩到最低限度。默认节点填充色和边框都被设为透明,实际可见的节点主体则由 RGSlotOnNode 提供。这个 slot 会在 node.text 外渲染一个圆角紫色包裹层,并绑定 onMouseEnteronMouseLeave 处理器,因此标准 DOM 指针事件就成为驱动全图关系高亮的触发器。

样式分为运行时状态和 SCSS 覆盖两部分。运行时代码会把诸如 my-node-highlightmy-line-highlight 这样的 className 写到 relation-graph 内部的节点和连线包裹元素上,而 my-relation-graph.scss 会把这些类转换成节点光晕、动画连线描边以及深色标签背景。共享的 DraggableWindow 组件则使用 RGHooks.useGraphStore()useGraphInstance() 来暴露画布拖拽模式、滚轮模式和图片导出能力,但这些工具更多属于辅助脚手架,而不是本示例的核心内容。

关键交互

主要交互是节点悬停。进入某个节点时,会先清除之前的所有高亮,再标记当前悬停节点,然后在当前图实例中找出所有与之相连的连线,并把这些连线切换到更粗、带动画的状态。

重置路径则是节点离开。一旦指针离开 slot 渲染的节点主体,示例就会移除所有节点和所有连线上的高亮类,并恢复默认线宽,因此同一时间只能有一个节点的邻域处于激活状态。

悬浮辅助窗口提供了次级控制。它可以被拖动、最小化、切换到设置面板、在运行时更改滚轮和画布拖拽行为,也可以在 relation-graph 为截图准备好画布之后导出图片。

关键代码片段

下面这个片段展示了透明节点外观、正交连线和固定工具栏位置的组合,它们共同定义了图谱的基础行为。

const graphOptions: RGOptions = {
    defaultNodeColor: 'transparent',
    defaultNodeBorderWidth: 0,
    defaultNodeBorderColor: 'transparent',
    defaultLineColor: 'rgba(128, 128, 255)',
    defaultNodeShape: RGNodeShape.rect,
    toolBarDirection: 'h',
    toolBarPositionH: 'right',
    toolBarPositionV: 'bottom',
    defaultPolyLineRadius: 10,
    defaultLineShape: RGLineShape.SimpleOrthogonal,
    defaultLineWidth: 1,

下面这个片段将图谱固定为一个宽阔的自上而下树形布局,而不是在运行时切换布局。

defaultJunctionPoint: RGJunctionPoint.tb,
layout: {
    layoutName: 'tree',
    from: 'top',
    treeNodeGapH: 150,
    treeNodeGapV: 150,
}

下面这个片段展示了数据如何以内联树的形式加载,其中包含显式的连线 id 和每条线的自定义元数据。

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'a', data: { icon: 'align_bottom' } },
        { id: 'b', text: 'b', data: { icon: 'basketball' } },
        // ...
    ],
    lines: [
        { id: 'l1', data: { 'myLineId': 'line1' }, from: 'a', to: 'b', text: 'Relation Description' },
        // ...
    ],
};

下面这个片段证明,高亮逻辑是基于运行时连线检查,而不是基于预先计算好的邻接数据。

const nodeMouseOver = (currentNode: RGNode) => {
    clearHighlightStatus();
    graphInstance.updateNode(currentNode, { className: 'my-node-highlight' });

    graphInstance.getLines().forEach(line => {
        if (line.from === currentNode.id || line.to === currentNode.id) {
            graphInstance.updateLine(line, {
                className: 'my-line-highlight',
                lineWidth: 3
            });
        }
    });
};

下面这个片段展示了重置逻辑会对整个图谱中的节点和连线执行一次完整遍历,从而保证下一次悬停总是从干净的基础状态开始。

const clearHighlightStatus = () => {
    graphInstance.getNodes().forEach(node => {
        graphInstance.updateNode(node, { className: '' });
    });
    graphInstance.getLines().forEach(line => {
        graphInstance.updateLine(line, {
            className: '',
            lineWidth: graphOptions.defaultLineWidth
        });
    });
};

下面这个片段展示了 RGSlotOnNode 如何把节点主体变成驱动高亮行为的 DOM 表面。

<RGSlotOnNode>
    {({ node }: RGNodeSlotProps) => (
        <div
            className="px-2 bg-purple-200 rounded"
            onMouseEnter={() => nodeMouseOver(node)}
            onMouseLeave={() => nodeMouseOut(node)}
        >
            <div className="my-node">
                {node.text}
            </div>
        </div>
    )}
</RGSlotOnNode>

下面这个片段展示了视觉强调如何最终在 SCSS 中完成,通过为 relation-graph 生成的连线包裹类添加样式来实现。

.rg-line-peel.my-line-highlight {
    .rg-line {
        animation: my-line-anm2 2s infinite;
    }

    .rg-line-label {
        color: #ffffff;
        background-color: rgba(75, 28, 119, 1);
    }
}

这个示例的独特之处

对比数据表明,这个示例最接近 line-hightlight-prodeep-eachcustom-line-stylelayout-tree,但它的关注点比这几个示例都更窄。与 line-hightlight-pro 相比,区别同时体现在触发方式和作用范围上:本示例从节点悬停开始,并高亮所有与该节点相连的连线;而另一个示例则从连线点击开始,只选择一条边及其两个端点。

deep-each 相比,这个示例停留在一跳检查,而不是递归遍历后代。与 custom-line-style 相比,它不是一个持久连线主题的展示集合;这个动画类只会作为临时反馈出现。与 layout-tree 相比,它保留了一个固定的自上而下布局,而不是把方向切换本身作为演示主题。

因此,它的独特组合是:使用 slot 化节点悬停处理器、通过 getLines() 执行直接邻居连线查找、在运行时调用 updateNode()updateLine() 改写样式,以及在宽阔的自上而下树中使用带动画的正交连接线。悬浮辅助窗口、设置面板和图片导出都很实用,但它们来自共享的示例脚手架,并不是这个示例独有的部分。

这种模式还适用于哪里

这种模式很适合迁移到组织架构图、依赖树、审批链、制造装配结构、主题层级以及类文件系统浏览器等场景,在这些场景中,用户需要查看直接关系,但不希望通过点击选择或扩展更大的焦点状态来完成。

生产版本可以保留这条从悬停到邻居高亮的处理链,同时替换成业务标签、图标、状态颜色或工具提示。它也可以把作用范围从直接邻居扩展到多跳路径,但核心技术保持不变:使用 slot 渲染的节点事件来驱动对已加载图谱的运行时样式更新。