@antv/g6
Version:
A Graph Visualization Framework in JavaScript
393 lines • 18.5 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LayoutController = void 0;
const graphlib_1 = require("@antv/graphlib");
const layout_1 = require("@antv/layout");
const util_1 = require("@antv/util");
const constants_1 = require("../constants");
const layouts_1 = require("../layouts");
const get_1 = require("../registry/get");
const animation_1 = require("../utils/animation");
const collapsibility_1 = require("../utils/collapsibility");
const element_1 = require("../utils/element");
const event_1 = require("../utils/event");
const graphlib_2 = require("../utils/graphlib");
const id_1 = require("../utils/id");
const layout_2 = require("../utils/layout");
const print_1 = require("../utils/print");
const traverse_1 = require("../utils/traverse");
class LayoutController {
get presetOptions() {
return {
animation: !!(0, animation_1.getAnimationOptions)(this.context.options, true),
};
}
get options() {
const { options } = this.context;
return options.layout;
}
constructor(context) {
this.instances = [];
this.context = context;
}
getLayoutInstance() {
return this.instances;
}
/**
* <zh/> 前布局,即在绘制前执行布局
*
* <en/> Pre-layout, that is, perform layout before drawing
* @param data - <zh/> 绘制数据 | <en/> Draw data
* @remarks
* <zh/> 前布局应该只在首次绘制前执行,后续更新不会触发
*
* <en/> Pre-layout should only be executed before the first drawing, and subsequent updates will not trigger
*/
preLayout(data) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
const { graph, model } = this.context;
const { add } = data;
(0, event_1.emit)(graph, new event_1.GraphLifeCycleEvent(constants_1.GraphEvent.BEFORE_LAYOUT, { type: 'pre' }));
const simulate = yield ((_a = this.context.layout) === null || _a === void 0 ? void 0 : _a.simulate());
(_b = simulate === null || simulate === void 0 ? void 0 : simulate.nodes) === null || _b === void 0 ? void 0 : _b.forEach((l) => {
const id = (0, id_1.idOf)(l);
const node = add.nodes.get(id);
model.syncNodeLikeDatum(l);
if (node)
Object.assign(node.style, l.style);
});
(_c = simulate === null || simulate === void 0 ? void 0 : simulate.edges) === null || _c === void 0 ? void 0 : _c.forEach((l) => {
const id = (0, id_1.idOf)(l);
const edge = add.edges.get(id);
model.syncEdgeDatum(l);
if (edge)
Object.assign(edge.style, l.style);
});
(_d = simulate === null || simulate === void 0 ? void 0 : simulate.combos) === null || _d === void 0 ? void 0 : _d.forEach((l) => {
const id = (0, id_1.idOf)(l);
const combo = add.combos.get(id);
model.syncNodeLikeDatum(l);
if (combo)
Object.assign(combo.style, l.style);
});
(0, event_1.emit)(graph, new event_1.GraphLifeCycleEvent(constants_1.GraphEvent.AFTER_LAYOUT, { type: 'pre' }));
this.transformDataAfterLayout('pre', data);
});
}
/**
* <zh/> 后布局,即在完成绘制后执行布局
*
* <en/> Post layout, that is, perform layout after drawing
* @param layoutOptions - <zh/> 布局配置项 | <en/> Layout options
*/
postLayout() {
return __awaiter(this, arguments, void 0, function* (layoutOptions = this.options) {
if (!layoutOptions)
return;
const pipeline = Array.isArray(layoutOptions) ? layoutOptions : [layoutOptions];
const { graph } = this.context;
(0, event_1.emit)(graph, new event_1.GraphLifeCycleEvent(constants_1.GraphEvent.BEFORE_LAYOUT, { type: 'post' }));
for (let index = 0; index < pipeline.length; index++) {
const options = pipeline[index];
const data = this.getLayoutData(options);
const opts = Object.assign(Object.assign({}, this.presetOptions), options);
(0, event_1.emit)(graph, new event_1.GraphLifeCycleEvent(constants_1.GraphEvent.BEFORE_STAGE_LAYOUT, { options: opts, index }));
const result = yield this.stepLayout(data, opts, index);
(0, event_1.emit)(graph, new event_1.GraphLifeCycleEvent(constants_1.GraphEvent.AFTER_STAGE_LAYOUT, { options: opts, index }));
if (!options.animation) {
this.updateElementPosition(result, false);
}
}
(0, event_1.emit)(graph, new event_1.GraphLifeCycleEvent(constants_1.GraphEvent.AFTER_LAYOUT, { type: 'post' }));
this.transformDataAfterLayout('post');
});
}
transformDataAfterLayout(type, data) {
const transforms = this.context.transform.getTransformInstance();
// @ts-expect-error skip type check
Object.values(transforms).forEach((transform) => transform.afterLayout(type, data));
}
/**
* <zh/> 模拟布局
*
* <en/> Simulate layout
* @returns <zh/> 模拟布局结果 | <en/> Simulated layout result
*/
simulate() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.options)
return {};
const pipeline = Array.isArray(this.options) ? this.options : [this.options];
let simulation = {};
for (let index = 0; index < pipeline.length; index++) {
const options = pipeline[index];
const data = this.getLayoutData(options);
const result = yield this.stepLayout(data, Object.assign(Object.assign(Object.assign({}, this.presetOptions), options), { animation: false }), index);
simulation = result;
}
return simulation;
});
}
stepLayout(data, options, index) {
return __awaiter(this, void 0, void 0, function* () {
if ((0, layout_2.isTreeLayout)(options))
return yield this.treeLayout(data, options, index);
return yield this.graphLayout(data, options, index);
});
}
graphLayout(data, options, index) {
return __awaiter(this, void 0, void 0, function* () {
const { animation, enableWorker, iterations = 300 } = options;
const layout = this.initGraphLayout(options);
if (!layout)
return {};
this.instances[index] = layout;
this.instance = layout;
// 使用 web worker 执行布局 / Use web worker to execute layout
if (enableWorker) {
const rawLayout = layout;
this.supervisor = new layout_1.Supervisor(rawLayout.graphData2LayoutModel(data), rawLayout.instance, { iterations });
return (0, layout_2.layoutMapping2GraphData)(yield this.supervisor.execute());
}
if ((0, layout_1.isLayoutWithIterations)(layout)) {
// 有动画,基于布局迭代 tick 更新位置 / Update position based on layout iteration tick
if (animation) {
return yield layout.execute(data, {
onTick: (tickData) => {
this.updateElementPosition(tickData, false);
},
});
}
// 无动画,直接返回终态位置 / No animation, return final position directly
layout.execute(data);
layout.stop();
return layout.tick(iterations);
}
// 无迭代的布局,直接返回终态位置 / Layout without iteration, return final position directly
const layoutResult = yield layout.execute(data);
if (animation) {
const animationResult = this.updateElementPosition(layoutResult, animation);
yield (animationResult === null || animationResult === void 0 ? void 0 : animationResult.finished);
}
return layoutResult;
});
}
treeLayout(data, options, index) {
return __awaiter(this, void 0, void 0, function* () {
const { type, animation } = options;
// @ts-expect-error @antv/hierarchy 布局格式与 @antv/layout 不一致,其导出的是一个方法,而非 class
// The layout format of @antv/hierarchy is inconsistent with @antv/layout, it exports a method instead of a class
const layout = (0, get_1.getExtension)('layout', type);
if (!layout)
return {};
const { nodes = [], edges = [] } = data;
const model = new graphlib_1.Graph({
nodes: nodes.map((node) => ({ id: (0, id_1.idOf)(node), data: node.data || {} })),
edges: edges.map((edge) => ({ id: (0, id_1.idOf)(edge), source: edge.source, target: edge.target, data: edge.data || {} })),
});
(0, graphlib_2.createTreeStructure)(model);
const layoutPreset = { nodes: [], edges: [] };
const layoutResult = { nodes: [], edges: [] };
const roots = model.getRoots(constants_1.TREE_KEY);
roots.forEach((root) => {
(0, traverse_1.dfs)(root, (node) => {
node.children = model.getSuccessors(node.id);
}, (node) => model.getSuccessors(node.id), 'TB');
const result = layout(root, options);
const { x: rx, y: ry, z: rz = 0 } = result;
// 将布局结果转化为 LayoutMapping 格式 / Convert the layout result to LayoutMapping format
(0, traverse_1.dfs)(result, (node) => {
const { id, x, y, z = 0 } = node;
layoutPreset.nodes.push({ id, style: { x: rx, y: ry, z: rz } });
layoutResult.nodes.push({ id, style: { x, y, z } });
}, (node) => node.children, 'TB');
});
const offset = this.inferTreeLayoutOffset(layoutResult);
applyTreeLayoutOffset(layoutResult, offset);
if (animation) {
// 先将所有节点移动到根节点位置 / Move all nodes to the root node position first
applyTreeLayoutOffset(layoutPreset, offset);
this.updateElementPosition(layoutPreset, false);
const animationResult = this.updateElementPosition(layoutResult, animation);
yield (animationResult === null || animationResult === void 0 ? void 0 : animationResult.finished);
}
return layoutResult;
});
}
inferTreeLayoutOffset(data) {
var _a;
let [minX, maxX] = [Infinity, -Infinity];
let [minY, maxY] = [Infinity, -Infinity];
(_a = data.nodes) === null || _a === void 0 ? void 0 : _a.forEach((node) => {
const { x = 0, y = 0 } = node.style || {};
minX = Math.min(minX, x);
maxX = Math.max(maxX, x);
minY = Math.min(minY, y);
maxY = Math.max(maxY, y);
});
const { canvas } = this.context;
const canvasSize = canvas.getSize();
const [x1, y1] = canvas.getCanvasByViewport([0, 0]);
const [x2, y2] = canvas.getCanvasByViewport(canvasSize);
if (minX >= x1 && maxX <= x2 && minY >= y1 && maxY <= y2)
return [0, 0];
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
return [cx - (minX + maxX) / 2, cy - (minY + maxY) / 2];
}
stopLayout() {
if (this.instance && (0, layout_1.isLayoutWithIterations)(this.instance)) {
this.instance.stop();
this.instance = undefined;
}
if (this.supervisor) {
this.supervisor.stop();
this.supervisor = undefined;
}
if (this.animationResult) {
this.animationResult.finish();
this.animationResult = undefined;
}
}
getLayoutData(options) {
const { nodeFilter = () => true, preLayout = false, isLayoutInvisibleNodes = false } = options;
const { nodes, edges, combos } = this.context.model.getData();
const { element, model } = this.context;
const getElement = (id) => element.getElement(id);
const filterFn = preLayout
? (node) => {
var _a;
if (!isLayoutInvisibleNodes) {
if (((_a = node.style) === null || _a === void 0 ? void 0 : _a.visibility) === 'hidden')
return false;
if (model.getAncestorsData(node.id, constants_1.TREE_KEY).some(collapsibility_1.isCollapsed))
return false;
if (model.getAncestorsData(node.id, constants_1.COMBO_KEY).some(collapsibility_1.isCollapsed))
return false;
}
return nodeFilter(node);
}
: (node) => {
const id = (0, id_1.idOf)(node);
const element = getElement(id);
if (!element)
return false;
if ((0, element_1.isToBeDestroyed)(element))
return false;
return nodeFilter(node);
};
const nodesToLayout = nodes.filter(filterFn);
const nodeLikeIdsMap = new Map(nodesToLayout.map((node) => [(0, id_1.idOf)(node), node]));
combos.forEach((combo) => nodeLikeIdsMap.set((0, id_1.idOf)(combo), combo));
const edgesToLayout = edges.filter(({ source, target }) => {
return nodeLikeIdsMap.has(source) && nodeLikeIdsMap.has(target);
});
return {
nodes: nodesToLayout,
edges: edgesToLayout,
combos,
};
}
/**
* <zh/> 创建布局实例
*
* <en/> Create layout instance
* @param options - <zh/> 布局配置项 | <en/> Layout options
* @returns <zh/> 布局对象 | <en/> Layout object
*/
initGraphLayout(options) {
var _a;
const { element, viewport } = this.context;
const { type, enableWorker, animation, iterations } = options, restOptions = __rest(options, ["type", "enableWorker", "animation", "iterations"]);
const [width, height] = viewport.getCanvasSize();
const center = [width / 2, height / 2];
const nodeSize = (_a = options === null || options === void 0 ? void 0 : options.nodeSize) !== null && _a !== void 0 ? _a : ((node) => {
const nodeElement = element === null || element === void 0 ? void 0 : element.getElement(node.id);
if (nodeElement)
return nodeElement.attributes.size;
return element === null || element === void 0 ? void 0 : element.getElementComputedStyle('node', node).size;
});
const Ctor = (0, get_1.getExtension)('layout', type);
if (!Ctor)
return print_1.print.warn(`The layout of ${type} is not registered.`);
const STDCtor = Object.getPrototypeOf(Ctor.prototype) === layouts_1.BaseLayout.prototype
? Ctor
: (0, layout_2.layoutAdapter)(Ctor, this.context);
const layout = new STDCtor(this.context);
const config = { nodeSize, width, height, center };
switch (layout.id) {
case 'd3-force':
case 'd3-force-3d':
Object.assign(config, {
center: { x: width / 2, y: height / 2, z: 0 },
});
break;
default:
break;
}
(0, util_1.deepMix)(layout.options, config, restOptions);
return layout;
}
updateElementPosition(layoutResult, animation) {
const { model, element } = this.context;
if (!element)
return null;
model.updateData(layoutResult);
return element.draw({ animation, silence: true });
}
destroy() {
var _a;
this.stopLayout();
// @ts-expect-error force delete
this.context = {};
(_a = this.supervisor) === null || _a === void 0 ? void 0 : _a.kill();
this.supervisor = undefined;
this.instance = undefined;
this.instances = [];
this.animationResult = undefined;
}
}
exports.LayoutController = LayoutController;
/**
* <zh/> 对树形布局结果应用偏移
*
* <en/> Apply offset to tree layout result
* @param data - <zh/> 布局数据 | <en/> Layout data
* @param offset - <zh/> 偏移量 | <en/> Offset
*/
const applyTreeLayoutOffset = (data, offset) => {
var _a;
const [ox, oy] = offset;
(_a = data.nodes) === null || _a === void 0 ? void 0 : _a.forEach((node) => {
if (node.style) {
const { x = 0, y = 0 } = node.style;
node.style.x = x + ox;
node.style.y = y + oy;
}
else {
node.style = { x: ox, y: oy };
}
});
};
//# sourceMappingURL=layout.js.map