JavaScript is required

模拟数据的流转路径

这个示例构建了一个预置技术网络图,用户可选择两个节点、计算最短连接路径,并沿该路径逐步回放模拟数据流。它结合了实时图最短路径查询、自定义连线插槽动画、运行时布局切换,以及共享画布与导出控制。

关系图上的最短路径数据流回放

这个示例构建了什么

这个示例构建了一个只读的技术网络查看器,把最短路径查询转化为可见的数据流回放。画布一开始会加载一张准备好的图,弱化不在所选路径上的节点和连线,然后按段依次播放所选路径上的动画,让流向更容易被看清。

在视觉上,这个示例使用了深色遥测风格背景、发光的连线处理,以及圆角图标式节点胶囊,而不是默认的节点标签。用户可以通过选择两个不同节点来触发一条路径,在树形布局和中心布局之间切换,修改回放速度,拖动或最小化悬浮控制窗口,打开共享画布设置,并把当前图导出为图片。首次渲染时,示例还会自动播放一条从 H-7-212 的默认路径。

数据是如何组织的

图数据在 initializeGraph() 内联组装为一个静态 RGJsonData 对象,其中包含 rootId、扁平的 nodes 数组以及扁平的 lines 数组。每条连线都带有 fromtotext,有些连线指向根节点,有些则背离根节点,这使得回放逻辑确实需要处理混合的可视方向。

在运行 setJsonData(...) 之前,示例会先预处理每条连线:挂上自定义 marker ID、开启起始箭头,并关闭终点箭头。路径搜索并不是针对另一份独立数据副本完成的。相反,calcShortestPath(...) 会根据当前 relation-graph 的节点和连线重建一份辅助图,找出节点路径,再把该节点路径映射回连线 ID,并弱化路径之外的所有内容。

在真实应用里,这样的结构可以表示服务依赖、ETL 阶段、设备跳转、队列传输、审批链,或任何需要用户查看某个对象如何在两个已知端点之间流动的管道。

relation-graph 是如何使用的

这个示例包裹在 RGProvider 中,而 RGHooks.useGraphInstance() 是核心运行时 API。初始 RGOptions 设置了 RGJunctionPoint.ltrb、圆形节点、RGLineShape.StandardCurve,以及从左到右的树形布局。数据加载完成后,代码通过 setJsonData(...)moveToCenter()zoomToFit()setCanvasOffset(...) 来构建并定位图。后续布局选择器会更新 layout.layoutName 并重新执行初始化,从而让同一份数据集可以在 treecenter 模式下重新回放。

最重要的定制点是 RGSlotOnLineMyLineContent 替换了默认连线表面,请求图实例生成路径几何信息,计算 SVG 路径长度,写入 --rg-line-path--my-step-time 之类的 CSS 变量,并在活动段上按条件渲染一个小的移动矩形。这个 slot 还会把点击事件继续转交回 relation-graph,因此自定义渲染不会破坏正常的连线点击事件链路。

RGSlotOnNode 的使用相对轻一些。它把每个节点渲染成一个带 LandPlotIcon 和文字的圆角胶囊,而 SCSS 覆盖样式则提供深色背景、青色边框、选中状态下标签显示、已播放路径的描边动画,以及反向播放样式。MySvgDefs 提供了加载后连线所使用的自定义起止 marker 定义。

悬浮的 DraggableWindow 是共享的示例脚手架,但它在这里依然重要,因为它承载了布局选择器、速度选择器、画布行为设置以及截图导出操作。在这个共享面板内部,RGHooks.useGraphStore() 暴露了当前的滚轮和拖拽模式,RGHooks.useGraphInstance() 则为图片导出提供 prepareForImageGeneration()restoreAfterImageGeneration()。这里仍然是一个查看器,而不是编辑器:用户可以检查、回放、重新布局和导出这张图,但不会创建节点和连线,也不会重新连接它们。

关键交互

主要交互是两步式节点选择。第一次点击节点会保存源节点 ID,第二次点击另一个不同节点时会计算最短路径并重新开始回放。连续点击同一个节点两次不会发生任何事。尽管悬浮面板中的辅助文字比代码表达得更宽泛,但这才是实际实现的行为。

回放本身由定时器驱动。每一步都会把当前连线标记为活动状态,把之前访问过的连线标记为已播放,突出显示当前接收节点,然后在选定的延迟后安排下一步。当所选路径沿着某条连线时,其方向与该连线记录的 from -> to 方向相反,示例就会设置 reverseLine,让绘制动画和移动 marker 在该段上反向运行。

还有一些会明显影响体验的辅助交互。点击连线会清除任何待完成的首次节点选择。修改回放速度会重新启动当前路径,使新的时序立即生效。切换布局会用同一份数据集重建图,然后在新的排布上重新执行同类数据流交互。悬浮窗口可以拖动、最小化、打开为设置覆盖层,并用于修改滚轮或拖拽行为,或者下载图的图片。

关键代码片段

这个片段说明,示例从普通的 relation-graph 配置开始,然后构建本地数据集,而不是从别处获取数据。

