mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 18:28:19 +08:00
支持更多场景下的布局过渡动画 (#4345)
* feat: support transition on layouts without iterations #4339 * feat: use immediately invoked layout as a shortcut #4339 * feat: handle scenario when layout is unset in spec * fix: disable css parsing for better performance
This commit is contained in:
parent
e2158e8592
commit
8aa3618ea2
@ -1,8 +1,13 @@
|
||||
import { isLayoutWithIterations, Layout, LayoutMapping, Supervisor } from '@antv/layout';
|
||||
import { Animation, DisplayObject, IAnimationEffectTiming } from '@antv/g';
|
||||
import { isLayoutWithIterations, Layout, LayoutMapping, OutNode, Supervisor } from '@antv/layout';
|
||||
import { stdLib } from '../../stdlib';
|
||||
import { IGraph } from '../../types';
|
||||
import {
|
||||
IGraph,
|
||||
isImmediatelyInvokedLayoutOptions,
|
||||
isLayoutWorkerized,
|
||||
LayoutOptions,
|
||||
} from '../../types';
|
||||
import { GraphCore } from '../../types/data';
|
||||
import { LayoutOptions } from '../../types/layout';
|
||||
|
||||
/**
|
||||
* Manages layout extensions and graph layout.
|
||||
@ -12,11 +17,14 @@ export class LayoutController {
|
||||
public extensions = {};
|
||||
public graph: IGraph;
|
||||
|
||||
private currentLayout: Layout<any>;
|
||||
private currentSupervisor: Supervisor;
|
||||
private currentLayout: Layout<any> | null;
|
||||
private currentSupervisor: Supervisor | null;
|
||||
private currentAnimation: Animation | null;
|
||||
private animatedDisplayObject: DisplayObject;
|
||||
|
||||
constructor(graph: IGraph<any>) {
|
||||
this.graph = graph;
|
||||
this.animatedDisplayObject = new DisplayObject({});
|
||||
this.tap();
|
||||
}
|
||||
|
||||
@ -27,67 +35,98 @@ export class LayoutController {
|
||||
this.graph.hooks.layout.tap(this.onLayout.bind(this));
|
||||
}
|
||||
|
||||
private async onLayout(params: { graphCore: GraphCore; options?: LayoutOptions }) {
|
||||
private async onLayout(params: { graphCore: GraphCore; options: LayoutOptions }) {
|
||||
/**
|
||||
* The final calculated result.
|
||||
*/
|
||||
let positions: LayoutMapping;
|
||||
|
||||
// Stop currentLayout if any.
|
||||
this.stopLayout();
|
||||
|
||||
const { graphCore, options } = params;
|
||||
|
||||
const {
|
||||
type,
|
||||
workerEnabled,
|
||||
animated,
|
||||
iterations = 300,
|
||||
...rest
|
||||
} = {
|
||||
...this.graph.getSpecification().layout,
|
||||
...options,
|
||||
};
|
||||
if (isImmediatelyInvokedLayoutOptions(options)) {
|
||||
const {
|
||||
animated = false,
|
||||
animationEffectTiming = {
|
||||
duration: 1000,
|
||||
} as Partial<IAnimationEffectTiming>,
|
||||
execute,
|
||||
...rest
|
||||
} = options;
|
||||
|
||||
// Find built-in layout algorithms.
|
||||
const layoutCtor = stdLib.layouts[type];
|
||||
if (!layoutCtor) {
|
||||
throw new Error(`Unknown layout algorithm: ${type}`);
|
||||
}
|
||||
// It will ignore some layout options such as `type` and `workerEnabled`.
|
||||
positions = await execute(graphCore, rest);
|
||||
|
||||
// Initialize layout.
|
||||
const layout = new layoutCtor(rest);
|
||||
this.currentLayout = layout;
|
||||
|
||||
let positions: LayoutMapping;
|
||||
|
||||
if (workerEnabled) {
|
||||
/**
|
||||
* Run algorithm in WebWorker, `animated` option will be ignored.
|
||||
*/
|
||||
const supervisor = new Supervisor(graphCore, layout, { iterations });
|
||||
this.currentSupervisor = supervisor;
|
||||
positions = await supervisor.execute();
|
||||
if (animated) {
|
||||
await this.animateLayoutWithoutIterations(positions, animationEffectTiming);
|
||||
}
|
||||
} else {
|
||||
if (isLayoutWithIterations(layout)) {
|
||||
if (animated) {
|
||||
positions = await layout.execute(graphCore, {
|
||||
onTick: (positionsOnTick: LayoutMapping) => {
|
||||
// Display the animated process of layout.
|
||||
this.updateNodesPosition(positionsOnTick);
|
||||
this.graph.emit('tick', positionsOnTick);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* Manually step simulation in a sync way. `onTick` won't get triggered in this case,
|
||||
* there will be no animation either.
|
||||
*/
|
||||
layout.execute(graphCore);
|
||||
layout.stop();
|
||||
positions = layout.tick(iterations);
|
||||
}
|
||||
const {
|
||||
type = 'grid',
|
||||
animated = false,
|
||||
animationEffectTiming = {
|
||||
duration: 1000,
|
||||
} as Partial<IAnimationEffectTiming>,
|
||||
iterations = 300,
|
||||
...rest
|
||||
} = options;
|
||||
let { workerEnabled = false } = options;
|
||||
|
||||
// Find built-in layout algorithms.
|
||||
const layoutCtor = stdLib.layouts[type];
|
||||
if (!layoutCtor) {
|
||||
throw new Error(`Unknown layout algorithm: ${type}`);
|
||||
}
|
||||
|
||||
// Initialize layout.
|
||||
const layout = new layoutCtor(rest);
|
||||
this.currentLayout = layout;
|
||||
|
||||
// CustomLayout is not workerized.
|
||||
if (!isLayoutWorkerized(options)) {
|
||||
workerEnabled = false;
|
||||
// TODO: console.warn();
|
||||
}
|
||||
|
||||
if (workerEnabled) {
|
||||
/**
|
||||
* `onTick` will get triggered in this case.
|
||||
* Run algorithm in WebWorker, `animated` option will be ignored.
|
||||
*/
|
||||
const supervisor = new Supervisor(graphCore, layout, { iterations });
|
||||
this.currentSupervisor = supervisor;
|
||||
positions = await supervisor.execute();
|
||||
} else {
|
||||
positions = await layout.execute(graphCore);
|
||||
// e.g. Force layout
|
||||
if (isLayoutWithIterations(layout)) {
|
||||
if (animated) {
|
||||
/**
|
||||
* `onTick` will get triggered in this case.
|
||||
*/
|
||||
positions = await layout.execute(graphCore, {
|
||||
onTick: (positionsOnTick: LayoutMapping) => {
|
||||
// Display the animated process of layout.
|
||||
this.updateNodesPosition(positionsOnTick);
|
||||
this.graph.emit('tick', positionsOnTick);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
/**
|
||||
* Manually step simulation in a sync way. `onTick` won't get triggered in this case,
|
||||
* there will be no animation either.
|
||||
*/
|
||||
layout.execute(graphCore);
|
||||
layout.stop();
|
||||
positions = layout.tick(iterations);
|
||||
}
|
||||
} else {
|
||||
positions = await layout.execute(graphCore);
|
||||
|
||||
if (animated) {
|
||||
await this.animateLayoutWithoutIterations(positions, animationEffectTiming);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +144,11 @@ export class LayoutController {
|
||||
this.currentSupervisor.stop();
|
||||
this.currentSupervisor = null;
|
||||
}
|
||||
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.finish();
|
||||
this.currentAnimation = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -126,4 +170,68 @@ export class LayoutController {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For those layout without iterations, e.g. circular, random, since they don't have `onTick` callback,
|
||||
* we have to translate each node from its initial position to final one after layout,
|
||||
* with the help of `onframe` from G's animation API.
|
||||
* @param positions
|
||||
* @param animationEffectTiming
|
||||
*/
|
||||
private async animateLayoutWithoutIterations(
|
||||
positions: LayoutMapping,
|
||||
animationEffectTiming: Partial<IAnimationEffectTiming>,
|
||||
) {
|
||||
// Animation should be executed only once.
|
||||
animationEffectTiming.iterations = 1;
|
||||
|
||||
const initialPositions = positions.nodes.map((node) => {
|
||||
// Should clone each node's initial data since it will be updated during the layout process.
|
||||
return { ...this.graph.getNodeData(`${node.id}`)! };
|
||||
});
|
||||
|
||||
// Add a connected displayobject so that we can animate it.
|
||||
if (!this.animatedDisplayObject.isConnected) {
|
||||
await this.graph.canvas.ready;
|
||||
this.graph.canvas.appendChild(this.animatedDisplayObject);
|
||||
}
|
||||
|
||||
// Use `opacity` since it is an interpolated property.
|
||||
this.currentAnimation = this.animatedDisplayObject.animate(
|
||||
[
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
],
|
||||
animationEffectTiming,
|
||||
) as Animation;
|
||||
|
||||
// Update each node's position on each frame.
|
||||
// @see https://g.antv.antgroup.com/api/animation/waapi#%E5%B1%9E%E6%80%A7
|
||||
this.currentAnimation.onframe = (e) => {
|
||||
// @see https://g.antv.antgroup.com/api/animation/waapi#progress
|
||||
const progress = (e.target as Animation).effect.getComputedTiming().progress as number;
|
||||
const interpolatedNodesPosition = (initialPositions as OutNode[]).map(({ id, data }, i) => {
|
||||
const { x: fromX = 0, y: fromY = 0 } = data;
|
||||
const { x: toX, y: toY } = positions.nodes[i].data;
|
||||
return {
|
||||
id,
|
||||
data: {
|
||||
x: fromX + (toX - fromX) * progress,
|
||||
y: fromY + (toY - fromY) * progress,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.updateNodesPosition({
|
||||
nodes: interpolatedNodesPosition,
|
||||
edges: [],
|
||||
});
|
||||
};
|
||||
|
||||
await this.currentAnimation.finished;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import EventEmitter from '@antv/event-emitter';
|
||||
import { Canvas } from '@antv/g';
|
||||
import { Canvas, runtime } from '@antv/g';
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { isArray, isNumber, isObject, isString } from '@antv/util';
|
||||
import { isArray, isNil, isNumber, isObject, isString } from '@antv/util';
|
||||
import {
|
||||
ComboUserModel,
|
||||
EdgeUserModel,
|
||||
@ -18,20 +18,29 @@ import { GraphCore } from '../types/data';
|
||||
import { EdgeModel, EdgeModelData } from '../types/edge';
|
||||
import { Hooks } from '../types/hook';
|
||||
import { ITEM_TYPE } from '../types/item';
|
||||
import { LayoutOptions } from '../types/layout';
|
||||
import {
|
||||
ImmediatelyInvokedLayoutOptions,
|
||||
LayoutOptions,
|
||||
StandardLayoutOptions,
|
||||
} from '../types/layout';
|
||||
import { NodeModel, NodeModelData } from '../types/node';
|
||||
import { FitViewRules, GraphAlignment } from '../types/view';
|
||||
import { createCanvas } from '../util/canvas';
|
||||
import {
|
||||
DataController,
|
||||
ExtensionController,
|
||||
InteractionController,
|
||||
ItemController,
|
||||
LayoutController,
|
||||
ThemeController,
|
||||
ExtensionController,
|
||||
} from './controller';
|
||||
import Hook from './hooks';
|
||||
|
||||
/**
|
||||
* Disable CSS parsing for better performance.
|
||||
*/
|
||||
runtime.enableCSSParsing = false;
|
||||
|
||||
export default class Graph<B extends BehaviorRegistry> extends EventEmitter implements IGraph<B> {
|
||||
public hooks: Hooks;
|
||||
// for nodes and edges, which will be separate into groups
|
||||
@ -92,7 +101,9 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
}
|
||||
this.backgroundCanvas = createCanvas(rendererType, container, width, height, pixelRatio);
|
||||
this.canvas = createCanvas(rendererType, container, width, height, pixelRatio);
|
||||
this.transientCanvas = createCanvas(rendererType, container, width, height, pixelRatio, true, { pointerEvents: 'none' });
|
||||
this.transientCanvas = createCanvas(rendererType, container, width, height, pixelRatio, true, {
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
Promise.all(
|
||||
[this.backgroundCanvas, this.canvas, this.transientCanvas].map((canvas) => canvas.ready),
|
||||
).then(() => (this.canvasReady = true));
|
||||
@ -118,7 +129,9 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
modes: string[];
|
||||
behaviors: BehaviorOptionsOf<{}>[];
|
||||
}>({ name: 'behaviorchange' }),
|
||||
itemstatechange: new Hook<{ ids: ID[], state: string, value: boolean }>({ name: 'itemstatechange' })
|
||||
itemstatechange: new Hook<{ ids: ID[]; state: string; value: boolean }>({
|
||||
name: 'itemstatechange',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -152,12 +165,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
});
|
||||
this.emit('afterrender');
|
||||
|
||||
// TODO: make read async?
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore: this.dataController.graphCore,
|
||||
});
|
||||
|
||||
this.emit('afterlayout');
|
||||
await this.layout();
|
||||
};
|
||||
if (this.canvasReady) {
|
||||
await emitRender();
|
||||
@ -183,11 +191,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
});
|
||||
this.emit('afterrender');
|
||||
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore: this.dataController.graphCore,
|
||||
});
|
||||
|
||||
this.emit('afterlayout');
|
||||
await this.layout();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -351,7 +355,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
let ids = this.itemController.findIdByState(itemType, state, value);
|
||||
if (additionalFilter) {
|
||||
const getDataFunc = itemType === 'node' ? this.getNodeData : this.getEdgeData; // TODO: combo
|
||||
ids = ids.filter(id => additionalFilter(getDataFunc(id)));
|
||||
ids = ids.filter((id) => additionalFilter(getDataFunc(id)));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
@ -498,7 +502,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
this.hooks.itemstatechange.emit({
|
||||
ids: idArr as ID[],
|
||||
states: stateArr as string[],
|
||||
value
|
||||
value,
|
||||
});
|
||||
}
|
||||
/**
|
||||
@ -524,7 +528,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
this.hooks.itemstatechange.emit({
|
||||
ids: idArr as ID[],
|
||||
states,
|
||||
value: false
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -568,9 +572,39 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
* Layout the graph (with current configurations if cfg is not assigned).
|
||||
*/
|
||||
public async layout(options?: LayoutOptions) {
|
||||
const { graphCore } = this.dataController;
|
||||
const formattedOptions = {
|
||||
...this.getSpecification().layout,
|
||||
...options,
|
||||
} as LayoutOptions;
|
||||
|
||||
const layoutUnset = !options && !this.getSpecification().layout;
|
||||
if (layoutUnset) {
|
||||
const nodes = graphCore.getAllNodes();
|
||||
if (nodes.every((node) => isNil(node.data.x) && isNil(node.data.y))) {
|
||||
// Use `grid` layout as default when x/y of each node is unset.
|
||||
(formattedOptions as StandardLayoutOptions).type = 'grid';
|
||||
} else {
|
||||
// Use user-defined position(x/y default to 0).
|
||||
(formattedOptions as ImmediatelyInvokedLayoutOptions).execute = async (graph) => {
|
||||
const nodes = graph.getAllNodes();
|
||||
return {
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
data: {
|
||||
x: Number(node.data.x) || 0,
|
||||
y: Number(node.data.y) || 0,
|
||||
},
|
||||
})),
|
||||
edges: [],
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore: this.dataController.graphCore,
|
||||
options,
|
||||
graphCore,
|
||||
options: formattedOptions,
|
||||
});
|
||||
this.emit('afterlayout');
|
||||
}
|
||||
|
@ -3,4 +3,11 @@ export { GraphData } from './data';
|
||||
export { NodeUserModel, NodeModel, NodeDisplayModel } from './node';
|
||||
export { EdgeUserModel, EdgeModel, EdgeDisplayModel } from './edge';
|
||||
export { ComboUserModel, ComboModel, ComboDisplayModel } from './combo';
|
||||
export { Specification } from './spec';
|
||||
export { Specification } from './spec';
|
||||
export {
|
||||
StandardLayoutOptions,
|
||||
ImmediatelyInvokedLayoutOptions,
|
||||
LayoutOptions,
|
||||
isImmediatelyInvokedLayoutOptions,
|
||||
isLayoutWorkerized,
|
||||
} from './layout';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { IAnimationEffectTiming } from '@antv/g';
|
||||
import {
|
||||
CircularLayoutOptions,
|
||||
ConcentricLayoutOptions,
|
||||
@ -6,28 +7,94 @@ import {
|
||||
ForceLayoutOptions,
|
||||
FruchtermanLayoutOptions,
|
||||
GridLayoutOptions,
|
||||
LayoutMapping,
|
||||
MDSLayoutOptions,
|
||||
RadialLayoutOptions,
|
||||
RandomLayoutOptions,
|
||||
} from '@antv/layout';
|
||||
import { GraphCore } from './data';
|
||||
|
||||
export type LayoutOptions = (
|
||||
| CircularLayout
|
||||
| RandomLayout
|
||||
| ConcentricLayout
|
||||
| GridLayout
|
||||
| MDSLayout
|
||||
| RadialLayout
|
||||
| FruchtermanLayout
|
||||
| D3ForceLayout
|
||||
| ForceLayout
|
||||
| ForceAtlas2
|
||||
) & {
|
||||
workerEnabled?: boolean;
|
||||
type Animatable = {
|
||||
/**
|
||||
* Make layout animated. For layouts with iterations, transitions will happen between ticks.
|
||||
*/
|
||||
animated?: boolean;
|
||||
|
||||
/**
|
||||
* Effect timing of animation for layouts without iterations.
|
||||
* @see https://g.antv.antgroup.com/api/animation/waapi#effecttiming
|
||||
*/
|
||||
animationEffectTiming?: Partial<IAnimationEffectTiming>;
|
||||
};
|
||||
|
||||
type Workerized = {
|
||||
/**
|
||||
* Make layout running in WebWorker.
|
||||
*/
|
||||
workerEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Iterations for iteratable layouts such as Force.
|
||||
*/
|
||||
iterations?: number;
|
||||
};
|
||||
|
||||
export type ImmediatelyInvokedLayoutOptions = {
|
||||
/**
|
||||
* like an IIFE.
|
||||
*/
|
||||
execute: (graph: GraphCore, options?: any) => Promise<LayoutMapping>;
|
||||
} & Animatable;
|
||||
|
||||
type CustomLayout = {
|
||||
type: string;
|
||||
[option: string]: any;
|
||||
};
|
||||
|
||||
export type StandardLayoutOptions =
|
||||
| (
|
||||
| CircularLayout
|
||||
| RandomLayout
|
||||
| ConcentricLayout
|
||||
| GridLayout
|
||||
| MDSLayout
|
||||
| RadialLayout
|
||||
| FruchtermanLayout
|
||||
| D3ForceLayout
|
||||
| ForceLayout
|
||||
| ForceAtlas2
|
||||
| CustomLayout
|
||||
) &
|
||||
Animatable &
|
||||
Workerized;
|
||||
|
||||
export type LayoutOptions = StandardLayoutOptions | ImmediatelyInvokedLayoutOptions;
|
||||
|
||||
export function isImmediatelyInvokedLayoutOptions(
|
||||
options: any,
|
||||
): options is ImmediatelyInvokedLayoutOptions {
|
||||
return !!options.execute;
|
||||
}
|
||||
|
||||
export function isLayoutWorkerized(options: StandardLayoutOptions) {
|
||||
return (
|
||||
[
|
||||
'circular',
|
||||
'random',
|
||||
'grid',
|
||||
'mds',
|
||||
'concentric',
|
||||
'radial',
|
||||
'fruchterman',
|
||||
'fruchtermanGPU',
|
||||
'd3force',
|
||||
'force',
|
||||
'gforce',
|
||||
'forceAtlas2',
|
||||
].indexOf(options.type) > -1
|
||||
);
|
||||
}
|
||||
|
||||
interface CircularLayout extends CircularLayoutOptions {
|
||||
type: 'circular';
|
||||
}
|
||||
|
@ -1,11 +1,123 @@
|
||||
import { Graph, Layout, LayoutMapping } from '@antv/layout';
|
||||
import G6, { IGraph, stdLib } from '../../src/index';
|
||||
import { Layout, LayoutMapping } from '@antv/layout';
|
||||
import G6, { stdLib } from '../../src/index';
|
||||
import { data } from '../datasets/dataset1';
|
||||
const container = document.createElement('div');
|
||||
document.querySelector('body').appendChild(container);
|
||||
document.querySelector('body')!.appendChild(container);
|
||||
|
||||
describe('layout', () => {
|
||||
let graph: IGraph<any>;
|
||||
let graph: any;
|
||||
|
||||
it('should use grid as default when layout is unset in spec.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(125);
|
||||
expect(nodesData[0].data.y).toBe(75);
|
||||
|
||||
expect(nodesData[1].data.x).toBe(225);
|
||||
expect(nodesData[1].data.y).toBe(125);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use user-defined x/y as node's position when layout is unset in spec.", (done) => {
|
||||
setTimeout(() => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'a',
|
||||
data: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
data: {
|
||||
x: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(100);
|
||||
expect(nodesData[0].data.y).toBe(100);
|
||||
expect(nodesData[1].data.x).toBe(100);
|
||||
expect(nodesData[1].data.y).toBe(0);
|
||||
expect(nodesData[2].data.x).toBe(0);
|
||||
expect(nodesData[2].data.y).toBe(0);
|
||||
|
||||
// re-layout
|
||||
await graph.layout({
|
||||
execute: async () => {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
id: 'a',
|
||||
data: {
|
||||
x: 200,
|
||||
y: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
data: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
data: {
|
||||
x: 250,
|
||||
y: 250,
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
};
|
||||
},
|
||||
animated: true,
|
||||
animationEffectTiming: {
|
||||
duration: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(nodesData[0].data.x).toBe(200);
|
||||
expect(nodesData[0].data.y).toBe(200);
|
||||
expect(nodesData[1].data.x).toBe(100);
|
||||
expect(nodesData[1].data.y).toBe(100);
|
||||
expect(nodesData[2].data.x).toBe(250);
|
||||
expect(nodesData[2].data.y).toBe(250);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('should apply circular layout correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
@ -145,7 +257,7 @@ describe('layout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the layout process with `animated`.', (done) => {
|
||||
it('should display the process in layout with iterations when `animated` enabled.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
@ -169,6 +281,106 @@ describe('layout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the process in layout without iterations when `animated` enabled.', (done) => {
|
||||
setTimeout(() => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
animated: true,
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
await graph.layout({
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 100,
|
||||
animated: true,
|
||||
animationEffectTiming: {
|
||||
duration: 1000,
|
||||
easing: 'in-out-bounce',
|
||||
},
|
||||
});
|
||||
|
||||
await graph.layout({
|
||||
type: 'random',
|
||||
animated: true,
|
||||
animationEffectTiming: {
|
||||
duration: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
await graph.layout({
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
animated: false,
|
||||
});
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('should execute an immediately invoked layout with animation.', (done) => {
|
||||
setTimeout(() => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
animated: true,
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
await graph.layout({
|
||||
execute: async (graph) => {
|
||||
const nodes = graph.getAllNodes();
|
||||
return {
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
data: {
|
||||
x: 250,
|
||||
y: 250,
|
||||
},
|
||||
})),
|
||||
edges: [],
|
||||
};
|
||||
},
|
||||
animated: true,
|
||||
animationEffectTiming: {
|
||||
duration: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it('should stop animated layout process with `stopLayout`.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
@ -225,10 +437,10 @@ describe('layout', () => {
|
||||
it('should allow registering custom layout at runtime.', (done) => {
|
||||
// Put all nodes at `[0, 0]`.
|
||||
class MyCustomLayout implements Layout<{}> {
|
||||
async assign(graph: Graph, options?: {}): Promise<void> {
|
||||
async assign(graph, options?: {}): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async execute(graph: Graph, options?: {}): Promise<LayoutMapping> {
|
||||
async execute(graph, options?: {}): Promise<LayoutMapping> {
|
||||
const nodes = graph.getAllNodes();
|
||||
return {
|
||||
nodes: nodes.map((node) => ({
|
||||
@ -255,7 +467,6 @@ describe('layout', () => {
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
// @ts-ignore
|
||||
type: 'myCustomLayout',
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user