JavaScript is required

布局系统

目的与范围

布局系统使用多种算法为图节点计算空间坐标 (x, y)。它实现了策略模式(Strategy Pattern):RelationGraphWith6Layout.doLayout() 会分发到特定算法类(RGTreeLayoutRGForceLayout 等),这些类会基于网络拓扑计算位置。

本页记录布局架构、算法实现与执行生命周期。节点/连线数据结构请参见 数据管理系统,与布局坐标交互的缩放/平移操作请参见 视图控制与效果

架构概览

布局系统通过工厂方法进行实例化,并实现了 策略模式。所有布局算法都继承自抽象基类,共享通用基础设施,同时实现各自特定的定位逻辑。

布局类层级

类层级图

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 -->|"继承"| RGSmartTreeLayout

RGLayout 接口契约

所有布局实现都实现了 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.xnode.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'
  • 对齐: 可配置 alignItemsXalignItemsY,用于在布局单元内对齐节点位置
  • 间距: treeNodeGapHtreeNodeGapV 控制节点之间的间隔
  • 层级间距: 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

将树结构与力微调结合的混合布局。

算法:

  1. 使用树分析进行网络拓扑分析
  2. 将节点放置为初始树结构
  3. 使用力导向调整以获得最终位置

关键特性:

  • 两者兼得:层级组织 + 自然间距
  • 支持所有树对齐选项
  • 通过 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.xnode.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 -.-> ReverseTree

node.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 根据 alignItemsXlot.xx
getNodeYByLotY() (alignOption, node) => number 根据 alignItemsYlot.yy
getNodeLotXY() (alignOption, node) => {x, y} 反向变换:x,ylot.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

算法细节:

  1. 主网络: 以根节点通过配置的算法进行布局
  2. 断开的连通分量: placeNextNetwork() 递归执行:
    • 选择第一个尚未放置的节点作为新 root
    • 使用相同算法调用 createLayout()
    • 若为力布局,将 maxLayoutTimes=0 以跳过动画
    • 设置 fixedRootNode=true 并将位置设为 (0,0)
    • 添加到 groupList
  3. 孤立节点: placeSingleNodes() 使用 rgSimpleGridLayout(),并根据计算得到的列数布局
  4. 最终排列: 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)建立后,布局能力即可使用。


关键要点:

  1. 策略模式: 通过 layoutName 配置实现布局算法可互换
  2. 两大族: 基于树的布局用于层级结构,基于力的布局用于自然定位
  3. 网络分析: 定位前通过 BasicNetworkAnalyzer 分析拓扑
  4. 双坐标: lot.x/lot.y 用于布局逻辑,x/y 用于最终渲染
  5. 多网络: 自动处理断开的图连通分量
  6. 可扩展性: 新布局可继承 RGBaseLayout 并在工厂中注册