const graphOptions: RGOptions = {
    defaultJunctionPoint: RGJunctionPoint.ltrb,
    defaultNodeShape: RGNodeShape.circle,
    defaultLineShape: RGLineShape.StandardCurve,
    layout: {
        layoutName: 'tree',
        from: 'left',
        treeNodeGapH: 100,
        treeNodeGapV: 20
    }
};

这个片段证明,连线 marker 会在加载前预处理,而且首次渲染后会立即开始默认回放。

myJsonData.lines.forEach(line => {
    line.endMarkerId = 'my-arrow-001';
    line.startMarkerId = 'my-arrow-001-start';
    line.showStartArrow = true;
});
myJsonData.lines.forEach((line) => {
    line.showEndArrow = false;
});
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
graphInstance.setCanvasOffset(canvasOffset.x, canvasOffset.y + 100);
showDataFlow('H-7-21', '2');

这个片段展示了,路径回放由两次不同的节点点击触发,并且会基于实时图实例重新计算。

const onNodeClick = (node: RGNode) => {
    if (checkedNodeIdRef.current) {
        if (checkedNodeIdRef.current === node.id) {
            return;
        }
        showDataFlow(checkedNodeIdRef.current, node.id);
        checkedNodeIdRef.current = '';
    } else {
        checkedNodeIdRef.current = node.id;
    }
};
const showDataFlow = (fromNodeId: string, toNodeId: string) => {
    const { lineIdsOnPath, nodeIdsOnPath } = calcShortestPath(fromNodeId, toNodeId, graphInstance, '');
    allLineIdsOnPathRef.current = lineIdsOnPath;
    allNodeIdsOnPathRef.current = nodeIdsOnPath;
    restartTask();
};

这个片段展示了,最短路径辅助逻辑会从当前 relation-graph 状态重建其工作图,而不是依赖一份独立索引。

const graphDb = new ShortestPathGraph();
graphDb.loadDataFromRelationGraph(graphInstance);
const nodeIdsOnPath = graphDb.findPath(fromNodeId, toNodeId);
if (!nodeIdsOnPath) {
    throw new Error('Cannot find the association between the start node and the end node!');
}
const lineIdsOnPath = getAllLineIdsOnPath(nodeIdsOnPath, graphInstance);
drawMinPath(nodeIdsOnPath, lineIdsOnPath, graphInstance, itemsOnPathClassName);

这个片段展示了,自定义连线 slot 如何把生成出的路径几何信息转为基于 CSS 的运动渲染。

const linePathInfo = useMemo<RGLinePathInfo>(() => graphInstance.generateLinePath(lineConfig), [lineConfig]);
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathEl.setAttribute('d', linePathInfo.pathData);
const pathLength = pathEl.getTotalLength();
return (<g
    className={`${lineConfig.line.data?.played ? (lineConfig.line.data?.reverseLine ? 'my-draw-path-animation reverse-line' : 'my-draw-path-animation') : ''}`}
    style={{
        '--rg-line-path': `path('${linePathInfo.pathData}')`,
        '--my-line-path-length': pathLength + 'px',
        '--my-step-time': `${lineConfig.line.data?.myStepTime || 1000}ms`
    }}>

这个示例的独特之处

根据对比数据,它功能上最接近的邻近示例是 line-style-pro,但侧重点不同。line-style-pro 展示的是在内置连线表面上复用路径效果主题,而这个示例更深入地推进了 slot 级别的运动渲染:它会计算一条路径,用自定义连线 slot 替换当前活动路径,并通过移动 marker 和描边绘制时序为每一段添加动画。

对比文件还把这个示例和 use-dagre-layout-2layout-tree 这类以布局为重点的查看器区分开来。那些示例把重新布局本身作为主要教学点。而在这里,运行时切换布局是次要能力。它存在的目的,是为了让同一种最短路径回放交互能在不同排布下重新演示,而不是单独讲解布局调优。

相较于 table-relationship 这类 heavily slot 化的技术查看器,以及 line-shape-and-label 这类以连线为中心的示例,这个示例把连线定制用于路径特定的运行时行为,而不是固定的端点连线方式,或全图范围内的连线几何控制。最突出的稀有组合,正如准备好的对比数据所指出的那样,是挂载时自动播放、基于实时图状态推导最短路径、弱化非路径上下文、自定义箭头 marker、深色遥测风格、速度控制,以及在布局或速度变化后可重新启动的方向感知分段回放。

这种模式还适用于哪里

这种模式可以自然迁移到依赖追踪、数据血缘讲解和服务跳转分析中。如果团队需要解释某个事件如何跨系统传播,或者为什么某个目标会经由一串特定的中间对象被到达,那么最短路径查找、路径弱化以及定时回放的组合,会比单纯的静态高亮路径更容易理解。

它也适合面向操作人员的工具和教学型视图。比如回放工作流审批、展示事故在基础设施中的影响传播路径、解释队列到消费者的投递链路,或者演示流程图中的推荐路径。在这些场景里,可复用的核心思路并不是占位用的 H-* 数据集,而是 relation-graph 如何被用来从实时图状态重建一条路径,再把这条路径转化为有节奏的可视化追踪。