拓扑图组件技术文档
一、组件概述
技术栈 :TypeScript + Konva + ELK
核心功能 :架构图的拓扑可视化渲染套件
视图模式
模块调用图(Module Call Graph)
模块接口调用图(Module Interface Call Graph)
二、核心功能
2.1 图渲染能力
✅ 自动布局 :基于ELK布局算法,自动计算节点位置
✅ 画布缩放 :支持滚轮缩放,缩放范围可配置
✅ 画布拖拽 :支持鼠标拖拽平移视图
✅ 视图自适应 :自动适配容器大小,支持居中显示
✅ 节点聚焦 :双击节点自动聚焦并居中显示
✅
节点高亮 :支持高亮显示选中的节点及其关联节点/边
✅ 小地图 :提供全局视图导航,快速定位
2.2 节点功能
✅ 模块节点 :渲染系统模块,显示模块名称
✅ 接口节点 :渲染接口定义,显示接口签名
✅ 边界节点 :支持边界条件节点渲染
✅ Tooltip提示 :鼠标悬停显示详细信息
✅ 右键菜单 :提供节点操作快捷菜单
✅ 徽章显示 :支持节点状态徽章
2.3 边功能
✅ 接口调用关系线 :渲染模块间/接口间调用关系
✅ 边标签 :显示调用关系信息
✅ 虚线样式 :支持虚线/实线两种边样式
✅ 高亮效果 :支持边的选中和高亮状态
2.4 交互功能
✅ 节点点击事件 :触发选中回调
✅ 边点击事件 :触发选中回调
✅ 滚轮缩放 :缩放视图并保持鼠标位置不变
✅ 动态更新 :支持数据动态更新和重新渲染
三、核心代码流程
3.1 初始化流程
flowchart TD
A[创建 CallGraph 实例] --> B[接收配置参数]
B --> B1[rootElement, modules, interfaces, calls等]
B1 --> C[初始化渲染器<br/>KonvaBackendRender]
C --> C1[创建 Konva Stage 和 Layer]
C1 --> D[计算图布局<br/>ELK Layout]
D --> D1[将业务数据转换为 ELK 格式]
D1 --> D2[调用 ELK 布局引擎计算节点位置]
D2 --> E[渲染图形]
E --> E1[根据布局结果创建 Konva 节点和边]
E1 --> E2[将节点和边添加到 Layer]
E2 --> F[注册事件监听]
F --> F1[监听节点/边点击事件]
F1 --> F2[监听滚轮缩放事件]
F2 --> F3[监听拖拽事件]
style A fill:#e1f5ff
style F3 fill:#e8f5e9
3.2 数据流转
flowchart TD
A[用户数据<br/>modules, interfaces, calls] --> B[业务层处理<br/>OverTimeSceneBusiness]
B --> B1[数据格式转换]
B --> B2[关系计算]
B --> B3[布局参数配置]
B1 --> C[Graph 实例<br/>基于 graphlib]
B2 --> C
B3 --> C
C --> C1[图结构维护]
C --> C2[节点/边管理]
C1 --> D[ELK Layout<br/>布局引擎]
C2 --> D
D --> D1[计算节点位置]
D --> D2[计算边路径]
D --> D3[生成布局结果]
D1 --> E[Konva 渲染]
D2 --> E
D3 --> E
E --> E1[创建可视化节点]
E --> E2[创建可视化边]
E --> E3[绘制到 Stage]
style A fill:#fff3e0
style E3 fill:#e8f5e9
3.3 事件处理
flowchart TD
A[用户交互事件] --> B[GraphController]
B --> B1[事件捕获]
B --> B2[事件分发]
B --> B3[状态更新]
B1 --> C[Konva 后端]
B2 --> C
B3 --> C
C --> C1[更新 Konva 对象状态]
C --> C2[触发重新渲染]
C --> C3[回调通知业务层]
C3 --> D[业务层]
D --> D1[执行业务逻辑]
style A fill:#fff3e0
style D1 fill:#e8f5e9
四、核心组件架构
4.1 架构分层
graph TB
subgraph Layer1[入口层 Entry Layer]
A[CallGraph<br/>主入口类]
end
subgraph Layer2[渲染层 Render Layer]
B[KonvaBackendRender<br/>Konva渲染后端]
B1[KonvaNodeRender<br/>节点渲染]
B2[KonvaEdgeRender<br/>边渲染]
end
subgraph Layer3[控制层 Controller Layer]
C[GraphController<br/>图控制器]
C1[MiniMap<br/>小地图控制器]
end
subgraph Layer4[业务层 Business Layer]
D[OverTimeSceneBusiness<br/>业务逻辑处理]
end
subgraph Layer5[布局层 Layout Layer]
E[ELK Layout<br/>布局引擎]
end
A --> B
B --> B1
B --> B2
B --> C
C --> C1
C --> D
D --> E
style A fill:#e1f5ff
style E fill:#e8f5e9
4.2 核心类说明
CallGraph
src/call-graph.ts
主入口类,协调各组件完成图的渲染
KonvaBackendRender
src/scene/call-graph/render/konva/index.ts
Konva渲染后端,管理Stage和Layer
GraphController
src/scene/call-graph/controller/graph.ts
图控制器,管理节点/边的交互
OverTimeSceneBusiness
src/scene/call-graph/business/over-time-scene-business.ts
业务逻辑处理,数据转换
MiniMap
src/scene/call-graph/render/konva/minimap/minimap.ts
小地图控制器
ELKLayout
src/scene/call-graph/layout/elk.ts
ELK布局引擎封装
五、核心API接口
5.1 CallGraph 构造函数
1 2 3 4 5 6 7 8 9 new CallGraph ({ rootElement : HTMLElement , modules : Module [], interfaces : Interface [], calls : Call [], miniMapElement ?: HTMLElement , sceneType ?: 'module' | 'interface' , config ?: CallGraphConfig , })
5.2 主要方法
render()
-
Promise<void>
渲染拓扑图
update()
-
Promise<void>
更新拓扑图(数据变更后)
focusNode(nodeId)
string
void
聚焦到指定节点
highlightNode(nodeId)
string
void
高亮指定节点
resetView()
-
void
重置视图到初始状态
zoomIn()
-
void
放大视图
zoomOut()
-
void
缩小视图
getGraphData()
-
GraphData
获取当前图数据
六、数据结构
6.1 Module(模块)
1 2 3 4 5 6 interface Module { id : string ; name : string ; type ?: string ; metadata ?: Record <string , any >; }
6.2 Interface(接口)
1 2 3 4 5 6 7 interface Interface { id : string ; name : string ; moduleId ?: string ; signature ?: string ; metadata ?: Record <string , any >; }
6.3 Call(调用关系)
1 2 3 4 5 6 7 interface Call { id : string ; source : string ; target : string ; type ?: 'sync' | 'async' ; metadata ?: Record <string , any >; }
七、技术栈与依赖
7.1 核心依赖
konva
latest
2D Canvas渲染库
elkjs
latest
图布局算法
graphlib
latest
图数据结构
lodash
latest
工具函数库
typescript
latest
类型定义
7.2 架构特点
分层架构 :清晰的分层设计,易于维护和扩展
多渲染器支持 :支持切换不同的渲染后端(目前支持Konva)
类型安全 :完整的TypeScript类型定义
可配置性强 :支持丰富的配置项定制
事件驱动 :基于事件的交互机制
八、使用示例
8.1 基础使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const modules = [ { id : 'm1' , name : '用户服务' }, { id : 'm2' , name : '订单服务' }, { id : 'm3' , name : '支付服务' }, ]; const interfaces = [ { id : 'i1' , name : 'getUserInfo' , moduleId : 'm1' }, { id : 'i2' , name : 'createOrder' , moduleId : 'm2' }, { id : 'i3' , name : 'processPayment' , moduleId : 'm3' }, ]; const calls = [ { id : 'c1' , source : 'i1' , target : 'i2' }, { id : 'c2' , source : 'i2' , target : 'i3' }, ]; const graph = new CallGraph ({ rootElement : document .getElementById ('graph-container' ), modules, interfaces, calls, sceneType : 'interface' , }); await graph.render ();graph.focusNode ('m2' );
8.2 带小地图的完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <template > <div class ="graph-container" > <div id ="graph-container" ref ="graphRef" > </div > <div id ="minimap-container" ref ="minimapRef" > </div > </div > </template > <script setup lang ="ts" > import { ref, onMounted } from 'vue' ;import { CallGraph } from '@tencent/wxpay-topology-diagram-vue3' ;const graphRef = ref<HTMLElement >();const minimapRef = ref<HTMLElement >();onMounted (async () => { const graph = new CallGraph ({ rootElement : graphRef.value !, modules : [...], interfaces : [...], calls : [...], miniMapElement : minimapRef.value !, sceneType : 'interface' , }); await graph.render (); }); </script > <style scoped > .graph-container { display : flex; position : relative; } #graph-container { width : 800px ; height : 600px ; border : 1px solid #ddd ; } #minimap-container { width : 200px ; height : 150px ; border : 1px solid #ccc ; position : absolute; right : 10px ; top : 10px ; } </style >
九、性能优化措施
9.1 渲染优化
虚拟化渲染 :只渲染可视区域内的节点和边
层级优化 :使用Konva的Layer机制,减少重绘范围
批量更新 :多个状态变更合并为一次渲染
9.2 布局优化
增量布局 :数据变更时只计算受影响的部分
布局缓存 :相同配置的布局结果缓存复用
9.3 交互优化
节流处理 :高频事件(如缩放)进行节流
懒加载 :Tooltip等延迟加载
十、扩展性说明
10.1 自定义节点渲染
可以通过继承 KonvaNodeRender 类实现自定义节点样式:
1 2 3 4 5 class CustomNodeRender extends KonvaNodeRender { renderNode (node : Node ): Konva .Group { } }
10.2 自定义边渲染
可以通过继承 KonvaEdgeRender 类实现自定义边样式:
1 2 3 4 5 class CustomEdgeRender extends KonvaEdgeRender { renderEdge (edge : Edge ): Konva .Line { } }
10.3 自定义布局
可以实现自定义的布局算法:
1 2 3 4 5 class CustomLayout implements LayoutEngine { layout (graph : Graph ): LayoutResult { } }
十一、注意事项
数据一致性 :确保
modules、interfaces、calls
之间的引用关系正确
ID唯一性 :所有节点ID必须唯一
容器尺寸 :确保 rootElement
有明确的宽高
性能考虑 :大规模图(节点数 >
1000)建议开启虚拟化渲染
内存管理 :组件销毁时调用 dispose()
方法释放资源
节点与边构建流程详解
一、流程概述
拓扑图的构建过程采用分层架构 ,从用户数据到最终渲染经历了以下关键阶段:
graph TB
A[用户输入参数<br/>modules, interfaces, calls] --> B[业务层处理<br/>OverTimeSceneBusiness]
B --> C[图数据结构<br/>Graphlib Graph]
C --> D[布局计算<br/>ELK Layout]
D --> E[图控制器<br/>GraphController]
E --> F[工厂函数<br/>createNode/createEdge]
F --> G[节点/边实例<br/>Konva Elements]
G --> H[Canvas渲染<br/>Konva Stage]
style A fill:#fff3e0
style H fill:#e8f5e9
二、入口参数结构
2.1 完整参数列表
1 2 3 4 5 6 7 8 9 interface CallGraphOptions { rootElement : HTMLElement ; modules ?: ModuleInterface []; interfaces : ModuleInterfaceInterface []; calls : ModuleInterfaceCallEdge []; callGraphType : CallGraphEnum ; showInterfaces ?: boolean ; miniMapElement ?: HTMLElement ; }
2.2 数据类型定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 interface ModuleInterface { moduleName : string ; boundaryName ?: string ; showModuleBadge ?: boolean ; badgeCount ?: number ; backgroundColor ?: string ; enableTooltip ?: boolean ; data ?: Record <string , any >; } interface ModuleInterfaceInterface { moduleName : string ; interfaceName : string ; inDegreeCounts ?: number ; outDegreeCounts ?: number ; backgroundColor ?: string ; badgeCount ?: number ; extentionData ?: any []; contextMenu ?: any []; data ?: Record <string , any >; } interface ModuleInterfaceCallEdge { id ?: string ; from : { moduleName : string ; interfaceName ?: string ; idc ?: string ; }; to : { moduleName : string ; interfaceName ?: string ; idc ?: string ; }; labels ?: Array <{ text : string ; width ?: number ; height ?: number ; }>; tooltip ?: any ; data ?: Record <string , any >; }
三、业务层处理流程
3.1 整体处理流程
sequenceDiagram
participant CG as CallGraph
participant OTSB as OverTimeSceneBusiness
participant Graph as Graphlib Graph
CG->>OTSB: new OverTimeSceneBusiness()
OTSB->>Graph: new Graph({compound, multigraph})
CG->>OTSB: setData({interfaces, calls, modules, showInterfaces})
alt modules存在
OTSB->>OTSB: buildBoundaries(modules)
Note over OTSB: 构建边界节点<br/>按boundaryName分组
end
OTSB->>OTSB: buildNodes(interfaces, calls, modules, showInterfaces)
Note over OTSB: 构建模块节点<br/>构建接口节点(可选)
OTSB->>OTSB: buildEdges(calls, showInterfaces)
Note over OTSB: 构建调用关系边
CG->>OTSB: transformData()
OTSB-->>CG: 返回Graph实例
3.2
边界节点构建(buildBoundaries)
flowchart TD
A[开始 buildBoundaries] --> B[按 boundaryName 分组模块]
B --> C{遍历每个边界}
C --> D[创建边界节点]
D --> D1[id: boundary-{boundaryName}]
D --> D2[type: boundary]
D --> D3[layoutOptions: 边距配置]
D1 --> E[设置边界节点到Graph]
E --> F[遍历边界内的模块]
F --> G[设置模块的父节点为边界]
G --> C
C --> H[结束]
style A fill:#fff3e0
style H fill:#e8f5e9
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public buildBoundaries (modules : ModuleInterface [] ) { const modulesGroupedByBoundary = _.groupBy (modules, 'boundaryName' ); _.keys (modulesGroupedByBoundary).forEach ((boundaryKey ) => { if (!boundaryKey || boundaryKey === 'undefined' ) return ; const boundaryModules = modulesGroupedByBoundary[boundaryKey]; const boundaryId = `boundary-${boundaryKey} ` ; this .graph .setNode (boundaryId, { id : boundaryId, type : 'boundary' , nodeData : { boundaryName : boundaryKey }, layoutOptions : { 'elk.partitioning.activate' : true , ...DEFAULT_BOUNDARY_PADDING , }, }); boundaryModules.forEach ((boundaryModule ) => { this .graph .setParent (boundaryModule.moduleName , boundaryId); }); }); }
3.3 节点构建(buildNodes)
flowchart TD
A[开始 buildNodes] --> B[按 moduleName 分组接口]
B --> C[创建模块映射Map]
C --> D{遍历每个模块}
D --> E[获取模块配置]
E --> E1[showModuleBadge]
E --> E2[badgeCount]
E --> E3[backgroundColor]
E1 --> F[创建模块节点]
F --> F1[id: moduleName]
F --> F2[type: module-group]
F --> F3[labels: 模块名称]
F --> F4[nodeData: 模块数据]
F4 --> G[设置模块节点到Graph]
G --> H{showInterfaces == true?}
H -->|是| I[遍历模块下的接口]
I --> J[计算接口的入度和出度]
J --> K[创建接口节点]
K --> K1[id: moduleName-interfaceName]
K --> K2[type: interface]
K --> K3[ports: 入度/出度端口]
K --> K4[nodeData: 接口数据]
K4 --> L[设置接口节点到Graph]
L --> M[设置接口的父节点为模块]
M --> I
I --> N[继续下一个模块]
H -->|否| N
N --> D
D --> O[结束]
style A fill:#fff3e0
style O fill:#e8f5e9
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 private buildNodes ( interfacesGroupedByModule : _.Dictionary <ModuleInterfaceInterface []>, calls : ModuleInterfaceCallEdge [], modules ?: ModuleInterface [], showInterfaces : boolean = true , ) { const moduleMap = new Map <string , ModuleInterface >(); if (modules) { modules.forEach ((module ) => { moduleMap.set (module .moduleName , module ); }); } _.keys (interfacesGroupedByModule).map ((moduleName ) => { const moduleData = moduleMap.get (moduleName); const showModuleBadge = moduleData?.showModuleBadge || false ; const badgeCount = moduleData?.badgeCount || 0 ; const backgroundColor = moduleData?.backgroundColor ; const moduleNode = { type : 'module-group' , id : moduleName, labels : [{ text : moduleName }], layoutOptions : { 'elk.partitioning.activate' : true , ...DEFAULT_INTERFACE_MODULE_PADDING , }, nodeData : { moduleName, showModuleBadge, badgeCount, backgroundColor, }, data : { ...moduleData?.data , enableTooltip : moduleData?.enableTooltip , }, width : 180 , height : 50 , }; this .graph .setNode (moduleName, moduleNode); if (showInterfaces) { interfacesGroupedByModule[moduleName].map ((moduleInterface ) => { const { moduleName, interfaceName } = moduleInterface; const outCallEdges = calls.filter (call => call.from .moduleName === moduleName && call.from .interfaceName === interfaceName ); const inCallEdges = calls.filter (call => call.to .moduleName === moduleName && call.to .interfaceName === interfaceName ); const inCallIdcs = _.uniq (inCallEdges.map (call => call.to .idc )); const outCallIdcs = _.uniq (outCallEdges.map (call => call.from .idc )); const nodeId = `${moduleName} -${interfaceName} ` ; const childInterfaceNode = { type : 'interface' , id : nodeId, labels : [{ text : interfaceName }], inIdcs : inCallIdcs, outIdcs : outCallIdcs, ports : [ ...inCallIdcs.map (idc => ({ id : `${nodeId} -in-${idc} ` , width : 7 , height : 7 , labels : [{ text : idc, width : 40 , height : 20 }], layoutOptions : { 'port.side' : 'NORTH' }, })), ...outCallIdcs.map (idc => ({ id : `${nodeId} -out-${idc} ` , width : 7 , height : 7 , labels : [{ text : idc, width : 40 , height : 20 }], layoutOptions : { 'port.side' : 'SOUTH' }, })), ], layoutOptions : { 'elk.partitioning.activate' : true , portConstraints : 'FIXED_ORDER' , ...DEFAULT_INTERFACE_PADDING , }, nodeData : { moduleName, interfaceName, extentionData : moduleInterface.extentionData || [], contextMenu : moduleInterface.contextMenu || [], inDegreeCount : moduleInterface.inDegreeCounts , outDegreeCount : moduleInterface.outDegreeCounts , backgroundColor : moduleInterface.backgroundColor , badgeCount : moduleInterface.badgeCount , }, data : moduleInterface.data || {}, ...DEFAULT_INTERFACE_SIZE , }; this .graph .setNode (nodeId, childInterfaceNode); this .graph .setParent (nodeId, moduleName); }); } }); }
3.4 边构建(buildEdges)
flowchart TD
A[开始 buildEdges] --> B{遍历每条调用关系}
B --> C{showInterfaces == true?}
C -->|是| D[source = moduleName-interfaceName]
C -->|是| E[target = moduleName-interfaceName]
C -->|否| F[source = moduleName]
C -->|否| G[target = moduleName]
D --> H{源节点和目标节点都存在?}
E --> H
F --> H
G --> H
H -->|否| I[跳过该边]
H -->|是| J[生成边ID]
J --> J1{edge.id存在?}
J1 -->|是| K[id = edge.id]
J1 -->|否| L[id = source -> target]
K --> M[创建边对象]
L --> M
M --> M1[id: 边ID]
M --> M2[type: interface-call]
M --> M3[sourceIdc / targetIdc]
M --> M4[labels: 边标签]
M --> M5[edgeData: 边数据]
M5 --> N[设置边到Graph]
N --> B
B --> O[结束]
style A fill:#fff3e0
style O fill:#e8f5e9
style I fill:#ffebee
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 private buildEdges (calls : ModuleInterfaceCallEdge [], showInterfaces : boolean = true ) { calls.forEach ((edge ) => { let source : string ; let target : string ; if (showInterfaces) { source = `${edge.from .moduleName} -${edge.from .interfaceName} ` ; target = `${edge.to.moduleName} -${edge.to.interfaceName} ` ; } else { source = edge.from .moduleName ; target = edge.to .moduleName ; } if (!this .graph .hasNode (source)) return ; if (!this .graph .hasNode (target)) return ; let id : string ; if (edge.id ) { id = edge.id ; } else if (showInterfaces) { id = `${edge.from .moduleName} :${edge.from .interfaceName} ->${edge.to.moduleName} :${edge.to.interfaceName} ` ; } else { id = `${edge.from .moduleName} ->${edge.to.moduleName} ` ; } this .graph .setEdge (source, target, { id, type : 'interface-call' , source, target, sourceIdc : edge.from .idc || '' , targetIdc : edge.to .idc || '' , labels : edge.labels ?.map (label => ({ text : label.text , width : label.width || DEFAULT_LABEL_WIDTH , height : label.height || DEFAULT_LABEL_HEIGHT , })), data : edge.data || {}, edgeData : { id : edge.id , tooltip : edge.tooltip , srcModuleName : edge.from .moduleName , srcInterfaceName : edge.from .interfaceName , dstModuleName : edge.to .moduleName , dstInterfaceName : edge.to .interfaceName , }, }, id); }); }
四、图数据结构
4.1 Graphlib Graph 结构
graph TB
subgraph Graph[Graphlib Graph]
subgraph Nodes[节点集合]
B1[Boundary节点<br/>type: boundary]
B2[Module节点<br/>type: module-group]
B3[Interface节点<br/>type: interface]
end
subgraph Edges[边集合]
E1[InterfaceCall边<br/>type: interface-call]
end
subgraph ParentChild[父子关系]
P1[Boundary → Module]
P2[Module → Interface]
end
end
B1 --> P1
B2 --> P1
B2 --> P2
B3 --> P2
B3 --> E1
style B1 fill:#e1f5ff
style B2 fill:#fff3e0
style B3 fill:#e8f5e9
4.2 节点数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 interface GraphNode { id : string ; type : string ; labels ?: Array <{ text : string }>; layoutOptions ?: object ; nodeData ?: object ; data ?: object ; width ?: number ; height ?: number ; ports ?: Port []; children ?: GraphNode []; } interface GraphEdge { id : string ; type : string ; source : string ; target : string ; sourceIdc ?: string ; targetIdc ?: string ; labels ?: Label []; data ?: object ; edgeData ?: object ; layout ?: { points : number []; labels : Label []; }; }
五、布局计算流程
5.1 ELK布局计算
sequenceDiagram
participant GC as GraphController
participant G as Graphlib Graph
participant ELK as ELK Layout Engine
participant Result as 布局结果
GC->>GC: transformLayoutOptionsToKonvaNodes()
GC->>G: nodes() / edges()
G-->>GC: 节点和边列表
GC->>GC: initRootNodes()
Note over GC: 构建树形结构<br/>递归获取子节点
GC->>GC: initEdgeLayoutOptions()
Note over GC: 构建边配置<br/>设置sources和targets
GC->>ELK: elkLayout({children, edges, ...options})
ELK->>ELK: 计算节点位置
ELK->>ELK: 计算边路径
ELK->>ELK: 计算标签位置
ELK-->>GC: 返回布局结果
GC->>GC: applyNodeLayoutToGraph()
Note over GC: 应用节点布局<br/>递归遍历子节点
GC->>GC: applyEdgeLayoutToGraph()
Note over GC: 应用边布局<br/>计算绝对坐标
5.2 布局结果应用
flowchart TD
A[ELK布局结果] --> B[节点布局]
A --> C[边布局]
B --> B1[遍历节点树]
B1 --> B2[计算绝对位置<br/>x + parentX, y + parentY]
B2 --> B3[设置 layout 到节点]
B3 --> B4[递归处理子节点]
C --> C1[遍历所有边]
C1 --> C2[获取路径点<br/>startPoint + bendPoints + endPoint]
C2 --> C3{边在根节点还是容器内?}
C3 -->|根节点| C4[直接使用坐标]
C3 -->|容器内| C5[加上容器偏移量]
C4 --> C6[设置 layout 到边]
C5 --> C6
style A fill:#fff3e0
style B3 fill:#e1f5ff
style C6 fill:#e8f5e9
六、渲染流程
6.1 整体渲染流程
sequenceDiagram
participant GC as GraphController
participant Factory as createNode/createEdge
participant Node as Node Classes
participant Edge as Edge Classes
participant Konva as Konva Stage
GC->>GC: renderGraph()
GC->>GC: 按优先级排序节点
Note over GC: boundary → module-group → interface
GC->>Factory: createNode(type, config)
Factory->>Node: new ModuleGroupNode(config)
Node->>Konva: 创建Konva元素
Node->>Konva: 添加到nodeLayer
Factory-->>GC: 返回节点实例
GC->>GC: 保存ref到图节点
loop 遍历所有节点
GC->>Factory: createNode(...)
end
GC->>Factory: createEdge(type, config)
Factory->>Edge: new InterfaceCallEdge(config)
Edge->>Konva: 创建Konva元素
Edge->>Konva: 添加到edgeLayer
Factory-->>GC: 返回边实例
GC->>GC: 保存ref到图边
loop 遍历所有边
GC->>Factory: createEdge(...)
end
GC->>Konva: batchDraw()
Note over Konva: 批量绘制所有图层
6.2 工厂函数
flowchart TD
A[createNode/createEdge] --> B{判断类型}
B -->|module-group| C[ModuleGroupNode]
B -->|interface| D[InterfaceNode]
B -->|boundary| E[BoundaryNode]
B -->|interface-call| F[InterfaceCallEdge]
C --> G[创建节点实例]
D --> G
E --> G
F --> H[创建边实例]
G --> I[调用 render 方法]
H --> I
I --> J[返回实例]
style A fill:#fff3e0
style J fill:#e8f5e9
工厂函数实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 export const NODE_FACTORY_REF_MAP = { 'module-group' : ModuleGroupNode , 'interface' : InterfaceNode , 'boundary' : BoundaryNode , }; export const EDGE_FACTORY_REF_MAP = { 'interface-call' : InterfaceCallEdge , }; export const NODE_RENDER_PRIORITY = [ 'boundary' , 'module-group' , 'interface' , ]; export const createNode = (type : string , config : NodeFactoryConfig ) => new NODE_FACTORY_REF_MAP [type ](config); export const createEdge = (type : string , config : EdgeFactoryConfig ) => new EDGE_FACTORY_REF_MAP [type ](config);
七、节点渲染详解
7.1 ModuleGroupNode 渲染流程
flowchart TD
A[ModuleGroupNode.render] --> B[创建Group容器]
B --> C[创建Rect矩形背景]
C --> D[创建Path图标]
D --> E[创建Text名称]
E --> F{showModuleBadge?}
F -->|是| G[创建徽章Group]
G --> G1[创建Rect徽章背景]
G1 --> G2[创建Text徽章数字]
G2 --> H[添加到Group]
F -->|否| H
H --> I[绑定Tooltip事件]
I --> J[添加到nodeLayer]
style A fill:#fff3e0
style J fill:#e8f5e9
关键配置 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public get moduleWrapperNodeConfig () { const fillColor = this .customBackgroundColor || '#f5f6fa' ; return { x : this .x , y : this .y , width : this .width , height : this .height , fill : fillColor, stroke : '#5F95FF' , strokeWidth : 1 , name : 'module-node' , perfectDrawEnabled : false , }; } public badgeFactory ( ) { const badgeText = String (this .badgeCount ); const isLongText = badgeText.length > 2 ; const badgeWidth = isLongText ? 24 : 18 ; const badgeHeight = 18 ; const badge = new Konva .Rect ({ x : -badgeWidth / 2 , y : -badgeHeight / 2 , width : badgeWidth, height : badgeHeight, cornerRadius : badgeHeight / 2 , fill : '#FF4D4F' , name : 'module-node__badge-bg' , }); const text = new Konva .Text ({ text : badgeText, fontSize : 12 , fill : '#FFFFFF' , align : 'center' , verticalAlign : 'middle' , }); }
7.2 InterfaceNode 渲染流程
flowchart TD
A[InterfaceNode.render] --> B[创建Group容器]
B --> C[创建Rect矩形背景<br/>虚线边框]
C --> D[创建Path图标]
D --> E[创建Text名称]
E --> F[创建入度/出度区域]
F --> F1[创建入度Circle]
F --> F2[创建出度Circle]
F1 --> G[绑定点击事件]
F2 --> G
G --> H{showBadge?}
H -->|是| I[创建徽章]
H -->|否| J[添加到nodeLayer]
I --> J
style A fill:#fff3e0
style J fill:#e8f5e9
7.3 BoundaryNode 渲染流程
flowchart TD
A[BoundaryNode.render] --> B[创建Group容器]
B --> C[创建Rect矩形背景<br/>虚线边框]
C --> D[创建Text边界名称]
D --> E[添加到nodeLayer]
style A fill:#fff3e0
style E fill:#e8f5e9
八、边渲染详解
8.1 InterfaceCallEdge 渲染流程
flowchart TD
A[InterfaceCallEdge.render] --> B[创建Arrow线段]
B --> B1[points: 路径点]
B --> B2[stroke: 线条颜色]
B --> B3[strokeWidth: 线条宽度]
B --> B4[dash: 虚线样式]
B1 --> C{有标签?}
C -->|是| D[创建Label标签]
D --> E[绑定鼠标事件]
C -->|否| E
E --> E1[mouseover: 显示Tooltip]
E --> E2[mouseout: 隐藏Tooltip]
E1 --> F[添加到edgeLayer]
style A fill:#fff3e0
style F fill:#e8f5e9
关键配置 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public get arrorLineConfig () { const isBridged = this .data ?.isBridged || false ; return { points : this .points , pointerAtEnding : true , stroke : '#A2B1C3' , strokeWidth : 2 , fill : '#A2B1C3' , name : 'interface-call-edge' , dash : isBridged ? [5 , 5 ] : undefined , }; }
九、完整流程总览
9.1 从参数到渲染的完整链路
graph TB
subgraph Input[输入层]
A1[modules: 模块列表]
A2[interfaces: 接口列表]
A3[calls: 调用关系]
A4[showInterfaces: 是否显示接口]
end
subgraph Business[业务层]
B1[buildBoundaries<br/>构建边界节点]
B2[buildNodes<br/>构建模块/接口节点]
B3[buildEdges<br/>构建调用边]
end
subgraph Graph[图数据层]
C1[Graphlib Graph<br/>图数据结构]
end
subgraph Layout[布局层]
D1[ELK Layout<br/>计算节点位置]
D2[计算边路径]
D3[计算标签位置]
end
subgraph Controller[控制层]
E1[GraphController<br/>图控制器]
E2[diffNodes<br/>节点差异比较]
E3[diffEdges<br/>边差异比较]
end
subgraph Factory[工厂层]
F1[createNode<br/>创建节点实例]
F2[createEdge<br/>创建边实例]
end
subgraph Render[渲染层]
G1[ModuleGroupNode]
G2[InterfaceNode]
G3[BoundaryNode]
G4[InterfaceCallEdge]
end
subgraph Output[输出层]
H1[Konva Stage<br/>Canvas渲染]
end
A1 --> B1
A2 --> B2
A3 --> B3
A4 --> B2
A4 --> B3
B1 --> C1
B2 --> C1
B3 --> C1
C1 --> D1
D1 --> D2
D2 --> D3
D3 --> E1
E1 --> E2
E1 --> E3
E2 --> F1
E3 --> F2
F1 --> G1
F1 --> G2
F1 --> G3
F2 --> G4
G1 --> H1
G2 --> H1
G3 --> H1
G4 --> H1
style Input fill:#fff3e0
style Output fill:#e8f5e9
9.2 核心类职责
CallGraph
src/scene/call-graph/index.ts
主入口类,协调各层完成渲染
OverTimeSceneBusiness
src/scene/call-graph/business/overtime/index.ts
业务逻辑处理,数据转换
GraphController
src/scene/call-graph/render/konva/graph.ts
图控制器,管理节点/边的渲染和交互
createNode
src/scene/call-graph/render/konva/factory.ts
节点工厂函数
createEdge
src/scene/call-graph/render/konva/factory.ts
边工厂函数
ModuleGroupNode
src/scene/call-graph/render/konva/nodes/module-group.ts
模块节点渲染
InterfaceNode
src/scene/call-graph/render/konva/nodes/interface-node.ts
接口节点渲染
BoundaryNode
src/scene/call-graph/render/konva/nodes/boundary-node.ts
边界节点渲染
InterfaceCallEdge
src/scene/call-graph/render/konva/edges/interface-call.ts
调用边渲染
十、关键设计要点
10.1 分层架构优势
graph LR
A[业务层] -->|数据转换| B[图数据层]
B -->|布局计算| C[布局层]
C -->|渲染指令| D[渲染层]
D -->|Canvas绑定| E[输出层]
style A fill:#e1f5ff
style E fill:#e8f5e9
优势说明 :
解耦合 :业务逻辑与渲染逻辑分离
可扩展 :支持多种渲染后端(Konva、X6等)
可维护 :清晰的职责划分,易于定位问题
可测试 :每层可独立测试
10.2 工厂模式应用
1 2 3 4 5 6 7 8 9 10 11 NODE_FACTORY_REF_MAP = { 'module-group' : ModuleGroupNode , 'interface' : InterfaceNode , 'boundary' : BoundaryNode , } createNode (type , config ) { return new NODE_FACTORY_REF_MAP [type ](config); }
优势 :
10.3 Diff更新机制
flowchart LR
A[旧Graph] --> B{Diff比较}
C[新Graph] --> B
B --> D[交集节点<br/>更新属性]
B --> E[新增节点<br/>创建实例]
B --> F[删除节点<br/>销毁实例]
D --> G[重新渲染]
E --> G
F --> G
style B fill:#fff3e0
style G fill:#e8f5e9
实现逻辑 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const nodesIntersection = _.intersection (sourceNodes, targetNodes);const nodesNotInSource = _.xor (targetNodes, nodesIntersection);const nodesNotInTarget = _.xor (sourceNodes, nodesIntersection);nodesNotInSource.forEach (nodeId => { sourceGraph.setNode (nodeId, targetGraph.node (nodeId)); }); nodesNotInTarget.forEach (nodeId => { const node = sourceGraph.node (nodeId); node.ref ?.dispose (); sourceGraph.removeNode (nodeId); }); nodesIntersection.forEach (nodeId => { const sourceNode = sourceGraph.node (nodeId); const targetNode = targetGraph.node (nodeId); sourceNode.ref ?.update (targetNode); });
十一、总结
11.1 构建流程总结
数据输入 :接收 modules、interfaces、calls
等参数
业务处理 :
构建边界节点(按 boundaryName 分组)
构建模块节点和接口节点(根据 showInterfaces 参数)
构建调用关系边
图结构 :使用 Graphlib 存储节点和边的图结构
布局计算 :使用 ELK 计算节点位置和边路径
渲染 :
工厂函数创建节点/边实例
节点/边实例创建 Konva 元素
添加到对应的 Layer
批量绘制
11.2 核心技术点
图数据结构 :Graphlib(支持复合图、多重图)
布局算法 :ELK(Eclipse Layout Kernel)
渲染引擎 :Konva(2D Canvas 渲染库)
设计模式 :工厂模式、观察者模式
优化策略 :Diff更新、批量渲染、虚拟化渲染
小地图(MiniMap)实现详解
一、小地图概述
小地图是拓扑图可视化中的重要辅助工具,它提供了一个全局视图的缩略图,帮助用户快速了解整体布局并进行导航。
核心功能
✅ 显示拓扑图的整体缩略图
✅ 显示当前视口位置(游标窗口)
✅ 通过小地图快速定位到指定位置
✅ 拖拽游标窗口同步更新主图视口
✅ 主图缩放/拖拽时自动同步游标窗口位置和大小
二、实现原理
2.1 整体架构
graph TB
subgraph UserLayer[用户交互层]
A[用户操作主图]
B[用户操作小地图]
end
subgraph KonvaLayer[Konva渲染层]
C[KonvaBackendRender<br/>渲染器后端]
D[graphStage<br/>主画布Stage]
end
subgraph MiniMapLayer[MiniMap层]
E[MiniMap<br/>小地图核心类]
F[wrapper<br/>容器DOM]
G[imageDomElement<br/>缩略图图片]
H[cursorWindow<br/>游标窗口]
end
subgraph SyncLayer[数据同步]
I[Stage事件监听]
J[DOM事件监听]
end
A --> I
B --> J
C --> E
D --> E
I --> E
J --> E
E --> H
E --> G
H --> D
style A fill:#fff3e0
style B fill:#fff3e0
style E fill:#e1f5ff
style H fill:#e8f5e9
2.2 核心设计思路
1. 双图联动机制
主图使用 Konva Stage 渲染(高性能)
小地图使用静态图片(缩略图)
通过计算比例关系实现坐标同步
2. 视口映射算法
计算视图缩放比例:viewScale = graphSize.width / containerSize.width
游标窗口大小 = 主图视口大小 / viewScale
游标窗口位置 = -主图位置 / viewScale
3. 双向同步
主图变化 → 监听Stage事件 → 更新游标窗口
小地图操作 → 监听DOM事件 → 更新主图Stage位置
三、核心代码实现
3.1 MiniMap 类结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 export class MiniMap { public wrapper : HTMLElement ; public cursorWindow : HTMLElement ; public resizePointer : HTMLElement ; public targetDomElement : HTMLElement ; public imageDomElement : HTMLImageElement ; public dragEleImgElement : HTMLImageElement ; public graphStage : Stage ; public graphSize : { width : number ; height : number }; public containerSize : { width : number ; height : number }; public viewWindowSize : { width : number ; height : number }; public viewWindowPosition : { x : number ; y : number }; public isCursorOnDrag = false ; public dragStartPosition = { x : 0 , y : 0 }; public get viewScale () { return this .graphSize .width / this .containerSize .width || 1 ; } public get cursorViewWindowActualSize () { return { width : this .viewWindowSize .width / this .viewScale , height : this .viewWindowSize .height / this .viewScale , }; } public get cursorViewWindowActualPosition () { return { x : this .viewWindowPosition .x / this .viewScale , y : this .viewWindowPosition .y / this .viewScale , }; } }
3.2 初始化流程
sequenceDiagram
participant Client as 调用方
participant Render as KonvaBackendRender
participant MiniMap as MiniMap
participant Stage as graphStage
participant DOM as DOM
Client->>Render: renderMiniMap(targetDomElement)
Render->>Render: 构建miniMapOptions
Render->>MiniMap: new MiniMap(options)
MiniMap->>DOM: getBoundingClientRect()
DOM-->>MiniMap: containerSize
MiniMap->>MiniMap: mountDom()
Note over MiniMap: 创建DOM结构<br/>wrapper, image, cursorWindow
MiniMap->>DOM: appendChild()
MiniMap->>MiniMap: listenDragEvent()
Note over MiniMap: 监听Stage事件<br/>监听DOM事件
Render->>MiniMap: updateMiniMapPreview()
MiniMap->>Stage: clone()
MiniMap->>Stage: toDataURL()
Stage-->>MiniMap: url
MiniMap->>DOM: imageDomElement.src = url
MiniMap->>MiniMap: initCursorWindowPosition()
3.3 DOM结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div class ="architecture-graph-minimap" style ="width: 100%; height: 100%;" > <img class ="architecture-graph-minimap-image" style ="width: ${containerSize.width}px" /> <div class ="architecture-graph-minimap-cursor-window" draggable ="true" > <div class ="architecture-graph-minimap-cursor-resize-pointer" > </div > <img class ="architecture-graph-minimap-cursor-window__dragimg" src ="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" /> </div > </div >
3.4 核心方法实现
3.4.1 渲染缩略图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public updateMiniMapPreview ( ) { if (!this .targetDomElement ) { return ; } const scale : number = this .containerSize .width / this .graphSize .width ; const clonedGraphStage = this .graphStage .clone ({ listen : false }); clonedGraphStage.position ({ x : 0 , y : 0 }); clonedGraphStage.size ({ width : this .graphSize .width , height : this .graphSize .height , }); const url = clonedGraphStage.toDataURL ({ pixelRatio : scale, }); this .imageDomElement .setAttribute ('src' , url); this .initCursorWindowPosition (); }
关键点说明 :
使用 clone() 复制Stage,避免影响主图
通过 pixelRatio 参数控制导出图片的分辨率
导出图片后立即初始化游标窗口
3.4.2 初始化游标窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public initCursorWindowPosition = () => { this .getViewWindowSize (); this .getViewWindowPosition (); this .setCursorWindowSize (); this .setCursorWindowPosition (); }; public getViewWindowSize ( ) { const viewWindowSize = { width : this .graphStage .width () / this .graphStage .scaleX (), height : this .graphStage .height () / this .graphStage .scaleY (), }; this .viewWindowSize = viewWindowSize; } public getViewWindowPosition ( ) { const viewWindowPosition = { x : this .graphStage .x () / this .graphStage .scaleX (), y : this .graphStage .y () / this .graphStage .scaleY (), }; this .viewWindowPosition = viewWindowPosition; } public setCursorWindowSize ( ) { this .cursorWindow .style .height = `${this .cursorViewWindowActualSize.height} px` ; this .cursorWindow .style .width = `${this .cursorViewWindowActualSize.width} px` ; } public setCursorWindowPosition ( ) { this .cursorWindow .style .left = `${-this .cursorViewWindowActualPosition.x} px` ; this .cursorWindow .style .top = `${-this .cursorViewWindowActualPosition.y} px` ; }
3.4.3 事件监听与双向同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 public listenDragEvent ( ) { this .graphStage .on ('scaleXChange' , this .onStageXScaleChange ); this .graphStage .on ('xChange' , this .onStageXChange ); this .graphStage .on ('yChange' , this .onStageYChange ); this .cursorWindow .addEventListener ('dragstart' , this .onCursorWindowDragStart ); this .wrapper .addEventListener ('click' , this .onWrapperClick ); this .wrapper .addEventListener ('dragover' , this .onWrapperDragOver ); this .wrapper .addEventListener ('dragend' , this .onWrapperDragEnd ); } public onStageXScaleChange = () => { if (this .isCursorOnDrag ) { return ; } this .initCursorWindowPosition (); }; public onStageXChange = () => { if (this .isCursorOnDrag ) { return ; } this .getViewWindowPosition (); this .setCursorWindowSize (); this .setCursorWindowPosition (); }; public onCursorWindowDragStart = (evt : DragEvent ) => { if (evt.dataTransfer ) { evt.dataTransfer .setDragImage (this .dragEleImgElement , 0 , 0 ); } this .dragStartPosition = { x : evt.x , y : evt.y , }; this .isCursorOnDrag = true ; }; public onWrapperClick = (evt : MouseEvent ) => { const { x : wrapperX, y : wrapperY } = this .wrapper .getBoundingClientRect (); const { height : cursorHeight, width : cursorWidth } = this .cursorWindow .getBoundingClientRect (); const xDiff = evt.x - wrapperX - cursorWidth / 2 ; const yDiff = evt.y - wrapperY - cursorHeight / 2 ; this .cursorWindow .style .left = `${xDiff} px` ; this .cursorWindow .style .top = `${yDiff} px` ; this .viewWindowPosition .x = -xDiff; this .viewWindowPosition .y = -yDiff; this .setGraphActualPosition (); }; public onWrapperDragOver = (evt : DragEvent ) => { if (this .isCursorOnDrag ) { const { x : wrapperX, y : wrapperY } = this .wrapper .getBoundingClientRect (); const { height : cursorHeight, width : cursorWidth } = this .cursorWindow .getBoundingClientRect (); const xDiff = evt.x - wrapperX - cursorWidth / 2 ; const yDiff = evt.y - wrapperY - cursorHeight / 2 ; this .cursorWindow .style .left = `${xDiff} px` ; this .cursorWindow .style .top = `${yDiff} px` ; this .viewWindowPosition .x = -xDiff; this .viewWindowPosition .y = -yDiff; this .setGraphActualPosition (); } }; public onWrapperDragEnd = (evt : DragEvent ) => { if (this .isCursorOnDrag ) { this .isCursorOnDrag = false ; } this .setGraphActualPosition (); }; public setGraphActualPosition ( ) { this .graphStage .position (this .graphStageActualPosition ); } public get graphStageActualPosition () { return { x : this .viewWindowPosition .x * this .viewScale * this .graphStage .scaleX (), y : this .viewWindowPosition .y * this .viewScale * this .graphStage .scaleY (), }; }
3.5 在渲染器中的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 export class KonvaBackendRender implements BackendRenderService { private minimap ?: MiniMap ; private graphWidth : number = 0 ; private graphHeight : number = 0 ; public renderMiniMap (targetDomElement : HTMLElement ): void { const miniMapOptions = { graphSize : { width : this .graphWidth , height : this .graphHeight , }, graphStage : this .graphStage , targetDomElement, }; logger.debug ('minimap options -> ' , miniMapOptions); if (!this .minimap ) { this .minimap = new MiniMap (miniMapOptions); } this .minimap .updateMiniMapPreview (); } public destoryMiniMap ( ) { if (!this .minimap ) { return ; } this .minimap .dispose (); this .minimap = undefined ; } }
四、关键算法详解
4.1 坐标转换公式
graph TD
A[主图 graphStage<br/>尺寸: graphSize.width × graphSize.height<br/>缩放: stage.scaleX, stage.scaleY<br/>位置: stage.x, stage.y]
B[小地图 MiniMap<br/>尺寸: containerSize.width × containerSize.height<br/>视图缩放比例: viewScale = graphSize.width / containerSize.width]
C[游标窗口 cursorWindow<br/>大小: viewWindowSize / viewScale<br/>位置: -viewWindowPosition / viewScale]
A -->|按比例缩小| B
B -->|映射| C
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#e8f5e9
4.2 核心公式汇总
视图缩放比例
viewScale = graphSize.width / containerSize.width
主图到小地图的缩放比
主图视口尺寸
viewWidth = stage.width() / stage.scaleX()
当前视口实际内容大小
游标窗口大小
cursorSize = viewSize / viewScale
游标窗口反映视口大小
主图视口位置
viewPos = stage.pos / stage.scale()
当前视口左上角在世界坐标系的位置
游标窗口位置
cursorPos = -viewPos / viewScale
游标窗口位置与视口位置相反
主图实际位置
actualPos = viewPos × viewScale × stage.scale()
反向计算主图应该的位置
4.3 坐标系说明
graph LR
A[屏幕坐标系<br/>CSS像素] -->|点击事件| B[容器坐标系<br/>相对位置]
B -->|除以 viewScale| C[世界坐标系<br/>实际图坐标]
C -->|乘以 scale| D[Stage坐标系<br/>渲染坐标]
E[Stage.position] -->|除以 scale| C
C -->|乘以 viewScale| B
B -->|CSS left/top| F[cursorWindow样式]
style A fill:#fff3e0
style D fill:#e1f5ff
style F fill:#e8f5e9
五、交互流程
5.1 点击小地图导航
sequenceDiagram
participant User as 用户
participant MiniMap as MiniMap
participant DOM as DOM事件
participant Stage as graphStage
User->>DOM: 点击小地图
DOM->>MiniMap: onWrapperClick(evt)
MiniMap->>MiniMap: 计算点击偏移
Note over MiniMap: xDiff = evt.x - wrapperX<br/> - cursorWidth / 2
MiniMap->>DOM: 更新cursorWindow样式
DOM->>User: 游标跳转到点击位置
MiniMap->>MiniMap: 更新viewWindowPosition
Note over MiniMap: viewWindowPosition.x = -xDiff
MiniMap->>MiniMap: 计算graphStageActualPosition
Note over MiniMap: x = viewPos × viewScale × scaleX
MiniMap->>Stage: stage.position(actualPos)
Stage->>User: 主图视口更新
5.2 拖拽游标导航
stateDiagram-v2
[*] --> 拖拽开始: dragstart
拖拽开始 --> 拖拽中: isCursorOnDrag=true
拖拽中 --> 拖拽中: dragover<br/>持续更新位置
拖拽中 --> 拖拽结束: dragend
拖拽结束 --> [*]: isCursorOnDrag=false
state 拖拽中 {
[*] --> 监听dragover事件
监听dragover事件 --> 计算新位置
计算新位置 --> 更新游标样式
更新游标样式 --> 计算主图位置
计算主图位置 --> 同步到Stage
同步到Stage --> 监听dragover事件
}
5.3 主图变化同步到小地图
flowchart TD
A[用户操作主图] --> B{操作类型}
B -->|缩放| C[scaleXChange事件]
B -->|拖拽| D[xChange / yChange事件]
C --> E[onStageXScaleChange]
D --> F[onStageXChange / onStageYChange]
E --> G{isCursorOnDrag?}
F --> G
G -->|否| H[getViewWindowSize]
G -->|否| I[getViewWindowPosition]
H --> J[setCursorWindowSize]
I --> K[setCursorWindowPosition]
J --> L[更新DOM样式]
K --> L
L --> M[游标窗口更新]
G -->|是| N[跳过更新<br/>避免冲突]
style A fill:#fff3e0
style M fill:#e8f5e9
style N fill:#ffebee
六、样式定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 .architecture-graph-minimap { -moz-user-select : none; -webkit-user-select : none; -ms-user-select : none; -khtml-user-select : none; user-select : none; overflow : hidden; } .architecture-graph-minimap-cursor-window { position : absolute; top : 0 ; left : 0 ; width : 10px ; height : 10px ; border : 1px solid #dddddd ; } .architecture-graph-minimap-cursor-resize-pointer { position : absolute; right : -4px ; bottom : -4px ; width : 4px ; height : 4px ; background : #dddddd ; cursor : nwse-resize; }
七、性能优化
7.1 拖拽期间禁用同步
1 2 3 4 5 6 7 8 9 10 public onStageXChange = () => { if (this .isCursorOnDrag ) { return ; } this .getViewWindowPosition (); this .setCursorWindowSize (); this .setCursorWindowPosition (); };
7.2 克隆时不监听事件
1 2 const clonedGraphStage = this .graphStage .clone ({ listen : false });
7.3 使用透明GIF隐藏拖拽图标
1 2 3 4 5 6 this .dragEleImgElement .setAttribute ( 'src' , 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' , ); evt.dataTransfer .setDragImage (this .dragEleImgElement , 0 , 0 );
八、使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { CallGraph } from '@tencent/wxpay-topology-diagram-vue3' ;const graph = new CallGraph ({ rootElement : document .getElementById ('container' ), modules : [...], interfaces : [...], calls : [...], miniMapElement : document .getElementById ('minimap-container' ), }); await graph.renderMiniMap ();graph.destoryMiniMap ();
1 2 3 4 5 6 7 8 <div style ="display: flex;" > <div id ="container" style ="width: 800px; height: 600px;" > </div > <div id ="minimap-container" style ="width: 200px; height: 150px; border: 1px solid #ccc;" > </div > </div >
九、扩展点
9.1 自定义样式
可以通过修改CSS类名自定义小地图样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .architecture-graph-minimap-cursor-window { border : 2px solid #1890ff ; background : rgba (24 , 144 , 255 , 0.1 ); border-radius : 4px ; } .architecture-graph-minimap-cursor-resize-pointer { background : #1890ff ; width : 8px ; height : 8px ; right : -4px ; bottom : -4px ; border-radius : 50% ; }
9.2 监听小地图事件
可以扩展MiniMap类,添加回调事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export interface MiniMapOptions { graphStage : Stage ; targetDomElement : HTMLElement ; graphSize : { width : number ; height : number }; onViewChange ?: (position : { x: number ; y: number } ) => void ; } export class MiniMap { constructor (private opts : MiniMapOptions ) { this .setGraphActualPosition = () => { this .opts .graphStage .position (this .graphStageActualPosition ); this .opts .onViewChange ?.(this .viewWindowPosition ); }; } }
十、总结
10.1 核心特点
轻量级
使用DOM + 图片,无额外图形库依赖
高性能
静态缩略图,只在需要时更新
双向同步
主图和小地图实时联动
易扩展
清晰的接口设计,支持自定义
10.2 技术要点
Konva Stage克隆技术 :使用
clone({ listen: false }) 避免事件冲突
坐标变换算法 :精确的视图坐标映射
事件冲突处理 :拖拽期间禁用同步避免循环更新
DOM与Canvas结合 :利用Canvas导出,DOM交互
10.3 适用场景
✅ 大规模拓扑图导航
✅ 复杂图表快速定位
✅ 地图类应用预览
✅ 需要全局视图的场景