import EventEmitter from '@antv/event-emitter'; import type { AABB, BaseStyleProps } from '@antv/g'; import { debounce, isEqual, isFunction, isNumber, isObject, isString, omit } from '@antv/util'; import { COMBO_KEY, GraphEvent } from '../constants'; import type { Plugin } from '../plugins/types'; import type { BehaviorOptions, ComboData, ComboOptions, EdgeData, EdgeOptions, GraphData, GraphOptions, LayoutOptions, NodeData, NodeOptions, PluginOptions, ThemeOptions, TransformOptions, } from '../spec'; import type { UpdateBehaviorOption } from '../spec/behavior'; import type { UpdatePluginOption } from '../spec/plugin'; import type { UpdateTransformOption } from '../spec/transform'; import type { DataID, EdgeDirection, ElementDatum, ElementType, FitViewOptions, HierarchyKey, ID, IEvent, NodeLikeData, PartialEdgeData, PartialGraphData, PartialNodeLikeData, Point, State, Vector2, ViewportAnimationEffectTiming, } from '../types'; import { isCollapsed } from '../utils/collapsibility'; import { sizeOf } from '../utils/dom'; import { getSubgraphRelatedEdges } from '../utils/edge'; import { GraphLifeCycleEvent, emit } from '../utils/event'; import { idOf } from '../utils/id'; import { isPreLayout } from '../utils/layout'; import { format } from '../utils/print'; import { subtract } from '../utils/vector'; import { getZIndexOf } from '../utils/z-index'; import { Animation } from './animation'; import { BatchController } from './batch'; import { BehaviorController } from './behavior'; import type { DataURLOptions } from './canvas'; import { Canvas } from './canvas'; import { DataController } from './data'; import type { CollapseExpandNodeOptions } from './element'; import { ElementController } from './element'; import { LayoutController } from './layout'; import { inferOptions } from './options'; import { PluginController } from './plugin'; import { TransformController } from './transform'; import { RuntimeContext } from './types'; import { ViewportController } from './viewport'; export class Graph extends EventEmitter { private options: GraphOptions = {}; /** * @internal */ static defaultOptions: GraphOptions = { autoResize: false, theme: 'light', rotation: 0, zoom: 1, zoomRange: [0.01, 10], }; /** * 当前图实例是否已经渲染 * * Whether the current graph instance has been rendered */ public rendered = false; /** * 当前图实例是否已经被销毁 * * Whether the current graph instance has been destroyed */ public destroyed = false; // @ts-expect-error will be initialized in createRuntime private context: RuntimeContext = { model: new DataController(), }; constructor(options: GraphOptions) { super(); this._setOptions(Object.assign({}, Graph.defaultOptions, options), true); this.context.graph = this; // Listening resize to autoResize. this.options.autoResize && globalThis.addEventListener?.('resize', this.onResize); } /** * 获取配置项 * * Get options * @returns 配置项 | options * @apiCategory option */ public getOptions(): GraphOptions { return this.options; } /** * 设置配置项 * * Set options * @param options - 配置项 | options * @remarks * 要更新 devicePixelRatio、container 属性请销毁后重新创建实例 * * To update devicePixelRatio and container properties, please destroy and recreate the instance * @apiCategory option */ public setOptions(options: GraphOptions): void { this._setOptions(options, false); } private _setOptions(options: GraphOptions, isInit: boolean) { this.updateCanvas(options); Object.assign(this.options, inferOptions(options)); if (isInit) { const { data } = options; if (data) this.addData(data); return; } const { behaviors, combo, data, edge, layout, node, plugins, theme, transforms } = options; if (behaviors) this.setBehaviors(behaviors); if (data) this.setData(data); if (node) this.setNode(node); if (edge) this.setEdge(edge); if (combo) this.setCombo(combo); if (layout) this.setLayout(layout); if (theme) this.setTheme(theme); if (plugins) this.setPlugins(plugins); if (transforms) this.setTransforms(transforms); } /** * 获取当前画布容器的尺寸 * * Get the size of the current canvas container * @returns 画布尺寸 | canvas size * @apiCategory canvas */ public getSize(): [number, number] { if (this.context.canvas) return this.context.canvas.getSize(); return [this.options.width || 0, this.options.height || 0]; } /** * 设置当前画布容器的尺寸 * * Set the size of the current canvas container * @param width - 画布宽度 | canvas width * @param height - 画布高度 | canvas height * @apiCategory canvas */ public setSize(width: number, height: number): void { if (width) this.options.width = width; if (height) this.options.height = height; this.resize(width, height); } /** * 设置当前图的缩放区间 * * Get the zoom range of the current graph * @param zoomRange - 缩放区间 | zoom range * @apiCategory viewport */ public setZoomRange(zoomRange: GraphOptions['zoomRange']): void { this.options.zoomRange = zoomRange; } /** * 获取当前图的缩放区间 * * Get the zoom range of the current graph * @returns 缩放区间 | zoom range * @apiCategory viewport */ public getZoomRange(): GraphOptions['zoomRange'] { return this.options.zoomRange; } /** * 设置节点样式映射 * * Set node mapper * @param node - 节点配置 | node options * @remarks * 即 `options.node` 的值 * * The value of `options.node` * @apiCategory element */ public setNode(node: NodeOptions): void { this.options.node = node; this.context.model.refreshData(); } /** * 设置边样式映射 * * Set edge mapper * @param edge - 边配置 | edge options * @remarks * 即 `options.edge` 的值 * * The value of `options.edge` * @apiCategory element */ public setEdge(edge: EdgeOptions): void { this.options.edge = edge; this.context.model.refreshData(); } /** * 设置组合样式映射 * * Set combo mapper * @param combo - 组合配置 | combo options * @remarks * 即 `options.combo` 的值 * * The value of `options.combo` * @apiCategory element */ public setCombo(combo: ComboOptions): void { this.options.combo = combo; this.context.model.refreshData(); } /** * 获取主题 * * Get theme * @returns 当前主题 | current theme * @apiCategory theme */ public getTheme(): ThemeOptions { return this.options.theme!; } /** * 设置主题 * * Set theme * @param theme - 主题名 | theme name * @example * ```ts * graph.setTheme('dark'); * ``` * @apiCategory theme */ public setTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void { this.options.theme = isFunction(theme) ? theme(this.getTheme()) : theme; } /** * 设置布局 * * Set layout * @param layout - 布局配置 | layout options * @example * ```ts * graph.setLayout({ * type: 'dagre', * }) * ``` * @apiCategory layout */ public setLayout(layout: LayoutOptions | ((prev: LayoutOptions) => LayoutOptions)): void { this.options.layout = isFunction(layout) ? layout(this.getLayout()) : layout; } /** * 获取布局配置 * * Get layout options * @returns 布局配置 | layout options * @apiCategory layout */ public getLayout(): LayoutOptions { return this.options.layout!; } /** * 设置交互 * * Set behaviors * @param behaviors - 交互配置 | behavior options * @remarks * 设置的交互会全量替换原有的交互,如果需要新增交互可以使用如下方式: * * The set behavior will completely replace the original behavior. If you need to add behavior, you can use the following method: * * ```ts * graph.setBehaviors((behaviors) => [...behaviors, { type: 'zoom-canvas' }]) * ``` * @apiCategory behavior */ public setBehaviors(behaviors: BehaviorOptions | ((prev: BehaviorOptions) => BehaviorOptions)): void { this.options.behaviors = isFunction(behaviors) ? behaviors(this.getBehaviors()) : behaviors; this.context.behavior?.setBehaviors(this.options.behaviors); } /** * 更新指定的交互配置 * * Update specified behavior options * @param behavior - 交互配置 | behavior options * @remarks * 如果要更新一个交互,那么必须在交互配置中指定 key 字段,例如: * * If you want to update a behavior, you must specify the key field in the behavior options, for example: * ```ts * { * behaviors: [{ type: 'zoom-canvas', key: 'zoom-canvas' }] * } * * graph.updateBehavior({ key: 'zoom-canvas', enable: false }) * ``` * @apiCategory behavior */ public updateBehavior(behavior: UpdateBehaviorOption): void { this.setBehaviors((behaviors) => behaviors.map((_behavior) => { if (typeof _behavior === 'object' && _behavior.key === behavior.key) { return { ..._behavior, ...behavior }; } return _behavior; }), ); } /** * 获取交互配置 * * Get behaviors options * @returns 交互配置 | behavior options * @apiCategory behavior */ public getBehaviors(): BehaviorOptions { return this.options.behaviors || []; } /** * 设置插件配置 * * Set plugins options * @param plugins - 插件配置 | plugin options * @remarks * 设置的插件会全量替换原有的插件配置,如果需要新增插件可以使用如下方式: * * The set plugin will completely replace the original plugin configuration. If you need to add a plugin, you can use the following method: * ```ts * graph.setPlugins((plugins) => [...plugins, { key: 'grid-line' }]) * ``` * @apiCategory plugin */ public setPlugins(plugins: PluginOptions | ((prev: PluginOptions) => PluginOptions)): void { this.options.plugins = isFunction(plugins) ? plugins(this.getPlugins()) : plugins; this.context.plugin?.setPlugins(this.options.plugins); } /** * 更新插件配置 * * Update plugin options * @param plugin - 插件配置 | plugin options * @remarks * 如果要更新一个插件,那么必须在插件配置中指定 key 字段,例如: * * If you want to update a plugin, you must specify the key field in the plugin options, for example: * ```ts * { * plugins: [{ key: 'grid-line' }] * } * * graph.updatePlugin({ key: 'grid-line', follow: true }) * ``` * @apiCategory plugin */ public updatePlugin(plugin: UpdatePluginOption): void { this.setPlugins((plugins) => plugins.map((_plugin) => { if (typeof _plugin === 'object' && _plugin.key === plugin.key) { return { ..._plugin, ...plugin }; } return _plugin; }), ); } /** * 获取插件配置 * * Get plugins options * @returns 插件配置 | plugin options * @apiCategory plugin */ public getPlugins(): PluginOptions { return this.options.plugins || []; } /** * 获取插件实例 * * Get plugin instance * @param key - 插件 key(在配置 plugin 时需要手动传入指定) | plugin key(need to be specified manually when configuring plugin) * @returns 插件实例 | plugin instance * @remarks * 一些插件提供了 API 方法可供调用,例如全屏插件可以调用 `request` 和 `exit` 方法来请求和退出全屏 * * Some plugins provide API methods for calling, such as the full-screen plugin can call the `request` and `exit` methods to request and exit full-screen * ```ts * const fullscreen = graph.getPluginInstance('fullscreen'); * * fullscreen.request(); * * fullscreen.exit(); * ``` * @apiCategory plugin */ public getPluginInstance(key: string) { return this.context.plugin!.getPluginInstance(key) as unknown as T; } /** * 设置数据转换器 * * Set data transforms * @param transforms - 数据转换配置 | data transform options * @remarks * 数据转换器能够在图渲染过程中执行数据转换,目前支持在渲染前对绘制数据进行转化。 * * Data transforms can perform data transformation during the rendering process of the graph. Currently, it supports transforming the drawing data before rendering. * @apiCategory transform */ public setTransforms(transforms: TransformOptions | ((prev: TransformOptions) => TransformOptions)): void { this.options.transforms = isFunction(transforms) ? transforms(this.getTransforms()) : transforms; this.context.transform?.setTransforms(this.options.transforms); } /** * 更新数据转换器 * * Update data transform * @param transform - 数据转换器配置 | data transform options * @apiCategory transform */ public updateTransform(transform: UpdateTransformOption): void { this.setTransforms((transforms) => transforms.map((_transform) => { if (typeof _transform === 'object' && _transform.key === transform.key) { return { ..._transform, ...transform }; } return _transform; }), ); this.context.model.refreshData(); } /** * 获取数据转换器配置 * * Get data transforms options * @returns 数据转换配置 | data transform options * @apiCategory transform */ public getTransforms(): TransformOptions { return this.options.transforms || []; } /** * 获取图数据 * * Get graph data * @returns 图数据 | Graph data * 获取当前图的数据,包括节点、边、组合数据 * * Get the data of the current graph, including node, edge, and combo data * @apiCategory data */ public getData(): Required { return this.context.model.getData(); } /** * 获取单个元素数据 * * Get element data by ID * @param id - 元素 ID | element ID * @returns 元素数据 | element data * @remarks * 直接获取元素的数据而不必考虑元素类型 * * Get element data directly without considering the element type * @apiCategory data */ public getElementData(id: ID): ElementDatum; /** * 批量获取多个元素数据 * * Get multiple element data in batch * @param ids - 元素 ID 数组 | element ID array * @remarks * 直接获取元素的数据而不必考虑元素类型 * * Get element data directly without considering the element type * @apiCategory data */ public getElementData(ids: ID[]): ElementDatum[]; public getElementData(ids: ID | ID[]): ElementDatum | ElementDatum[] { if (Array.isArray(ids)) return ids.map((id) => this.context.model.getElementDataById(id)); return this.context.model.getElementDataById(ids); } /** * 获取所有节点数据 * * Get all node data * @returns 节点数据 | node data * @apiCategory data */ public getNodeData(): NodeData[]; /** * 获取单个节点数据 * * Get single node data * @param id - 节点 ID | node ID * @returns 节点数据 | node data * @example * ```ts * const node1 = graph.getNodeData('node-1'); * ``` * @apiCategory data * @remarks * 节点 id 必须存在,否则会抛出异常 * * Node id must exist, otherwise an exception will be thrown */ public getNodeData(id: ID): NodeData; /** * 批量获取多个节点数据 * * Get multiple node data in batch * @param ids - 节点 ID 数组 | node ID array * @returns 节点数据 | node data * @example * ```ts * const [node1, node2] = graph.getNodeData(['node-1', 'node-2']); * ``` * @apiCategory data * @remarks * 数组中的每个节点 id 必须存在,否则将抛出异常 * * Each node id in the array must exist, otherwise an exception will be thrown */ public getNodeData(ids: ID[]): NodeData[]; public getNodeData(id?: ID | ID[]): NodeData | NodeData[] { if (id === undefined) return this.context.model.getNodeData(); if (Array.isArray(id)) return this.context.model.getNodeData(id); return this.context.model.getNodeLikeDatum(id); } /** * 获取所有边数据 * * Get all edge data * @returns 边数据 | edge data * @apiCategory data */ public getEdgeData(): EdgeData[]; /** * 获取单条边数据 * * Get single edge data * @param id - 边 ID | edge ID * @returns 边数据 | edge data * @example * ```ts * const edge1 = graph.getEdgeData('edge-1'); * ``` * @apiCategory data * @remarks * 边 id 必须存在,否则会抛出异常 * * Edge id must exist, otherwise an exception will be thrown */ public getEdgeData(id: ID): EdgeData; /** * 批量获取多条边数据 * * Get multiple edge data in batch * @param ids - 边 ID 数组 | edge ID array * @returns 边数据 | edge data * @example * ```ts * const [edge1, edge2] = graph.getEdgeData(['edge-1', 'edge-2']); * ``` * @apiCategory data * @remarks * 数组中的每个边 id 必须存在,否则将抛出异常 * * Each edge id in the array must exist, otherwise an exception will be thrown */ public getEdgeData(ids: ID[]): EdgeData[]; public getEdgeData(id?: ID | ID[]): EdgeData | EdgeData[] { if (id === undefined) return this.context.model.getEdgeData(); if (Array.isArray(id)) return this.context.model.getEdgeData(id); return this.context.model.getEdgeDatum(id); } /** * 获取所有组合数据 * * Get all combo data * @returns 组合数据 | combo data * @apiCategory data */ public getComboData(): ComboData[]; /** * 获取单个组合数据 * * Get single combo data * @param id - 组合ID | combo ID * @returns 组合数据 | combo data * @example * ```ts * const combo1 = graph.getComboData('combo-1'); * ``` * @apiCategory data * @remarks * 组合 id 必须存在,否则会抛出异常 * * Combo id must exist, otherwise an exception will be thrown */ public getComboData(id: ID): ComboData; /** * 批量获取多个组合数据 * * Get multiple combo data in batch * @param ids - 组合ID 数组 | combo ID array * @returns 组合数据 | combo data * @example * ```ts * const [combo1, combo2] = graph.getComboData(['combo-1', 'combo-2']); * ``` * @apiCategory data * @remarks * 数组中的每个组合 id 必须存在,否则将抛出异常 * * Each combo id in the array must exist, otherwise an exception will be thrown */ public getComboData(ids: ID[]): ComboData[]; public getComboData(id?: ID | ID[]): ComboData | ComboData[] { if (id === undefined) return this.context.model.getComboData(); if (Array.isArray(id)) return this.context.model.getComboData(id); return this.context.model.getNodeLikeDatum(id); } /** * 设置全量数据 * * Set full data * @param data - 数据 | data * @remarks * 设置全量数据会替换当前图中的所有数据,G6 会自动进行数据差异计算 * * Setting full data will replace all data in the current graph, and G6 will automatically calculate the data difference * @apiCategory data */ public setData(data: GraphData | ((prev: GraphData) => GraphData)): void { this.context.model.setData(isFunction(data) ? data(this.getData()) : data); } /** * 新增元素数据 * * Add element data * @param data - 元素数据 | element data * @example * ```ts * graph.addData({ * nodes: [{ id: 'node-1' }, { id: 'node-2' }], * edges: [{ source: 'node-1', target: 'node-2' }], * }); * ``` * @apiCategory data */ public addData(data: GraphData | ((prev: GraphData) => GraphData)): void { this.context.model.addData(isFunction(data) ? data(this.getData()) : data); } /** * 新增节点数据 * * Add node data * @param data - 节点数据 | node data * @example * ```ts * graph.addNodeData([{ id: 'node-1' }, { id: 'node-2' }]); * ``` * @apiCategory data */ public addNodeData(data: NodeData[] | ((prev: NodeData[]) => NodeData[])): void { this.context.model.addNodeData(isFunction(data) ? data(this.getNodeData()) : data); } /** * 新增边数据 * * Add edge data * @param data - 边数据 | edge data * @example * ```ts * graph.addEdgeData([{ source: 'node-1', target: 'node-2' }]); * ``` * @apiCategory data */ public addEdgeData(data: EdgeData[] | ((prev: EdgeData[]) => EdgeData[])): void { this.context.model.addEdgeData(isFunction(data) ? data(this.getEdgeData()) : data); } /** * 新增组合数据 * * Add combo data * @param data - 组合数据 | combo data * @example * ```ts * graph.addComboData([{ id: 'combo-1' }]); * ``` * @apiCategory data */ public addComboData(data: ComboData[] | ((prev: ComboData[]) => ComboData[])): void { this.context.model.addComboData(isFunction(data) ? data(this.getComboData()) : data); } /** * 为树图节点添加子节点数据 * * Add child node data to the tree node * @param parentId - 父节点 ID | parent node ID * @param childrenData - 子节点数据 | child node data * @remarks * 为组合添加子节点使用 addNodeData / addComboData 方法 * * Use addNodeData / addComboData method to add child nodes to the combo * @apiCategory data */ public addChildrenData(parentId: ID, childrenData: NodeData[]) { this.context.model.addChildrenData(parentId, childrenData); } /** * 更新元素数据 * * Update element data * @param data - 元素数据 | element data * @remarks * 只需要传入需要更新的数据即可,不必传入完整的数据 * * Just pass in the data that needs to be updated, no need to pass in the complete data * @example * ```ts * graph.updateData({ * nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }], * edges: [{ id: 'edge-1', style: { lineWidth: 2 } }] * }); * ``` * @apiCategory data */ public updateData(data: PartialGraphData | ((prev: GraphData) => PartialGraphData)): void { this.context.model.updateData(isFunction(data) ? data(this.getData()) : data); } /** * 更新节点数据 * * Update node data * @param data - 节点数据 | node data * @remarks * 只需要传入需要更新的数据即可,不必传入完整的数据 * * Just pass in the data that needs to be updated, no need to pass in the complete data * @example * ```ts * graph.updateNodeData([{ id: 'node-1', style: { x: 100, y: 100 } }]); * ``` * @apiCategory data */ public updateNodeData( data: PartialNodeLikeData[] | ((prev: NodeData[]) => PartialNodeLikeData[]), ): void { this.context.model.updateNodeData(isFunction(data) ? data(this.getNodeData()) : data); } /** * 更新边数据 * * Update edge data * @param data - 边数据 | edge data * @remarks * 只需要传入需要更新的数据即可,不必传入完整的数据 * * Just pass in the data that needs to be updated, no need to pass in the complete data * @example * ```ts * graph.updateEdgeData([{ id: 'edge-1', style: { lineWidth: 2 } }]); * ``` * @apiCategory data */ public updateEdgeData(data: PartialEdgeData[] | ((prev: EdgeData[]) => PartialEdgeData[])): void { this.context.model.updateEdgeData(isFunction(data) ? data(this.getEdgeData()) : data); } /** * 更新组合数据 * * Update combo data * @param data - 组合数据 | combo data * @remarks * 只需要传入需要更新的数据即可,不必传入完整的数据 * * Just pass in the data that needs to be updated, no need to pass in the complete data * @example * ```ts * graph.updateComboData([{ id: 'combo-1', style: { x: 100, y: 100 } }]); * ``` * @apiCategory data */ public updateComboData( data: PartialNodeLikeData[] | ((prev: ComboData[]) => PartialNodeLikeData[]), ): void { this.context.model.updateComboData(isFunction(data) ? data(this.getComboData()) : data); } /** * 删除元素数据 * * Remove element data * @param ids - 元素 ID 数组 | element ID array * @example * ```ts * graph.removeData({ * nodes: ['node-1', 'node-2'], * edges: ['edge-1'], * }); * ``` * @apiCategory data */ public removeData(ids: DataID | ((data: GraphData) => DataID)): void { this.context.model.removeData(isFunction(ids) ? ids(this.getData()) : ids); } /** * 删除节点数据 * * Remove node data * @param ids - 节点 ID 数组 | node ID array * @example * ```ts * graph.removeNodeData(['node-1', 'node-2']); * ``` * @apiCategory data */ public removeNodeData(ids: ID[] | ((data: NodeData[]) => ID[])): void { this.context.model.removeNodeData(isFunction(ids) ? ids(this.getNodeData()) : ids); } /** * 删除边数据 * * Remove edge data * @param ids - 边 ID 数组 | edge ID array * @remarks * 如果传入边数据时仅提供了 source 和 target,那么需要通过 `idOf` 方法获取边的实际 ID * * If only the source and target are provided when passing in the edge data, you need to get the actual ID of the edge through the `idOf` method * @example * ```ts * graph.removeEdgeData(['edge-1']); * ``` * @apiCategory data */ public removeEdgeData(ids: ID[] | ((data: EdgeData[]) => ID[])): void { this.context.model.removeEdgeData(isFunction(ids) ? ids(this.getEdgeData()) : ids); } /** * 删除组合数据 * * Remove combo data * @param ids - 组合 ID 数组 | 组合 ID array * @example * ```ts * graph.removeComboData(['combo-1']); * ``` * @apiCategory data */ public removeComboData(ids: ID[] | ((data: ComboData[]) => ID[])): void { this.context.model.removeComboData(isFunction(ids) ? ids(this.getComboData()) : ids); } /** * 获取元素类型 * * Get element type * @param id - 元素 ID | element ID * @returns 元素类型 | element type * @apiCategory element */ public getElementType(id: ID): ElementType { return this.context.model.getElementType(id); } /** * 获取节点或组合关联边的数据 * * Get edge data related to the node or combo * @param id - 节点或组合ID | node or combo ID * @param direction - 边的方向 | edge direction * @returns 边数据 | edge data * @apiCategory data */ public getRelatedEdgesData(id: ID, direction: EdgeDirection = 'both'): EdgeData[] { return this.context.model.getRelatedEdgesData(id, direction); } /** * 获取节点或组合的一跳邻居节点数据 * * Get the one-hop neighbor node data of the node or combo * @param id - 节点或组合ID | node or combo ID * @returns 邻居节点数据 | neighbor node data * @apiCategory data */ public getNeighborNodesData(id: ID): NodeData[] { return this.context.model.getNeighborNodesData(id); } /** * 获取节点或组合的祖先元素数据 * * Get the ancestor element data of the node or combo * @param id - 节点或组合ID | node or combo ID * @param hierarchy - 指定树图层级关系还是组合层级关系 | specify tree or combo hierarchy relationship * @returns 祖先元素数据 | ancestor element data * @remarks * 数组中的顺序是从父节点到祖先节点 * * The order in the array is from the parent node to the ancestor node * @apiCategory data */ public getAncestorsData(id: ID, hierarchy: HierarchyKey): NodeLikeData[] { return this.context.model.getAncestorsData(id, hierarchy); } /** * 获取节点或组合的父元素数据 * * Get the parent element data of the node or combo * @param id - 节点或组合ID | node or combo ID * @param hierarchy - 指定树图层级关系还是组合层级关系 | specify tree or combo hierarchy relationship * @returns 父元素数据 | parent element data * @apiCategory data */ public getParentData(id: ID, hierarchy: HierarchyKey): NodeLikeData | undefined { return this.context.model.getParentData(id, hierarchy); } /** * 获取节点或组合的子元素数据 * * Get the child element data of the node or combo * @param id - 节点或组合ID | node or combo ID * @returns 子元素数据 | child element data * @apiCategory data */ public getChildrenData(id: ID): NodeLikeData[] { return this.context.model.getChildrenData(id); } /** * 获取节点或组合的后代元素数据 * * Get the descendant element data of the node or combo * @param id - 节点或组合ID | node or combo ID * @returns 后代元素数据 | descendant element data * @apiCategory data */ public getDescendantsData(id: ID): NodeLikeData[] { return this.context.model.getDescendantsData(id); } /** * 获取指定状态下的节点数据 * * Get node data in a specific state * @param state - 状态 | state * @returns 节点数据 | node data * @example * ```ts * const nodes = graph.getElementDataByState('node', 'selected'); * ``` * @apiCategory data */ public getElementDataByState(elementType: 'node', state: State): NodeData[]; /** * 获取指定状态下的边数据 * * Get edge data in a specific state * @param state - 状态 | state * @returns 边数据 | edge data * @example * ```ts * const nodes = graph.getElementDataByState('edge', 'selected'); * ``` * @apiCategory data */ public getElementDataByState(elementType: 'edge', state: State): EdgeData[]; /** * 获取指定状态下的组合数据 * * Get combo data in a specific state * @param state - 状态 | state * @returns 组合数据 | combo data * @example * ```ts * const nodes = graph.getElementDataByState('node', 'selected'); * ``` * @apiCategory data */ public getElementDataByState(elementType: 'combo', state: State): ComboData[]; public getElementDataByState(elementType: ElementType, state: State): ElementDatum[] { return this.context.model.getElementDataByState(elementType, state); } private async initCanvas() { if (this.context.canvas) return await this.context.canvas.ready; const { container = 'container', width, height, renderer, cursor, background, canvas: canvasOptions, devicePixelRatio = globalThis.devicePixelRatio ?? 1, } = this.options; if (container instanceof Canvas) { this.context.canvas = container; if (cursor) container.setCursor(cursor); if (renderer) container.setRenderer(renderer); await container.ready; } else { const $container = isString(container) ? document.getElementById(container!) : container; const containerSize = sizeOf($container!); this.emit(GraphEvent.BEFORE_CANVAS_INIT, { container: $container, width, height }); const options = { ...canvasOptions, container: $container!, width: width || containerSize[0], height: height || containerSize[1], background, renderer, cursor, devicePixelRatio, }; const canvas = new Canvas(options); this.context.canvas = canvas; await canvas.ready; this.emit(GraphEvent.AFTER_CANVAS_INIT, { canvas }); } } private updateCanvas(options: GraphOptions) { const { renderer, cursor, height, width } = options; const canvas = this.context.canvas; if (!canvas) return; if (renderer) { this.emit(GraphEvent.BEFORE_RENDERER_CHANGE, { renderer: this.options.renderer }); canvas.setRenderer(renderer); this.emit(GraphEvent.AFTER_RENDERER_CHANGE, { renderer }); } if (cursor) canvas.setCursor(cursor); if (isNumber(width) || isNumber(height)) this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0); } private initRuntime() { this.context.options = this.options; if (!this.context.batch) this.context.batch = new BatchController(this.context); if (!this.context.plugin) this.context.plugin = new PluginController(this.context); if (!this.context.viewport) this.context.viewport = new ViewportController(this.context); if (!this.context.transform) this.context.transform = new TransformController(this.context); if (!this.context.element) this.context.element = new ElementController(this.context); if (!this.context.animation) this.context.animation = new Animation(this.context); if (!this.context.layout) this.context.layout = new LayoutController(this.context); if (!this.context.behavior) this.context.behavior = new BehaviorController(this.context); } private async prepare(): Promise { // 等待同步任务执行完成,避免 render 后立即调用 destroy 导致的问题 // Wait for synchronous tasks to complete, to avoid problems caused by calling destroy immediately after render await Promise.resolve(); if (this.destroyed) { // 如果图实例已经被销毁,则不再执行任何操作 // If the graph instance has been destroyed, no further operations will be performed // eslint-disable-next-line no-console console.error(format('The graph instance has been destroyed')); return; } await this.initCanvas(); this.initRuntime(); } /** * 执行渲染 * * Render * @remarks * 此过程会执行数据更新、绘制元素、执行布局 * * > ⚠️ render 为异步方法,如果需要在 render 后执行一些操作,可以使用 `await graph.render()` 或者监听 GraphEvent.AFTER_RENDER 事件 * * This process will execute data update, element rendering, and layout execution * * > ⚠️ render is an asynchronous method. If you need to perform some operations after render, you can use `await graph.render()` or listen to the GraphEvent.AFTER_RENDER event * @apiCategory render */ public async render(): Promise { await this.prepare(); emit(this, new GraphLifeCycleEvent(GraphEvent.BEFORE_RENDER)); if (!this.options.layout) { const animation = this.context.element!.draw({ type: 'render' }); await Promise.all([animation?.finished, this.autoFit()]); } else if (!this.rendered && isPreLayout(this.options.layout)) { const animation = await this.context.element!.preLayoutDraw({ type: 'render' }); await Promise.all([animation?.finished, this.autoFit()]); } else { const animation = this.context.element!.draw({ type: 'render' }); await Promise.all([animation?.finished, this.context.layout!.postLayout()]); await this.autoFit(); } this.rendered = true; emit(this, new GraphLifeCycleEvent(GraphEvent.AFTER_RENDER)); } /** * 绘制元素 * * Draw elements * @returns 渲染结果 | draw result * @remarks * 仅执行元素绘制,不会重新布局 * * ⚠️ draw 为异步方法,如果需要在 draw 后执行一些操作,可以使用 `await graph.draw()` 或者监听 GraphEvent.AFTER_DRAW 事件 * * Only execute element drawing, no re-layout * * ⚠️ draw is an asynchronous method. If you need to perform some operations after draw, you can use `await graph.draw()` or listen to the GraphEvent.AFTER_DRAW event * @apiCategory render */ public async draw(): Promise { await this.prepare(); await this.context.element!.draw()?.finished; } /** * 执行布局 * * Execute layout * @param layoutOptions - 布局配置项 | Layout options * @apiCategory layout */ public async layout(layoutOptions?: LayoutOptions) { await this.context.layout!.postLayout(layoutOptions); } /** * 停止布局 * * Stop layout * @remarks * 适用于带有迭代动画的布局,目前有 `force` 属于此类布局,即停止力导布局的迭代,一般用于布局迭代时间过长情况下的手动停止迭代动画,例如在点击画布/节点的监听中调用 * * Suitable for layouts with iterative animations. Currently, `force` belongs to this type of layout, that is, stop the iteration of the force-directed layout. It is generally used to manually stop the iteration animation when the layout iteration time is too long, such as calling in the click canvas/node listener * @apiCategory layout */ public stopLayout() { this.context.layout!.stopLayout(); } /** * 清空画布元素 * * Clear canvas elements * @apiCategory canvas */ public async clear(): Promise { const { model, element } = this.context; model.setData({}); model.clearChanges(); element?.clear(); } /** * 销毁当前图实例 * * Destroy the current graph instance * @remarks * 销毁后无法进行任何操作,如果需要重新使用,需要重新创建一个新的图实例 * * After destruction, no operations can be performed. If you need to reuse it, you need to create a new graph instance * @apiCategory instance */ public destroy(): void { emit(this, new GraphLifeCycleEvent(GraphEvent.BEFORE_DESTROY)); const { layout, animation, element, model, canvas, behavior, plugin } = this.context; plugin?.destroy(); behavior?.destroy(); layout?.destroy(); animation?.destroy(); element?.destroy(); model.destroy(); canvas?.destroy(); this.options = {}; // @ts-expect-error force delete this.context = {}; this.off(); globalThis.removeEventListener?.('resize', this.onResize); this.destroyed = true; emit(this, new GraphLifeCycleEvent(GraphEvent.AFTER_DESTROY)); } /** * 获取画布实例 * * Get canvas instance * @returns - 画布实例 | canvas instance * @apiCategory canvas */ public getCanvas(): Canvas { return this.context.canvas; } /** * 调整画布大小为图容器大小 * * Resize the canvas to the size of the graph container * @apiCategory viewport */ public resize(): void; /** * 调整画布大小为指定宽高 * * Resize the canvas to the specified width and height * @param width - 宽度 | width * @param height - 高度 | height * @apiCategory viewport */ public resize(width: number, height: number): void; public resize(width?: number, height?: number): void { const containerSize = sizeOf(this.context.canvas?.getContainer()); const specificSize: Vector2 = [width || containerSize[0], height || containerSize[1]]; if (!this.context.canvas) return; const canvasSize = this.context.canvas!.getSize(); if (isEqual(specificSize, canvasSize)) return; emit(this, new GraphLifeCycleEvent(GraphEvent.BEFORE_SIZE_CHANGE, { size: specificSize })); this.context.canvas.resize(...specificSize); emit(this, new GraphLifeCycleEvent(GraphEvent.AFTER_SIZE_CHANGE, { size: specificSize })); } /** * 将图缩放至合适大小并平移至视口中心 * * Zoom the graph to fit the viewport and move it to the center of the viewport * @param options - 适配配置 | fit options * @param animation - 动画配置 | animation options * @apiCategory viewport */ public async fitView(options?: FitViewOptions, animation?: ViewportAnimationEffectTiming): Promise { await this.context.viewport?.fitView(options, animation); } /** * 将图平移至视口中心 * * Move the graph to the center of the viewport * @param animation - 动画配置 | animation options * @apiCategory viewport */ public async fitCenter(animation?: ViewportAnimationEffectTiming): Promise { await this.context.viewport?.fitCenter({ animation }); } private async autoFit(): Promise { const { autoFit } = this.context.options; if (!autoFit) return; if (isString(autoFit)) { if (autoFit === 'view') await this.fitView(); else if (autoFit === 'center') await this.fitCenter(); } else { const { type, animation } = autoFit; if (type === 'view') await this.fitView(autoFit.options, animation); else if (type === 'center') await this.fitCenter(animation); } } /** * 聚焦元素 * * Focus on element * @param id - 元素 ID | element ID * @param animation - 动画配置 | animation options * @remarks * 移动图,使得元素对齐到视口中心 * * Move the graph so that the element is aligned to the center of the viewport * @apiCategory viewport */ public async focusElement(id: ID | ID[], animation?: ViewportAnimationEffectTiming): Promise { await this.context.viewport?.focusElements(Array.isArray(id) ? id : [id], { animation }); } /** * 基于当前缩放比例进行缩放(相对缩放) * * Zoom based on the current zoom ratio (relative zoom) * @param ratio - 缩放比例 | zoom ratio * @param animation - 动画配置 | animation options * @param origin - 缩放中心(视口坐标) | zoom center(viewport coordinates) * @remarks * * - ratio > 1 放大 * - ratio < 1 缩小 * * * - ratio > 1 zoom in * - ratio < 1 zoom out * @apiCategory viewport */ public async zoomBy(ratio: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise { await this.context.viewport!.transform({ mode: 'relative', scale: ratio, origin }, animation); } /** * 缩放画布至指定比例(绝对缩放) * * Zoom the canvas to the specified ratio (absolute zoom) * @param zoom - 指定缩放比例 | specified zoom ratio * @param animation - 动画配置 | animation options * @param origin - 缩放中心(视口坐标) | zoom center(viewport coordinates) * @remarks * * - zoom = 1 默认大小 * - zoom > 1 放大 * - zoom < 1 缩小 * * * - zoom = 1 default size * - zoom > 1 zoom in * - zoom < 1 zoom out * @apiCategory viewport */ public async zoomTo(zoom: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise { await this.context.viewport!.transform({ mode: 'absolute', scale: zoom, origin }, animation); } /** * 获取当前缩放比例 * * Get the current zoom ratio * @returns 缩放比例 | zoom ratio * @apiCategory viewport */ public getZoom(): number { return this.context.viewport!.getZoom(); } /** * 基于当前旋转角度进行旋转(相对旋转) * * Rotate based on the current rotation angle (relative rotation) * @param angle - 旋转角度 | rotation angle * @param animation - 动画配置 | animation options * @param origin - 旋转中心(视口坐标) | rotation center(viewport coordinates) * @apiCategory viewport */ public async rotateBy(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise { await this.context.viewport!.transform({ mode: 'relative', rotate: angle, origin }, animation); } /** * 旋转画布至指定角度 (绝对旋转) * * Rotate the canvas to the specified angle (absolute rotation) * @param angle - 目标角度 | target angle * @param animation - 动画配置 | animation options * @param origin - 旋转中心(视口坐标) | rotation center(viewport coordinates) * @apiCategory viewport */ public async rotateTo(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise { await this.context.viewport!.transform({ mode: 'absolute', rotate: angle, origin }, animation); } /** * 获取当前旋转角度 * * Get the current rotation angle * @returns 旋转角度 | rotation angle * @apiCategory viewport */ public getRotation(): number { return this.context.viewport!.getRotation(); } /** * 将图平移指定距离 (相对平移) * * Translate the graph by the specified distance (relative translation) * @param offset - 偏移量 | offset * @param animation - 动画配置 | animation options * @apiCategory viewport */ public async translateBy(offset: Point, animation?: ViewportAnimationEffectTiming): Promise { await this.context.viewport!.transform({ mode: 'relative', translate: offset }, animation); } /** * 将图平移至指定位置 (绝对平移) * * Translate the graph to the specified position (absolute translation) * @param position - 指定位置 | specified position * @param animation - 动画配置 | animation options * @apiCategory viewport */ public async translateTo(position: Point, animation?: ViewportAnimationEffectTiming): Promise { await this.context.viewport!.transform({ mode: 'absolute', translate: position }, animation); } /** * 获取图的位置 * * Get the position of the graph * @returns 图的位置 | position of the graph * @remarks * 即画布原点在视口坐标系下的位置。默认状态下,图的位置为 [0, 0] * * That is, the position of the canvas origin in the viewport coordinate system. By default, the position of the graph is [0, 0] * @apiCategory viewport */ public getPosition(): Point { return subtract([0, 0], this.getCanvasByViewport([0, 0])); } /** * 将元素平移指定距离 (相对平移) * * Translate the element by the specified distance (relative translation) * @param id - 元素 ID | element ID * @param offset - 偏移量 | offset * @param animation - 是否启用动画 | whether to enable animation * @apiCategory element */ public async translateElementBy(id: ID, offset: Point, animation?: boolean): Promise; /** * 批量将元素平移指定距离 (相对平移) * * Batch translate elements by the specified distance (relative translation) * @param offsets - 偏移量配置 | offset options * @param animation - 是否启用动画 | whether to enable animation * @apiCategory element */ public async translateElementBy(offsets: Record, animation?: boolean): Promise; public async translateElementBy( args1: ID | Record, args2?: Point | boolean, args3: boolean = true, ): Promise { const [config, animation] = isObject(args1) ? [args1, (args2 as boolean) ?? true] : [{ [args1 as ID]: args2 as Point }, args3]; Object.entries(config).forEach(([id, offset]) => this.context.model.translateNodeLikeBy(id, offset)); await this.context.element!.draw({ animation, stage: 'translate' })?.finished; } /** * 将元素平移至指定位置 (绝对平移) * * Translate the element to the specified position (absolute translation) * @param id - 元素 ID | element ID * @param position - 指定位置 | specified position * @param animation - 是否启用动画 | whether to enable animation * @apiCategory element */ public async translateElementTo(id: ID, position: Point, animation?: boolean): Promise; /** * 批量将元素平移至指定位置 (绝对平移) * * Batch translate elements to the specified position (absolute translation) * @param positions - 位置配置 | position options * @param animation - 是否启用动画 | whether to enable animation * @apiCategory element */ public async translateElementTo(positions: Record, animation?: boolean): Promise; public async translateElementTo( args1: ID | Record, args2?: boolean | Point, args3: boolean = true, ): Promise { const [config, animation] = isObject(args1) ? [args1, (args2 as boolean) ?? true] : [{ [args1 as ID]: args2 as Point }, args3]; Object.entries(config).forEach(([id, position]) => this.context.model.translateNodeLikeTo(id, position)); await this.context.element!.draw({ animation, stage: 'translate' })?.finished; } /** * 获取元素位置 * * Get element position * @param id - 元素 ID | element ID * @returns 元素位置 | element position * @apiCategory element */ public getElementPosition(id: ID): Point { return this.context.model.getElementPosition(id); } /** * 获取元素渲染样式 * * Get element rendering style * @param id - 元素 ID | element ID * @returns 元素渲染样式 | element rendering style * @apiCategory element */ public getElementRenderStyle(id: ID): Record { return omit(this.context.element!.getElement(id)!.attributes, ['context']); } /** * 设置元素可见性 * * Set element visibility * @param id - 元素 ID | element ID * @param visibility - 可见性 | visibility * @param animation - 动画配置 | animation options * @remarks * 可见性配置包括 `visible` 和 `hidden` 两种状态 * * Visibility configuration includes two states: `visible` and `hidden` * @apiCategory element */ public async setElementVisibility( id: ID, visibility: BaseStyleProps['visibility'], animation?: boolean, ): Promise; /** * 批量设置元素可见性 * * Batch set element visibility * @param visibility - 可见性配置 | visibility options * @param animation - 动画配置 | animation options * @apiCategory element */ public async setElementVisibility( visibility: Record, animation?: boolean, ): Promise; public async setElementVisibility( args1: ID | Record, args2?: boolean | BaseStyleProps['visibility'], args3: boolean = true, ): Promise { const [config, animation] = isObject(args1) ? [args1, (args2 as boolean) ?? true] : [{ [args1]: args2 as BaseStyleProps['visibility'] }, args3]; const dataToUpdate: Required = { nodes: [], edges: [], combos: [] }; Object.entries(config).forEach(([id, value]) => { const elementType = this.getElementType(id); dataToUpdate[`${elementType}s`].push({ id, style: { visibility: value } }); }); const { model, element } = this.context; model.preventUpdateNodeLikeHierarchy(() => { model.updateData(dataToUpdate); }); await element!.draw({ animation, stage: 'visibility' })?.finished; } /** * 显示元素 * * Show element * @param id - 元素 ID | element ID * @param animation - 是否启用动画 | whether to enable animation * @apiCategory element */ public async showElement(id: ID | ID[], animation?: boolean): Promise { const ids = Array.isArray(id) ? id : [id]; await this.setElementVisibility( Object.fromEntries(ids.map((_id) => [_id, 'visible'] as [ID, BaseStyleProps['visibility']])), animation, ); } /** * 隐藏元素 * * Hide element * @param id - 元素 ID | element ID * @param animation - 是否启用动画 | whether to enable animation * @apiCategory element */ public async hideElement(id: ID | ID[], animation?: boolean): Promise { const ids = Array.isArray(id) ? id : [id]; await this.setElementVisibility( Object.fromEntries(ids.map((_id) => [_id, 'hidden'] as [ID, BaseStyleProps['visibility']])), animation, ); } /** * 获取元素可见性 * * Get element visibility * @param id - 元素 ID | element ID * @returns 元素可见性 | element visibility * @apiCategory element */ public getElementVisibility(id: ID): BaseStyleProps['visibility'] { const element = this.context.element!.getElement(id)!; return element?.style?.visibility ?? 'visible'; } /** * 设置元素层级 * * Set element z-index * @param id - 元素 ID | element ID * @param zIndex - 层级 | z-index * @apiCategory element */ public async setElementZIndex(id: ID, zIndex: number): Promise; /** * 批量设置元素层级 * * Batch set element z-index * @param zIndex - 层级配置 | z-index options * @apiCategory element */ public async setElementZIndex(zIndex: Record): Promise; public async setElementZIndex(args1: ID | Record, args2?: number): Promise { const dataToUpdate: Required = { nodes: [], edges: [], combos: [] }; const config = isObject(args1) ? args1 : { [args1 as ID]: args2 as number }; Object.entries(config).forEach(([id, value]) => { const elementType = this.getElementType(id); dataToUpdate[`${elementType}s`].push({ id, style: { zIndex: value } }); }); const { model, element } = this.context; model.preventUpdateNodeLikeHierarchy(() => model.updateData(dataToUpdate)); await element!.draw({ animation: false, stage: 'zIndex' })?.finished; } /** * 将元素置于最顶层 * * Bring the element to the front * @param id - 元素 ID | element ID * @apiCategory element */ public async frontElement(id: ID | ID[]): Promise { const ids = Array.isArray(id) ? id : [id]; const { model } = this.context; const zIndexes: Record = {}; ids.map((_id) => { const zIndex = model.getFrontZIndex(_id); const elementType = model.getElementType(_id); if (elementType === 'combo') { const ancestor = model.getAncestorsData(_id, COMBO_KEY).at(-1) || this.getComboData(_id); const descendants = [ancestor, ...model.getDescendantsData(idOf(ancestor))]; const delta = zIndex - getZIndexOf(ancestor); descendants.forEach((combo) => { zIndexes[idOf(combo)] = this.getElementZIndex(idOf(combo)) + delta; }); const { internal } = getSubgraphRelatedEdges(descendants.map(idOf), (id) => model.getRelatedEdgesData(id)); internal.forEach((edge) => { const edgeId = idOf(edge); zIndexes[edgeId] = this.getElementZIndex(edgeId) + delta; }); } else zIndexes[_id] = zIndex; }); await this.setElementZIndex(zIndexes); } /** * 获取元素层级 * * Get element z-index * @param id - 元素 ID | element ID * @returns 元素层级 | element z-index * @apiCategory element */ public getElementZIndex(id: ID): number { return getZIndexOf(this.context.model.getElementDataById(id)); } /** * 设置元素状态 * * Set element state * @param id - 元素 ID | element ID * @param state - 状态 | state * @param animation - 动画配置 | animation options * @apiCategory element */ public async setElementState(id: ID, state: State | State[], animation?: boolean): Promise; /** * 批量设置元素状态 * * Batch set element state * @param state - 状态配置 | state options * @param animation - 动画配置 | animation options * @apiCategory element */ public async setElementState(state: Record, animation?: boolean): Promise; public async setElementState( args1: ID | Record, args2?: boolean | State | State[], args3: boolean = true, ): Promise { const [config, animation] = isObject(args1) ? [args1, (args2 as boolean) ?? true] : [{ [args1]: args2 as State | State[] }, args3]; const parseState = (state: State | State[]) => { if (!state) return []; return Array.isArray(state) ? state : [state]; }; const dataToUpdate: Required = { nodes: [], edges: [], combos: [] }; Object.entries(config).forEach(([id, value]) => { const elementType = this.getElementType(id); dataToUpdate[`${elementType}s`].push({ id, states: parseState(value) }); }); this.updateData(dataToUpdate); await this.context.element!.draw({ animation, stage: 'state' })?.finished; } /** * 获取元素状态 * * Get element state * @param id - 元素 ID | element ID * @returns 元素状态 | element state * @apiCategory element */ public getElementState(id: ID): State[] { return this.context.model.getElementState(id); } /** * 获取元素自身以及子节点在世界坐标系下的渲染包围盒 * * Get the rendering bounding box of the element itself and its child nodes in the world coordinate system * @param id - 元素 ID | element ID * @returns 渲染包围盒 | render bounding box * @apiCategory element */ public getElementRenderBounds(id: ID): AABB { return this.context.element!.getElement(id)!.getRenderBounds(); } private isCollapsingExpanding = false; /** * 收起元素 * * Collapse element * @param id - 元素 ID | element ID * @param options - 是否启用动画或者配置收起节点的配置项 | whether to enable animation or the options of collapsing node * @apiCategory element */ public async collapseElement(id: ID, options: boolean | CollapseExpandNodeOptions = true): Promise { const { model, element } = this.context; if (isCollapsed(model.getNodeLikeData([id])[0])) return; if (this.isCollapsingExpanding) return; if (typeof options === 'boolean') options = { animation: options, align: true }; const elementType = model.getElementType(id); await this.frontElement(id); this.isCollapsingExpanding = true; // 更新折叠状态 / Update collapse style model.updateData( elementType === 'node' ? { nodes: [{ id, style: { collapsed: true } }], } : { combos: [{ id, style: { collapsed: true } }], }, ); if (elementType === 'node') await element!.collapseNode(id, options); else if (elementType === 'combo') await element!.collapseCombo(id, !!options.animation); this.isCollapsingExpanding = false; } /** * 展开元素 * * Expand Element * @param id - 元素 ID | element ID * @param animation - 是否启用动画或者配置收起节点的配置项 | whether to enable animation or the options of collapsing node * @param options * @apiCategory element */ public async expandElement(id: ID, options: boolean | CollapseExpandNodeOptions = true): Promise { const { model, element } = this.context; if (!isCollapsed(model.getNodeLikeData([id])[0])) return; if (this.isCollapsingExpanding) return; if (typeof options === 'boolean') options = { animation: options, align: true }; const elementType = model.getElementType(id); this.isCollapsingExpanding = true; // 更新折叠状态 / Update collapse style model.updateData( elementType === 'node' ? { nodes: [{ id, style: { collapsed: false } }], } : { combos: [{ id, style: { collapsed: false } }], }, ); if (elementType === 'node') await element!.expandNode(id, options); else if (elementType === 'combo') await element!.expandCombo(id, !!options.animation); this.isCollapsingExpanding = false; } private setElementCollapsibility(id: ID, collapsed: boolean) { const elementType = this.getElementType(id); if (elementType === 'node') this.updateNodeData([{ id, style: { collapsed } }]); else if (elementType === 'combo') this.updateComboData([{ id, style: { collapsed } }]); } /** * 导出画布内容为 DataURL * * Export canvas content as DataURL * @param options - 导出配置 | export options * @returns DataURL | DataURL * @apiCategory exportImage */ public async toDataURL(options: Partial = {}): Promise { return this.context.canvas!.toDataURL(options); } /** * 给定的视窗 DOM 坐标,转换为画布上的绘制坐标 * * Convert the given viewport DOM coordinates to the drawing coordinates on the canvas * @param point - 视窗坐标 | viewport coordinates * @returns 画布上的绘制坐标 | drawing coordinates on the canvas * @apiCategory viewport */ public getCanvasByViewport(point: Point): Point { return this.context.canvas.getCanvasByViewport(point); } /** * 给定画布上的绘制坐标,转换为视窗 DOM 的坐标 * * Convert the given drawing coordinates on the canvas to the coordinates of the viewport DOM * @param point - 画布坐标 | canvas coordinates * @returns 视窗 DOM 的坐标 | coordinates of the viewport DOM * @apiCategory viewport */ public getViewportByCanvas(point: Point): Point { return this.context.canvas.getViewportByCanvas(point); } /** * 给定画布上的绘制坐标,转换为浏览器坐标 * * Convert the given drawing coordinates on the canvas to browser coordinates * @param point - 画布坐标 | canvas coordinates * @returns 浏览器坐标 | browser coordinates * @apiCategory viewport */ public getClientByCanvas(point: Point): Point { return this.context.canvas.getClientByCanvas(point); } /** * 给定的浏览器坐标,转换为画布上的绘制坐标 * * Convert the given browser coordinates to drawing coordinates on the canvas * @param point - 浏览器坐标 | browser coordinates * @returns 画布上的绘制坐标 | drawing coordinates on the canvas * @apiCategory viewport */ public getCanvasByClient(point: Point): Point { return this.context.canvas.getCanvasByClient(point); } /** * 获取视口中心的画布坐标 * * Get the canvas coordinates of the viewport center * @returns 视口中心的画布坐标 | Canvas coordinates of the viewport center * @apiCategory viewport */ public getViewportCenter(): Point { return this.context.viewport!.getViewportCenter(); } /** * 获取视口中心的视口坐标 * * Get the viewport coordinates of the viewport center * @returns 视口中心的视口坐标 | Viewport coordinates of the viewport center * @apiCategory viewport */ public getCanvasCenter(): Point { return this.context.viewport!.getCanvasCenter(); } private onResize = debounce(() => { this.resize(); }, 300); /** * 监听事件 * * Listen to events * @param eventName - 事件名称 | event name * @param callback - 回调函数 | callback function * @param once - 是否只监听一次 | whether to listen only once * @returns Graph 实例 | Graph instance * @apiCategory event */ public on(eventName: string, callback: (event: T) => void, once?: boolean): this { return super.on(eventName, callback, once); } /** * 一次性监听事件 * * Listen to events once * @param eventName - 事件名称 | event name * @param callback - 回调函数 | callback function * @returns Graph 实例 | Graph instance * @apiCategory event */ public once(eventName: string, callback: (event: T) => void): this { return super.once(eventName, callback); } /** * 移除全部事件监听 * * Remove all event listeners * @returns Graph 实例 | Graph instance * @apiCategory event */ public off(): this; /** * 移除指定事件的全部监听 * * Remove all listeners for the specified event * @param eventName - 事件名称 | event name * @returns Graph 实例 | Graph instance * @apiCategory event */ public off(eventName: string): this; /** * 移除事件监听 * * Remove event listener * @param eventName - 事件名称 | event name * @param callback - 回调函数 | callback function * @returns Graph 实例 | Graph instance * @apiCategory event */ public off(eventName: string, callback: (...args: any[]) => void): this; public off(eventName?: string, callback?: (...args: any[]) => void) { return super.off(eventName, callback); } }