import { Graph as Graphlib } from '@antv/graphlib';
import { deepMix, isNumber } from '@antv/util';
import { COMBO_KEY } from '../constants';
import { BaseLayout } from '../layouts/base-layout';
import { idOf } from './id';
import { parsePoint } from './point';
import type { LayoutMapping, Graph as LayoutModel, Node as LayoutNodeData } from '@antv/layout';
import type { AntVLayout } from '../layouts/types';
import type { RuntimeContext } from '../runtime/types';
import type { GraphData } from '../spec/data';
import type { LayoutOptions, STDLayoutOptions } from '../spec/layout';
import type { AdaptiveLayout, ID } from '../types';
/**
* 判断是否是 combo 布局
*
* Determine if it is a combo layout
* @param options - 布局配置项 | Layout options
* @returns 是否是 combo 布局 | Whether it is a combo layout
*/
export function isComboLayout(options: STDLayoutOptions) {
const { type } = options;
if (['comboCombined', 'comboForce'].includes(type)) return true;
if (type === 'antv-dagre' && options.sortByCombo) return true;
return false;
}
/**
* 判断是否是树图布局
*
* Determine if it is a tree layout
* @param options - 布局配置项 | Layout options
* @returns 是否是树图布局 | Whether it is a tree layout
*/
export function isTreeLayout(options: STDLayoutOptions) {
const { type } = options;
return ['compact-box', 'mindmap', 'dendrogram', 'indented'].includes(type);
}
/**
* 数据中是否指定了位置
*
* Is the position specified in the data
* @param data - 数据 | Data
* @returns 是否指定了位置 | Is the position specified
*/
export function isPositionSpecified(data: Record) {
return isNumber(data.x) && isNumber(data.y);
}
/**
* 是否是前布局
*
* Is pre-layout
* @remarks
* 前布局是指在初始化元素前计算布局,适用于一些布局需要提前计算位置的场景。
*
* Pre-layout refers to calculating the layout before initializing the elements, which is suitable for some layouts that need to calculate the position in advance.
* @param options - 布局配置项 | Layout options
* @returns 是否是前布局 | Is it a pre-layout
*/
export function isPreLayout(options?: LayoutOptions) {
return !Array.isArray(options) && options?.preLayout;
}
/**
* 将图布局结果转换为 G6 数据
*
* Convert the layout result to G6 data
* @param layoutMapping - 布局映射 | Layout mapping
* @returns G6 数据 | G6 data
*/
export function layoutMapping2GraphData(layoutMapping: LayoutMapping): GraphData {
const { nodes, edges } = layoutMapping;
const data: GraphData = { nodes: [], edges: [], combos: [] };
nodes.forEach((nodeLike) => {
const target = nodeLike.data._isCombo ? data.combos : data.nodes;
const { x, y, z = 0 } = nodeLike.data;
target?.push({
id: nodeLike.id as ID,
style: { x, y, z },
});
});
edges.forEach((edge) => {
const {
id,
source,
target,
data: { points = [], controlPoints = points.slice(1, points.length - 1) },
} = edge;
data.edges!.push({
id: id as ID,
source: source as ID,
target: target as ID,
style: {
/**
* antv-dagre 返回 controlPoints,dagre 返回 points
* antv-dagre returns controlPoints, dagre returns points
*/
...(controlPoints?.length ? { controlPoints: controlPoints.map(parsePoint) } : {}),
},
});
});
return data;
}
/**
* 将 @antv/layout 布局适配为 G6 布局
*
* Adapt @antv/layout layout to G6 layout
* @param Ctor - 布局类 | Layout class
* @param context - 运行时上下文 | Runtime context
* @returns G6 布局类 | G6 layout class
*/
export function layoutAdapter(
Ctor: new (options: Record) => AntVLayout,
context: RuntimeContext,
): new (context: RuntimeContext, options?: Record) => BaseLayout {
class AdaptLayout extends BaseLayout implements AdaptiveLayout {
public instance: AntVLayout;
public id: string;
constructor(context: RuntimeContext, options?: Record) {
super(context, options);
this.instance = new Ctor({});
this.id = this.instance.id;
if ('stop' in this.instance && 'tick' in this.instance) {
const instance = this.instance;
this.stop = instance.stop.bind(instance);
this.tick = (iterations?: number) => {
const tickResult = instance.tick(iterations);
return layoutMapping2GraphData(tickResult);
};
}
}
public async execute(model: GraphData, options?: STDLayoutOptions): Promise {
return layoutMapping2GraphData(
await this.instance.execute(
this.graphData2LayoutModel(model),
this.transformOptions(deepMix({}, this.options, options)),
),
);
}
private transformOptions(options: STDLayoutOptions) {
if (!('onTick' in options)) return options;
const onTick = options.onTick as (data: GraphData) => void;
options.onTick = (data: LayoutMapping) => onTick(layoutMapping2GraphData(data));
return options;
}
public graphData2LayoutModel(data: GraphData): LayoutModel {
const { nodes = [], edges = [], combos = [] } = data;
const nodesToLayout: LayoutNodeData[] = nodes.map((datum) => {
const id = idOf(datum);
const { data, style, combo, ...rest } = datum;
const result = {
id,
data: {
// grid 布局会直接读取 data[sortBy],兼容处理,需要避免用户 data 下使用 data, style 等字段
// The grid layout will directly read data[sortBy], compatible processing, need to avoid users using data, style and other fields under data
...data,
data,
// antv-dagre 会读取 data.parentId
// antv-dagre will read data.parentId
...(combo ? { parentId: combo } : {}),
style,
...rest,
},
};
// 一些布局会从 data 中读取位置信息
if (style?.x) Object.assign(result.data, { x: style.x });
if (style?.y) Object.assign(result.data, { y: style.y });
if (style?.z) Object.assign(result.data, { z: style.z });
return result;
});
const nodesIdMap = new Map(nodesToLayout.map((node) => [node.id, node]));
const edgesToLayout = edges
.filter((edge) => {
const { source, target } = edge;
return nodesIdMap.has(source) && nodesIdMap.has(target);
})
.map((edge) => {
const { source, target, data, style } = edge;
return { id: idOf(edge), source, target, data: { ...data }, style: { ...style } };
});
const combosToLayout: LayoutNodeData[] = combos.map((combo) => {
return { id: idOf(combo), data: { _isCombo: true, ...combo.data }, style: { ...combo.style } };
});
const layoutModel = new Graphlib({
nodes: [...nodesToLayout, ...combosToLayout],
edges: edgesToLayout,
});
if (context.model.model.hasTreeStructure(COMBO_KEY)) {
layoutModel.attachTreeStructure(COMBO_KEY);
// 同步层级关系 / Synchronize hierarchical relationships
nodesToLayout.forEach((node) => {
const parent = context.model.model.getParent(node.id, COMBO_KEY);
if (parent && layoutModel.hasNode(parent.id)) {
layoutModel.setParent(node.id, parent.id, COMBO_KEY);
}
});
}
return layoutModel;
}
}
return AdaptLayout;
}
/**
* 调用布局成员方法
*
* Call layout member methods
* @remarks
* 提供一种通用的调用方式来调用 G6 布局和 \@antv/layout 布局上的方法
*
* Provide a common way to call methods on G6 layout and \@antv/layout layout
* @param layout - 布局实例 | Layout instance
* @param method - 方法名 | Method name
* @param args - 参数 | Arguments
* @returns 返回值 | Return value
*/
export function invokeLayoutMethod(layout: BaseLayout, method: string, ...args: unknown[]) {
if (method in layout) {
return (layout as any)[method](...args);
}
// invoke AdaptLayout method
if ('instance' in layout) {
const instance = (layout as any).instance;
if (method in instance) return instance[method](...args);
}
return null;
}
/**
* 获取布局成员属性
*
* Get layout member properties
* @param layout - 布局实例 | Layout instance
* @param name - 属性名 | Property name
* @returns 返回值 | Return value
*/
export function getLayoutProperty(layout: BaseLayout, name: string) {
if (name in layout) return (layout as any)[name];
if ('instance' in layout) {
const instance = (layout as any).instance;
if (name in instance) return instance[name];
}
return null;
}