布局系统
目的与范围
布局系统使用多种算法为图节点计算空间坐标 (x, y)。它实现了策略模式(Strategy Pattern):RelationGraphWith6Layout.doLayout() 会分发到特定算法类(RGTreeLayout、RGForceLayout 等),这些类会基于网络拓扑计算位置。
本页记录布局架构、算法实现与执行生命周期。节点/连线数据结构请参见 数据管理系统,与布局坐标交互的缩放/平移操作请参见 视图控制与效果。
架构概览
布局系统通过工厂方法进行实例化,并实现了 策略模式。所有布局算法都继承自抽象基类,共享通用基础设施,同时实现各自特定的定位逻辑。
布局类层级
类层级图
graph TB
RGWith6Layout["RelationGraphWith6Layout
doLayout()
createLayout()
placeOtherNodes()"]
RGBaseLayout["RGBaseLayout
abstract
placeNodes()
updateNodePosition()
layoutEnd()"]
NetworkAnalyzer["BasicNetworkAnalyzer
analyzeNetwork()"]
NodesAnalytic["RGNodesAnalytic
getNodeXByLotX()
getNodeYByLotY()"]
GraphMath["RGGraphMath
getOvalPoint()
getBorderPoint4MultiLine()"]
RGTreeLayout["RGTreeLayout
from: left|right|top|bottom
placeRelativePosition()"]
RGIOTreeLayout["RGIOTreeLayout
input/output trees
buildNetwork()"]
RGFolderLayout["RGFolderLayout
folder hierarchy
enableGatherNodes"]
RGForceLayout["RGForceLayout
physics simulation
start()/stop()
doForceLayout()"]
RGCenterLayout["RGCenterLayout
extends RGForceLayout
radial positioning"]
RGCircleLayout["RGCircleLayout
extends RGForceLayout
pure circular"]
RGSmartTreeLayout["RGSmartTreeLayout
tree + force hybrid"]
RGFixedLayout["RGFixedLayout
preserves positions"]
RGWith6Layout -->|"通过工厂实例化"| RGBaseLayout
RGBaseLayout -->|"使用"| NetworkAnalyzer
RGBaseLayout -->|"使用"| NodesAnalytic
RGBaseLayout -->|"使用"| GraphMath
RGBaseLayout -->|"继承"| RGTreeLayout
RGBaseLayout -->|"继承"| RGIOTreeLayout
RGBaseLayout -->|"继承"| RGFolderLayout
RGBaseLayout -->|"继承"| RGForceLayout
RGBaseLayout -->|"继承"| RGFixedLayout
RGForceLayout -->|"继承"| RGCenterLayout
RGForceLayout -->|"继承"| RGCircleLayout
RGForceLayout -->|"继承"| RGSmartTreeLayoutRGLayout 接口契约
所有布局实现都实现了 packages/relation-graph-models/types.ts:556-567 中定义的 RGLayout 接口:
| 属性/方法 | 类型 | 描述 |
|---|---|---|
isMainLayouer |
boolean |
标识主布局(控制自动布局行为) |
requireLinks |
boolean |
若为 true,则在 placeNodes() 之前必须调用 setLinks() |
allNodes |
RGNode[] |
需要定位的节点(由 placeNodes() 赋值) |
rootNode? |
RGNode |
分层算法的起始节点 |
layoutOptions |
RGLayoutOptions |
算法特定配置 |
networkAnalyzer |
RGNetworkAnalyzer |
BasicNetworkAnalyzer 的实例 |
setLinks() |
(links: RGLink[]) => void |
提供连线数据(当 requireLinks=true 时必需) |
placeNodes() |
(allNodes: RGNode[], rootNode?: RGNode) => void |
主要算法执行——设置 node.x、node.y |
start()? |
() => void |
启动动画循环(仅力导向布局) |
stop()? |
() => void |
停止动画循环(仅力导向布局) |
布局工厂与配置
createLayout() 工厂方法
布局工厂分发
graph TB
LayoutOptions["layoutOptions
{layoutName: string}"]
createLayout["createLayout<T extends RGLayout>()
RelationGraphWith6Layout:250-275"]
TreeCase["case 'tree':
new RGTreeLayout()"]
ForceCase["case 'force':
new RGForceLayout()"]
CenterCase["case 'center':
new RGCenterLayout()"]
CircleCase["case 'circle':
new RGCircleLayout()"]
FixedCase["case 'fixed':
new RGFixedLayout()"]
SmartTreeCase["case 'smart-tree':
new RGSmartTreeLayout()"]
IOTreeCase["case 'io-tree':
new RGIOTreeLayout()"]
FolderCase["case 'folder':
new RGFolderLayout()"]
LayoutOptions --> createLayout
createLayout -->|"layoutName='tree'"| TreeCase
createLayout -->|"layoutName='force'"| ForceCase
createLayout -->|"layoutName='center'"| CenterCase
createLayout -->|"layoutName='circle'"| CircleCase
createLayout -->|"layoutName='fixed'"| FixedCase
createLayout -->|"layoutName='smart-tree'"| SmartTreeCase
createLayout -->|"layoutName='io-tree'"| IOTreeCase
createLayout -->|"layoutName='folder'"| FolderCase该工厂会在实例化之前通过 appendDefaultOptions4Layout() 应用默认选项。每个布局类接收 (layoutOptions, graphOptions, graphInstance) 构造参数。
布局选项类型层级
类型定义:RGLayoutOptions
graph TB
RGLayoutOptionsCore["RGLayoutOptionsCore
types.ts:478-484
━━━━━━━━━━━━━━
layoutName: string
layoutDirection?: 'h'|'v'
fixedRootNode?: boolean
alignItemsX?: start|center|end
alignItemsY?: start|center|end"]
RGForceLayoutOptions["RGForceLayoutOptions
types.ts:495-502
━━━━━━━━━━━━━━
fastStart?: boolean
maxLayoutTimes?: number
byNode?: boolean
byLine?: boolean
force_node_repulsion?: number
force_line_elastic?: number"]
RGCenterLayoutOptions["RGCenterLayoutOptions
types.ts:508-511
━━━━━━━━━━━━━━
distance_coefficient?: number
levelDistance?: number[]"]
RGTreeLayoutOptions["RGTreeLayoutOptions
types.ts:524-534
━━━━━━━━━━━━━━
from: left|right|top|bottom
treeNodeGapH?: number
treeNodeGapV?: number
levelGaps?: number[]
layoutExpansionDirection?: start|center|end
simpleTree?: boolean
ignoreNodeSize?: boolean
alignParentItemsX?: start|center|end
alignParentItemsY?: start|center|end"]
RGLayoutOptionsCore -->|"继承"| RGForceLayoutOptions
RGForceLayoutOptions -->|"继承"| RGCenterLayoutOptions
RGLayoutOptionsCore -->|"继承"| RGTreeLayoutOptions布局家族
基于树的布局
树布局根据网络拓扑将节点组织为层级结构。它们从根节点开始遍历图,并以计算得到的偏移量放置后代节点。
RGTreeLayout
标准树布局,具备可配置的方向与对齐方式。
关键特性:
- 方向: 通过
from选项支持'left'、'right'、'top'、'bottom' - 对齐: 可配置
alignItemsX、alignItemsY,用于在布局单元内对齐节点位置 - 间距:
treeNodeGapH与treeNodeGapV控制节点之间的间隔 - 层级间距:
levelGaps[]允许为各层级设置自定义间距 - 双向: 默认同时向关系层级的上与下扩展,除非
simpleTree=true
配置示例:
layoutOptions: {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 50,
treeNodeGapV: 10,
alignItemsX: 'end',
alignItemsY: 'center',
levelGaps: [100, 150, 200]
}
RGIOTreeLayout
面向输入/输出树结构的变体,适用于关系具有明确方向语义的场景。
与 RGTreeLayout 的差异:
- 针对具有显式输入/输出节点模式的图进行了优化
- 增强了对混合父/子关系节点的处理
- 针对
childrenSize与可见性计算提供了特殊逻辑
RGFolderLayout
用于模拟文件夹层级可视化的专用树布局。
关键特性:
- 基于缩进的视觉层级
- 可配置
bottomJuctionPointOffsetX以控制连接线位置 - 通过
enableGatherNodes选项内置聚合节点能力 - 针对文件系统或组织结构图可视化进行了优化
力导向布局
力导向布局使用物理仿真进行节点定位。它们在连线上施加吸引力、在节点间施加斥力,以获得平衡且自然的布局。
RGForceLayout
基于迭代仿真的核心物理布局。
物理模型:
- 节点斥力: 节点之间根据
force_node_repulsion系数相互排斥 - 连线弹性: 相连节点根据
force_line_elastic系数相互吸引 - 迭代: 最多运行
maxLayoutTimes次迭代,或直到各力达到平衡
关键配置:
layoutOptions: {
layoutName: 'force',
fastStart: false, // 跳过初始定位
maxLayoutTimes: 300, // 最大迭代次数
byNode: true, // 启用节点斥力
byLine: true, // 启用连线吸引力
force_node_repulsion: 1, // 斥力强度
force_line_elastic: 1 // 吸引力强度
}
动画支持:
力布局通过 start() 与 stop() 方法支持实时动画。当启用 autoLayouting 时,布局会持续更新节点位置。
RGCenterLayout
在力布局之上扩展,围绕中心节点进行径向定位。
关键特性:
- 初始放置使用以根节点为中心的径向/环形定位
distance_coefficient用于缩放各层级的理想距离levelDistance[]数组可为每个层级指定自定义距离- 继承全部力仿真能力
RGCircleLayout
纯环形排列,不进行力仿真。
关键特性:
- 将可见节点围绕根节点放置成一圈
- 根据节点数量自动计算半径
- 无物理仿真——静态定位
- 半径限制在 200 到 800 单位之间
RGSmartTreeLayout
将树结构与力微调结合的混合布局。
算法:
- 使用树分析进行网络拓扑分析
- 将节点放置为初始树结构
- 使用力导向调整以获得最终位置
关键特性:
- 两者兼得:层级组织 + 自然间距
- 支持所有树对齐选项
- 通过
rotate选项支持旋转 - 双向树分析
固定布局
保留用户指定的节点位置,不做修改。
使用场景:
- 用户已手动摆放节点
- 加载已保存的图布局
- 具有预设坐标的静态图示
行为:
- 执行网络分析,但跳过位置计算
- 尊重
node.fixed属性 - 仅调用
layoutEnd()以完成生命周期
布局执行过程
doLayout() 执行流程
方法调用顺序:RelationGraphWith6Layout.doLayout()
sequenceDiagram
participant User
participant doLayout["doLayout()
:30-45"]
participant _doLayout["_doLayout()
:51-96"]
participant createLayout["createLayout()
:250-275"]
participant LayoutImpl["布局实现
(e.g. RGTreeLayout)"]
participant NetworkAnalyzer["BasicNetworkAnalyzer"]
User->>doLayout: doLayout(customRootNode?)
doLayout->>doLayout: sleep(300 - timeSinceLastAdd)
doLayout->>_doLayout: _doLayout(customRootNode)
_doLayout->>_doLayout: updateNodesVisibleProperty()
_doLayout->>_doLayout: getNodes()
_doLayout->>_doLayout: getRootNode() or use first node
_doLayout->>createLayout: createLayout(options.layout, true)
createLayout-->>_doLayout: layoutInstance
alt layoutName === 'force'
_doLayout->>LayoutImpl: placeNodes(allNodes, rootNode)
LayoutImpl->>NetworkAnalyzer: analyzeNetwork()
LayoutImpl->>LayoutImpl: 初始定位
Note over LayoutImpl: 若 isMainLayouer,则会通过
start() 启动自动布局
else Tree/Other Layouts
_doLayout->>LayoutImpl: placeNodes(allNodes, rootNode)
LayoutImpl->>NetworkAnalyzer: analyzeNetwork()
LayoutImpl->>LayoutImpl: 计算位置
LayoutImpl->>LayoutImpl: layoutEnd()
LayoutImpl-->>_doLayout: 位置计算完成
_doLayout->>_doLayout: placeOtherNodes(mainGroupNodes)
end
_doLayout->>_doLayout: forEach node: updateNode(id, {x, y})
_doLayout->>_doLayout: updateElementLines()布局生命周期方法
| 方法 | 位置 | 签名 | 描述 |
|---|---|---|---|
placeNodes() |
各布局实现 | (allNodes: RGNode[], rootNode?: RGNode) => void |
主要定位算法——必须设置 node.x、node.y |
layoutEnd() |
RGBaseLayout:132-143 |
() => void |
触发 onLayoutCompleted 事件 |
updateNodePosition() |
RGBaseLayout:132-143 |
(node: RGNode, x: number, y: number) => void |
调用 graphInstance.updateNode(node.id, {x, y}) |
placeOtherNodes() |
RelationGraphWith6Layout:117-153 |
(mainGroupNodes: RGNode[]) => void |
递归布局断开的连通分量 |
placeSingleNodes() |
RelationGraphWith6Layout:159-176 |
(singleNodes: RGNode[]) => void |
通过 rgSimpleGridLayout() 进行网格布局 |
sortGroups() |
RelationGraphWith6Layout:182-210 |
(groupList: {nodes: RGNode[]}[]) => void |
排列多个分量组 |
网络分析基础设施
BasicNetworkAnalyzer
网络拓扑分析流程
graph TB
Input["输入:
allNodes: RGNode[]
rootNode: RGNode"]
Analyzer["BasicNetworkAnalyzer
analyzeNetwork()"]
BuildTree["buildTree()
━━━━━━━━━━━━━━
从 root 遍历
设置 node.lot.level
设置 node.lot.childs
设置 node.lot.parent"]
CalcStrength["calcStrength()
━━━━━━━━━━━━━━
计算子树大小
设置 node.lot.strength"]
Output["输出:
tree.networkNodes
tree.analyticResult
━━━━━━━━━━━━━━
max_deep: number
max_strength: number"]
ReverseTree["可选:
reverseTree analysis
(若 bidirectional=true)"]
Input --> Analyzer
Analyzer --> BuildTree
BuildTree --> CalcStrength
CalcStrength --> Output
Analyzer -.-> ReverseTreenode.lot 结构 (types.ts:250-271)
node.lot 对象存放由 BasicNetworkAnalyzer 填充的布局相关元数据:
node.lot = {
childs: RGNode[], // 直接子节点(由 buildTree 填充)
parent?: RGNode, // 父节点引用
level?: number, // 从 root 开始的层级深度(0 = root)
strength?: number, // 子树大小(子树中的节点数量)
childrenSize?: number, // 直接子节点数量
childrenSizeVisible?: number, // 可见子节点数量(已展开)
x?: number, // 临时布局坐标
y?: number, // 临时布局坐标
// 特定布局使用的其他属性
}
坐标系统与对齐
坐标变换流水线
graph LR
LotXY["node.lot.x
node.lot.y
━━━━━━━━━━━━━━
布局坐标
(逻辑位置)"]
Align["RGNodesAnalytic
getNodeXByLotX()
getNodeYByLotY()
━━━━━━━━━━━━━━
应用 alignItemsX/Y"]
NodeXY["node.x
node.y
━━━━━━━━━━━━━━
画布坐标
(渲染位置)"]
LotXY -->|"对齐变换"| Align
Align --> NodeXY对齐选项 (types.ts:466-469)
alignItemsX/Y |
对坐标的影响 |
|---|---|
'start' |
node.x = lot.x(左上角位于 lot 位置) |
'center' |
node.x = lot.x - node.el_W / 2(中心位于 lot 位置) |
'end' |
node.x = lot.x - node.el_W(右下角位于 lot 位置) |
RGTreeLayout 中的示例用法:
// After calculating lot.x, lot.y in layout space:
const x = RGNodesAnalytic.getNodeXByLotX({alignItemsX: 'end'}, thisNode);
const y = RGNodesAnalytic.getNodeYByLotY({alignItemsY: 'center'}, thisNode);
this.updateNodePosition(thisNode, x, y);
支撑工具
RGNodesAnalytic
节点测量与定位工具
位于 packages/relation-graph-models/utils/RGNodesAnalytic.ts,该模块提供:
| 函数 | 签名 | 目的 |
|---|---|---|
getNodeWidth() |
(node: RGNode) => number |
返回 node.width || node.el_W |
getNodeHeight() |
(node: RGNode) => number |
返回 node.height || node.el_H |
getNodeXByLotX() |
(alignOption, node) => number |
根据 alignItemsX 将 lot.x → x |
getNodeYByLotY() |
(alignOption, node) => number |
根据 alignItemsY 将 lot.y → y |
getNodeLotXY() |
(alignOption, node) => {x, y} |
反向变换:x,y → lot.x, lot.y |
isVisibleNode() |
(node: RGNode) => boolean |
考虑父节点展开状态检查可见性 |
RGGraphMath
布局相关数学运算
位于 packages/relation-graph-models/utils/RGGraphMath.ts:
| 函数 | 签名 | 目的 |
|---|---|---|
getOvalPoint() |
(cx, cy, r, index, total) => {x, y} |
环形布局的椭圆点 |
getBorderPoint4MultiLine() |
(params: CreateJunctionPointParams) => {x, y} |
连线-节点的连接点 |
getRotatedPoint() |
(x, y, cx, cy, angle) => {x, y} |
围绕中心旋转坐标 |
getFlippedX() |
(x, centerX) => number |
镜像 X 坐标 |
getFlippedY() |
(y, centerY) => number |
镜像 Y 坐标 |
getNodeDistance() |
(x1, y1, x2, y2) => number |
欧氏距离 |
rgSimpleGridLayout() |
(columns, gap, items[], callback) |
网格定位算法 |
布局配置选项
核心选项(所有布局)
interface RGLayoutOptionsCore {
layoutName: string; // 'tree' | 'force' | 'center' | 'circle' | 'fixed' | 'smart-tree' | 'io-tree' | 'folder'
layoutDirection?: 'h' | 'v'; // Horizontal or vertical
fixedRootNode?: boolean; // Keep root at current position
alignItemsX?: 'start' | 'center' | 'end';
alignItemsY?: 'start' | 'center' | 'end';
}
树布局选项
interface RGTreeLayoutOptions extends RGLayoutOptionsCore {
from: 'left' | 'right' | 'top' | 'bottom';
treeNodeGapH?: number; // Horizontal gap between nodes
treeNodeGapV?: number; // Vertical gap between nodes
levelGaps?: number[]; // Custom gap for each level
layoutExpansionDirection?: 'start' | 'center' | 'end';
simpleTree?: boolean; // Unidirectional expansion
ignoreNodeSize?: boolean; // Treat all nodes as same size
alignParentItemsX?: 'start' | 'center' | 'end';
alignParentItemsY?: 'start' | 'center' | 'end';
}
力布局选项
interface RGForceLayoutOptions extends RGLayoutOptionsCore {
fastStart?: boolean; // Skip initial center layout
maxLayoutTimes?: number; // Max iterations (default: 300)
byNode?: boolean; // Enable node repulsion
byLine?: boolean; // Enable line attraction
force_node_repulsion?: number; // Repulsion coefficient (default: 1)
force_line_elastic?: number; // Attraction coefficient (default: 1)
}
中心布局选项
interface RGCenterLayoutOptions extends RGForceLayoutOptions {
distance_coefficient?: number; // Scale ideal distances
levelDistance?: number[]; // Custom distance per level
}
多网络处理
断开连通分量的布局策略
graph TB
AllNodes["getNodes()
所有图节点"]
Classify["节点分类
RelationGraphWith6Layout:117-153"]
MainGroup["mainGroupNodes
getNetworkNodesByNode(root)"]
OtherNetworks["notInMainGroupNodes
断开的连通分量"]
SingleNodes["singleNodes
无连接"]
MainLayout["布局主网络
doLayout(rootNode)"]
PlaceNext["placeNextNetwork()
:216-248
━━━━━━━━━━━━━━
递归布局
每个分量"]
PlaceSingle["placeSingleNodes()
:159-176
━━━━━━━━━━━━━━
rgSimpleGridLayout()"]
SortGroups["sortGroups()
:182-210
━━━━━━━━━━━━━━
以网格模式
排列分组"]
AllNodes --> Classify
Classify --> MainGroup
Classify --> OtherNetworks
Classify --> SingleNodes
MainGroup --> MainLayout
OtherNetworks --> PlaceNext
SingleNodes --> PlaceSingle
MainLayout --> SortGroups
PlaceNext --> SortGroups
PlaceSingle --> SortGroups算法细节:
- 主网络: 以根节点通过配置的算法进行布局
- 断开的连通分量:
placeNextNetwork()递归执行:- 选择第一个尚未放置的节点作为新 root
- 使用相同算法调用
createLayout() - 若为力布局,将
maxLayoutTimes=0以跳过动画 - 设置
fixedRootNode=true并将位置设为 (0,0) - 添加到
groupList
- 孤立节点:
placeSingleNodes()使用rgSimpleGridLayout(),并根据计算得到的列数布局 - 最终排列:
sortGroups()将每个分量视为一个“超级节点”,并以网格方式排列
性能考量
布局优化策略
| 策略 | 实现 | 使用场景 |
|---|---|---|
| 快速启动 | fastStart: true |
跳过大型力布局的初始定位 |
| 迭代上限 | maxLayoutTimes |
控制力布局的计算时间 |
| 忽略节点尺寸 | ignoreNodeSize: true |
当节点尺寸相近时加速树布局 |
| 固定节点 | node.fixed = true |
将特定节点排除在重新定位之外 |
| 禁用实时更新 | disableLiveChanges: true |
在力动画期间降低开销 |
节点可见性优化
在布局执行之前,系统会调用 updateNodesVisibleProperty() 来计算应渲染的节点,依据包括:
- 父节点展开状态(
node.expanded) - 祖先可见性链
- 显式隐藏(
node.hidden)
这会将布局计算范围缩小到仅可见节点。
与核心系统的集成
布局系统通过 RelationGraphWith6Layout 与核心图实例集成,它位于继承链中:
RelationGraphBase
→ RelationGraphWith1View
→ RelationGraphWith2Data
→ RelationGraphWith3Options
→ RelationGraphWith4Line
→ RelationGraphWith5Zoom
→ RelationGraphWith6Effect
→ RelationGraphWith6Layout ← 布局集成点
→ RelationGraphWith7Event
→ ...
→ RelationGraphCore
这种层级位置确保在数据管理(With2Data)、选项管理(With3Options)与效果(With6Effect)建立后,布局能力即可使用。
关键要点:
- 策略模式: 通过
layoutName配置实现布局算法可互换 - 两大族: 基于树的布局用于层级结构,基于力的布局用于自然定位
- 网络分析: 定位前通过
BasicNetworkAnalyzer分析拓扑 - 双坐标:
lot.x/lot.y用于布局逻辑,x/y用于最终渲染 - 多网络: 自动处理断开的图连通分量
- 可扩展性: 新布局可继承
RGBaseLayout并在工厂中注册