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);
}
}