mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 18:28:19 +08:00
V5 layout (#4308)
* feat: unified datachange from changeData, addData, updateData, and removeData; feat: item definition and first drawing * feat: update canvas while addData; feat: update canvas while updateData (node); chore: unified additems, removeitems, updateitems to be itemchange hook * feat: init layout controller * feat: node and edge updating and drawing * chore: neaten * feat: draw and update labels for node and edge * feat: icon for edge; feat: custom node and edge and register to lib * feat: init layout controller * fix: use latest g-webgl * feat: layout controller should support & option * fix: registry typo & add custom layout test case * feat: state related API for graph and item * chore: update notes --------- Co-authored-by: Yanyan-Wang <yanyanwang93@gmail.com>
This commit is contained in:
parent
875a9eb39b
commit
0f23a181db
@ -47,6 +47,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/graphlib": "^2.0.0-alpha.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.7.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
@ -65,8 +66,7 @@
|
||||
"tslint-config-airbnb": "^5.11.2",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"typescript": "^4.6.3",
|
||||
"@antv/graphlib": "^2.0.0-alpha.0"
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.35",
|
||||
@ -88,4 +88,4 @@
|
||||
"normalize-url": "^4.1.0",
|
||||
"sharp": "^0.30.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,10 @@
|
||||
"@antv/g": "^5.15.7",
|
||||
"@antv/g-canvas": "^1.9.28",
|
||||
"@antv/g-svg": "^1.8.36",
|
||||
"@antv/graphlib": "^2.0.0-alpha.0",
|
||||
"@antv/g-webgl": "^1.7.44",
|
||||
"@antv/graphlib": "^2.0.0",
|
||||
"@antv/layout": "^1.0.0-alpha.17",
|
||||
"@antv/layout-gpu": "^1.0.0-alpha.3",
|
||||
"@antv/util": "~2.0.5",
|
||||
"typedoc-plugin-markdown": "^3.14.0"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Graph as GraphLib, ID } from '@antv/graphlib';
|
||||
import { GraphData, IGraph, ComboModel, ComboUserModel } from '../../types';
|
||||
import { registery } from '../../stdlib';
|
||||
import { registry } from '../../stdlib';
|
||||
import { getExtension } from '../../util/extension';
|
||||
import { clone, isArray, isNumber, isString, isFunction, isObject } from '@antv/util';
|
||||
import { NodeModel, NodeModelData, NodeUserModel, NodeUserModelData } from '../../types/node';
|
||||
@ -85,7 +85,7 @@ export class DataController {
|
||||
return transform
|
||||
.map((config) => ({
|
||||
config,
|
||||
func: getExtension(config, registery.useLib, 'transform'),
|
||||
func: getExtension(config, registry.useLib, 'transform'),
|
||||
}))
|
||||
.filter((ext) => !!ext.func);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IGraph } from "../../types";
|
||||
import { registery } from '../../stdlib';
|
||||
import { getExtension } from "../../util/extension";
|
||||
import { isObject } from "@antv/util";
|
||||
import { isObject } from '@antv/util';
|
||||
import { registry } from '../../stdlib';
|
||||
import { IGraph } from '../../types';
|
||||
import { getExtension } from '../../util/extension';
|
||||
|
||||
/**
|
||||
* Manages the interaction extensions and graph modes;
|
||||
@ -29,14 +29,16 @@ export class InteractionController {
|
||||
|
||||
/**
|
||||
* Get the extensions from useLib, stdLib is a sub set of useLib.
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
private getExtensions() {
|
||||
const { modes = {} } = this.graph.getSpecification();
|
||||
const modeBehaviors = {};
|
||||
Object.keys(modes).forEach(mode => {
|
||||
modeBehaviors[mode] = modes[mode].map(config => getExtension(config, registery.useLib, 'behavior')).filter(behavior => !!behavior);
|
||||
})
|
||||
Object.keys(modes).forEach((mode) => {
|
||||
modeBehaviors[mode] = modes[mode]
|
||||
.map((config) => getExtension(config, registry.useLib, 'behavior'))
|
||||
.filter((behavior) => !!behavior);
|
||||
});
|
||||
return modeBehaviors;
|
||||
}
|
||||
|
||||
@ -50,25 +52,35 @@ export class InteractionController {
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Listener of graph's behaviorchange hook. Update, add, or remove behaviors from modes.
|
||||
* @param param contains action, modes, and behaviors
|
||||
*/
|
||||
private onBehaviorChange(self, param: { action: 'update' | 'add' | 'remove', modes: string[], behaviors: (string | { key: string, type: string })[] }) {
|
||||
private onBehaviorChange(
|
||||
self,
|
||||
param: {
|
||||
action: 'update' | 'add' | 'remove';
|
||||
modes: string[];
|
||||
behaviors: (string | { key: string; type: string })[];
|
||||
},
|
||||
) {
|
||||
const { action, modes, behaviors } = param;
|
||||
modes.forEach(mode => {
|
||||
modes.forEach((mode) => {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
behaviors.forEach(config => self.extensions[mode].push(getExtension(config, registery.useLib, 'behavior')));
|
||||
behaviors.forEach((config) =>
|
||||
self.extensions[mode].push(getExtension(config, registry.useLib, 'behavior')),
|
||||
);
|
||||
break;
|
||||
case 'remove':
|
||||
behaviors.forEach(key => {
|
||||
self.extensions[mode] = self.extensions[mode].filter(behavior => behavior.getKey() === key)
|
||||
behaviors.forEach((key) => {
|
||||
self.extensions[mode] = self.extensions[mode].filter(
|
||||
(behavior) => behavior.getKey() === key,
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 'update':
|
||||
behaviors.forEach(config => {
|
||||
behaviors.forEach((config) => {
|
||||
if (isObject(config) && config.hasOwnProperty('key')) {
|
||||
const behaviorItem = self.extensions[mode].find(behavior => behavior.getKey() === config.key);
|
||||
if (behaviorItem) behaviorItem.updateConfig(config);
|
||||
@ -80,4 +92,4 @@ export class InteractionController {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { ComboModel, IGraph } from '../../types';
|
||||
import { registery } from '../../stdlib';
|
||||
import { registry } from '../../stdlib';
|
||||
import { getExtension } from '../../util/extension';
|
||||
import { GraphCore } from '../../types/data';
|
||||
import { NodeDisplayModel, NodeEncode, NodeModel, NodeModelData } from '../../types/node';
|
||||
@ -87,13 +87,13 @@ export class ItemController {
|
||||
const comboTypes = ['circle-combo', 'rect-combo']; // TODO: WIP
|
||||
return {
|
||||
node: nodeTypes
|
||||
.map((config) => getExtension(config, registery.useLib, 'node'))
|
||||
.map((config) => getExtension(config, registry.useLib, 'node'))
|
||||
.filter(Boolean),
|
||||
edge: edgeTypes
|
||||
.map((config) => getExtension(config, registery.useLib, 'edge'))
|
||||
.map((config) => getExtension(config, registry.useLib, 'edge'))
|
||||
.filter(Boolean),
|
||||
combo: comboTypes
|
||||
.map((config) => getExtension(config, registery.useLib, 'combo'))
|
||||
.map((config) => getExtension(config, registry.useLib, 'combo'))
|
||||
.filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,129 @@
|
||||
import { IGraph } from "../../types";
|
||||
import { isLayoutWithIterations, Layout, LayoutMapping, Supervisor } from '@antv/layout';
|
||||
import { stdLib } from '../../stdlib';
|
||||
import { IGraph } from '../../types';
|
||||
import { GraphCore } from '../../types/data';
|
||||
import { LayoutOptions } from '../../types/layout';
|
||||
|
||||
/**
|
||||
* Manages layout extensions and graph layout.
|
||||
* It will also emit `afterlayout` & `tick` events on Graph.
|
||||
*/
|
||||
export class LayoutController {
|
||||
public extensions = {};
|
||||
public graph: IGraph;
|
||||
|
||||
private currentLayout: Layout<any>;
|
||||
private currentSupervisor: Supervisor;
|
||||
|
||||
constructor(graph: IGraph<any>) {
|
||||
this.graph = graph;
|
||||
// this.tap();
|
||||
this.tap();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe the lifecycle of graph.
|
||||
*/
|
||||
private tap() {
|
||||
this.graph.hooks.layout.tap(this.onLayout.bind(this));
|
||||
}
|
||||
|
||||
private async onLayout(params: { graphCore: GraphCore; options?: LayoutOptions }) {
|
||||
// Stop currentLayout if any.
|
||||
this.stopLayout();
|
||||
|
||||
const { graphCore, options } = params;
|
||||
|
||||
const {
|
||||
type,
|
||||
workerEnabled,
|
||||
animated,
|
||||
iterations = 300,
|
||||
...rest
|
||||
} = {
|
||||
...this.graph.getSpecification().layout,
|
||||
...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;
|
||||
|
||||
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();
|
||||
} 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* `onTick` will get triggered in this case.
|
||||
*/
|
||||
} else {
|
||||
positions = await layout.execute(graphCore);
|
||||
}
|
||||
}
|
||||
|
||||
// Update nodes' positions.
|
||||
this.updateNodesPosition(positions);
|
||||
}
|
||||
|
||||
stopLayout() {
|
||||
if (this.currentLayout && isLayoutWithIterations(this.currentLayout)) {
|
||||
this.currentLayout.stop();
|
||||
this.currentLayout = null;
|
||||
}
|
||||
|
||||
if (this.currentSupervisor) {
|
||||
this.currentSupervisor.stop();
|
||||
this.currentSupervisor = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopLayout();
|
||||
|
||||
if (this.currentSupervisor) {
|
||||
this.currentSupervisor.kill();
|
||||
}
|
||||
}
|
||||
|
||||
private updateNodesPosition(positions: LayoutMapping) {
|
||||
positions.nodes.forEach((node) => {
|
||||
this.graph.updateData('node', {
|
||||
id: node.id,
|
||||
data: {
|
||||
x: node.data.x,
|
||||
y: node.data.y,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import { GraphCore } from '../types/data';
|
||||
import { EdgeModel, EdgeModelData } from '../types/edge';
|
||||
import { Hooks } from '../types/hook';
|
||||
import { ITEM_TYPE } from '../types/item';
|
||||
import { LayoutCommonConfig } from '../types/layout';
|
||||
import { LayoutOptions } from '../types/layout';
|
||||
import { NodeModel, NodeModelData } from '../types/node';
|
||||
import { FitViewRules, GraphAlignment } from '../types/view';
|
||||
import { createCanvas } from '../util/canvas';
|
||||
@ -111,6 +111,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
graphCore: GraphCore;
|
||||
}>({ name: 'itemchange' }),
|
||||
render: new Hook<{ graphCore: GraphCore }>({ name: 'render' }),
|
||||
layout: new Hook<{ graphCore: GraphCore }>({ name: 'layout' }),
|
||||
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
|
||||
behaviorchange: new Hook<{
|
||||
action: 'update' | 'add' | 'remove';
|
||||
@ -143,20 +144,28 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
* @returns
|
||||
* @group Data
|
||||
*/
|
||||
public read(data: GraphData) {
|
||||
public async read(data: GraphData) {
|
||||
this.hooks.datachange.emit({ data, type: 'replace' });
|
||||
const emitRender = () => {
|
||||
const emitRender = async () => {
|
||||
this.hooks.render.emit({
|
||||
graphCore: this.dataController.graphCore,
|
||||
});
|
||||
this.emit('afterrender');
|
||||
|
||||
// TODO: make read async?
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore: this.dataController.graphCore,
|
||||
});
|
||||
|
||||
this.emit('afterlayout');
|
||||
};
|
||||
if (this.canvasReady) {
|
||||
emitRender();
|
||||
await emitRender();
|
||||
} else {
|
||||
Promise.all(
|
||||
await Promise.all(
|
||||
[this.backgroundCanvas, this.canvas, this.transientCanvas].map((canvas) => canvas.ready),
|
||||
).then(emitRender);
|
||||
);
|
||||
await emitRender();
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,12 +176,18 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
* @returns
|
||||
* @group Data
|
||||
*/
|
||||
public changeData(data: GraphData, type: 'replace' | 'mergeReplace' = 'mergeReplace') {
|
||||
public async changeData(data: GraphData, type: 'replace' | 'mergeReplace' = 'mergeReplace') {
|
||||
this.hooks.datachange.emit({ data, type });
|
||||
this.hooks.render.emit({
|
||||
graphCore: this.dataController.graphCore,
|
||||
});
|
||||
this.emit('afterrender');
|
||||
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore: this.dataController.graphCore,
|
||||
});
|
||||
|
||||
this.emit('afterlayout');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -551,20 +566,20 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
|
||||
// ===== layout =====
|
||||
/**
|
||||
* Layout the graph (with current configurations if cfg is not assigned).
|
||||
* @param {LayoutCommonConfig} cfg layout configurations. if assigned, the layout spec of the graph will be updated in the same time
|
||||
* @param {GraphAlignment} align align the result
|
||||
* @param {Point} canvasPoint align the result
|
||||
* @param {boolean} stack push it into stack
|
||||
* @group Layout
|
||||
*/
|
||||
public layout(
|
||||
cfg?: LayoutCommonConfig,
|
||||
align?: GraphAlignment,
|
||||
canvasPoint?: Point,
|
||||
stack?: boolean,
|
||||
) {
|
||||
// TODO: LayoutConfig combination instead of LayoutCommonConfig
|
||||
// TODO
|
||||
public async layout(options?: LayoutOptions) {
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore: this.dataController.graphCore,
|
||||
options,
|
||||
});
|
||||
this.emit('afterlayout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Some layout algorithms has many iterations which can be stopped at any time.
|
||||
*/
|
||||
public stopLayout() {
|
||||
this.layoutController.stopLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IHook } from "../types/hook";
|
||||
import { IHook } from '../types/hook';
|
||||
|
||||
/**
|
||||
* A hook class unified the definitions of tap, untap, and emit.
|
||||
@ -16,14 +16,14 @@ export default class Hook<T> implements IHook<T> {
|
||||
}
|
||||
/**
|
||||
* Tap a listener to the corresponding lifecycle of this hook.
|
||||
* @param listener
|
||||
* @param listener
|
||||
*/
|
||||
public tap(listener: (param: T) => void) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
/**
|
||||
* Remove a listener from the corresponding lifecycle of this hook.
|
||||
* @param listener
|
||||
* @param listener
|
||||
*/
|
||||
public unTap(listener: (param: T) => void) {
|
||||
const idx = this.listeners.indexOf(listener);
|
||||
@ -31,28 +31,34 @@ export default class Hook<T> implements IHook<T> {
|
||||
}
|
||||
/**
|
||||
* Emit the corresponding lifecycle to call the listeners
|
||||
* @param param
|
||||
* @param param
|
||||
*/
|
||||
public emit(param: T) {
|
||||
this.listeners.forEach(listener => listener(param));
|
||||
this.listeners.forEach((listener) => listener(param));
|
||||
}
|
||||
/**
|
||||
* Linearly async emit the corresponding lifecycle to call the listeners
|
||||
* @param param
|
||||
* @param param
|
||||
*/
|
||||
public async emitLinearAsync(param: T): Promise<void> {
|
||||
return new Promise(async () => {
|
||||
let start = Promise.resolve();
|
||||
this.listeners.forEach(listener => {
|
||||
start = start.then(async () => new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await listener(param);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject();
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
for (const listener of this.listeners) {
|
||||
await listener(param);
|
||||
}
|
||||
// return new Promise(async () => {
|
||||
// let start = Promise.resolve();
|
||||
// this.listeners.forEach((listener) => {
|
||||
// start = start.then(
|
||||
// async () =>
|
||||
// new Promise(async (resolve, reject) => {
|
||||
// try {
|
||||
// await listener(param);
|
||||
// resolve();
|
||||
// } catch (e) {
|
||||
// reject();
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { comboFromNode } from "./data/comboFromNode"
|
||||
import DragCanvas from "./behavior/drag-canvas";
|
||||
import { Lib } from "../types/stdlib";
|
||||
import { CircleNode } from "./item/node";
|
||||
import { LineEdge } from "./item/edge";
|
||||
import { registry as layoutRegistry } from '@antv/layout';
|
||||
import { Lib } from '../types/stdlib';
|
||||
import DragCanvas from './behavior/drag-canvas';
|
||||
import { comboFromNode } from './data/comboFromNode';
|
||||
import { LineEdge } from './item/edge';
|
||||
import { CircleNode } from './item/node';
|
||||
|
||||
const stdLib = {
|
||||
transforms: {
|
||||
comboFromNode
|
||||
comboFromNode,
|
||||
},
|
||||
themes: {},
|
||||
layouts: {}, // from @antv/layout
|
||||
layouts: layoutRegistry,
|
||||
behaviors: {
|
||||
'drag-canvas': DragCanvas
|
||||
},
|
||||
@ -34,6 +35,6 @@ const useLib: Lib = {
|
||||
combos: {},
|
||||
};
|
||||
|
||||
const registery = { useLib };
|
||||
export default registery;
|
||||
export { stdLib, registery };
|
||||
const registry = { useLib };
|
||||
export default registry;
|
||||
export { stdLib, registry };
|
||||
|
@ -9,7 +9,7 @@ import { Padding, Point } from './common';
|
||||
import { DataChangeType, GraphData } from './data';
|
||||
import { EdgeModel, EdgeUserModel } from './edge';
|
||||
import { ITEM_TYPE } from './item';
|
||||
import { LayoutCommonConfig } from './layout';
|
||||
import { LayoutOptions } from './layout';
|
||||
import { NodeModel, NodeUserModel } from './node';
|
||||
import { Specification } from './spec';
|
||||
import { FitViewRules, GraphAlignment } from './view';
|
||||
@ -295,18 +295,9 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
|
||||
// ===== layout =====
|
||||
/**
|
||||
* Layout the graph (with current configurations if cfg is not assigned).
|
||||
* @param {LayoutCommonConfig} cfg layout configurations. if assigned, the layout spec of the graph will be updated in the same time
|
||||
* @param {GraphAlignment} align align the result
|
||||
* @param {Point} canvasPoint align the result
|
||||
* @param {boolean} stack push it into stack
|
||||
* @group Layout
|
||||
*/
|
||||
layout: (
|
||||
cfg?: LayoutCommonConfig,
|
||||
align?: GraphAlignment,
|
||||
canvasPoint?: Point,
|
||||
stack?: boolean,
|
||||
) => void;
|
||||
layout: (options?: LayoutOptions) => Promise<void>;
|
||||
stopLayout: () => void;
|
||||
|
||||
// ===== interaction =====
|
||||
/**
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { DataChangeType, GraphCore, GraphData } from "./data";
|
||||
import { NodeModel, NodeModelData, NodeUserModel } from "./node";
|
||||
import { EdgeModel, EdgeModelData, EdgeUserModel } from "./edge";
|
||||
import { NodeModelData } from "./node";
|
||||
import { EdgeModelData } from "./edge";
|
||||
import { ITEM_TYPE } from "./item";
|
||||
import { GraphChange, ID } from "@antv/graphlib";
|
||||
import { LayoutOptions } from "./layout";
|
||||
|
||||
export interface IHook<T> {
|
||||
name: string;
|
||||
@ -10,10 +11,11 @@ export interface IHook<T> {
|
||||
tap: (listener: (param: T) => void) => void;
|
||||
unTap: (listener: (param: T) => void) => void;
|
||||
emit: (param: T) => void;
|
||||
emitLinearAsync: (param: T) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Hooks {
|
||||
'init': IHook<void>;
|
||||
init: IHook<void>;
|
||||
// data
|
||||
'datachange': IHook<{
|
||||
type: DataChangeType;
|
||||
@ -24,14 +26,14 @@ export interface Hooks {
|
||||
changes: GraphChange<NodeModelData, EdgeModelData>[];
|
||||
graphCore: GraphCore;
|
||||
}>;
|
||||
'render': IHook<{ graphCore: GraphCore }>; // TODO: define param template
|
||||
// 'layout': IHook<any>; // TODO: define param template
|
||||
render: IHook<{ graphCore: GraphCore }>; // TODO: define param template
|
||||
layout: IHook<{ graphCore: GraphCore; options?: LayoutOptions }>; // TODO: define param template
|
||||
// 'updatelayout': IHook<any>; // TODO: define param template
|
||||
'modechange': IHook<{ mode: string }>;
|
||||
'behaviorchange': IHook<{
|
||||
modechange: IHook<{ mode: string }>;
|
||||
behaviorchange: IHook<{
|
||||
action: 'update' | 'add' | 'remove';
|
||||
modes: string[];
|
||||
behaviors: (string | { type: string, key: string })[];
|
||||
behaviors: (string | { type: string; key: string })[];
|
||||
}>;
|
||||
'itemstatechange': IHook<{
|
||||
ids: ID[],
|
||||
@ -41,4 +43,4 @@ export interface Hooks {
|
||||
// 'viewportchange': IHook<any>; // TODO: define param template
|
||||
// 'destroy': IHook<any>; // TODO: define param template
|
||||
// TODO: more timecycles here
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,69 @@
|
||||
import {
|
||||
CircularLayoutOptions,
|
||||
ConcentricLayoutOptions,
|
||||
D3ForceLayoutOptions,
|
||||
ForceAtlas2LayoutOptions,
|
||||
ForceLayoutOptions,
|
||||
FruchtermanLayoutOptions,
|
||||
GridLayoutOptions,
|
||||
MDSLayoutOptions,
|
||||
RadialLayoutOptions,
|
||||
RandomLayoutOptions,
|
||||
} from '@antv/layout';
|
||||
|
||||
|
||||
export interface LayoutCommonConfig {
|
||||
type?: string;
|
||||
gpuEnabled?: boolean;
|
||||
export type LayoutOptions = (
|
||||
| CircularLayout
|
||||
| RandomLayout
|
||||
| ConcentricLayout
|
||||
| GridLayout
|
||||
| MDSLayout
|
||||
| RadialLayout
|
||||
| FruchtermanLayout
|
||||
| D3ForceLayout
|
||||
| ForceLayout
|
||||
| ForceAtlas2
|
||||
) & {
|
||||
workerEnabled?: boolean;
|
||||
// Works when workerEnabled is true, config it with a visitable url to avoid visiting online version.
|
||||
workerScriptURL?: string;
|
||||
}
|
||||
animated?: boolean;
|
||||
iterations?: number;
|
||||
};
|
||||
|
||||
interface CircularLayout extends CircularLayoutOptions {
|
||||
type: 'circular';
|
||||
}
|
||||
|
||||
interface RandomLayout extends RandomLayoutOptions {
|
||||
type: 'random';
|
||||
}
|
||||
|
||||
interface GridLayout extends GridLayoutOptions {
|
||||
type: 'grid';
|
||||
}
|
||||
|
||||
interface MDSLayout extends MDSLayoutOptions {
|
||||
type: 'mds';
|
||||
}
|
||||
|
||||
interface ConcentricLayout extends ConcentricLayoutOptions {
|
||||
type: 'concentric';
|
||||
}
|
||||
|
||||
interface RadialLayout extends RadialLayoutOptions {
|
||||
type: 'radial';
|
||||
}
|
||||
|
||||
interface FruchtermanLayout extends FruchtermanLayoutOptions {
|
||||
type: 'fruchterman' | 'fruchtermanGPU';
|
||||
}
|
||||
|
||||
interface D3ForceLayout extends D3ForceLayoutOptions {
|
||||
type: 'd3force';
|
||||
}
|
||||
|
||||
interface ForceLayout extends ForceLayoutOptions {
|
||||
type: 'force' | 'gforce';
|
||||
}
|
||||
|
||||
interface ForceAtlas2 extends ForceAtlas2LayoutOptions {
|
||||
type: 'forceAtlas2';
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import { FetchDataConfig, GraphData, InlineDataConfig, TransformerFn } from "./d
|
||||
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeShapesEncode } from "./edge";
|
||||
import { NodeDisplayModel, NodeEncode, NodeModel, NodeShapesEncode } from "./node";
|
||||
import { GraphAlignment } from "./view";
|
||||
import { LayoutCommonConfig } from "./layout";
|
||||
import { ComboDisplayModel, ComboEncode, ComboModel, ComboShapesEncode } from "./combo";
|
||||
import { BehaviorOptionsOf, BehaviorRegistry } from "./behavior";
|
||||
import { LayoutOptions } from "./layout";
|
||||
|
||||
type rendererName = 'canvas' | 'svg' | 'webgl';
|
||||
|
||||
@ -15,24 +15,32 @@ export interface Specification<B extends BehaviorRegistry> {
|
||||
container: string | HTMLElement;
|
||||
width?: number;
|
||||
height?: number;
|
||||
renderer?: rendererName | {
|
||||
type: rendererName,
|
||||
pixelRatio: number,
|
||||
headless: boolean,
|
||||
};
|
||||
renderer?:
|
||||
| rendererName
|
||||
| {
|
||||
type: rendererName;
|
||||
pixelRatio: number;
|
||||
headless: boolean;
|
||||
};
|
||||
zoom?: number;
|
||||
autoFit?: 'view' | 'center' | {
|
||||
position: Point,
|
||||
alignment: GraphAlignment
|
||||
};
|
||||
autoFit?:
|
||||
| 'view'
|
||||
| 'center'
|
||||
| {
|
||||
position: Point;
|
||||
alignment: GraphAlignment;
|
||||
};
|
||||
optimizeThreshold?: number;
|
||||
|
||||
/** data */
|
||||
data: GraphData | InlineDataConfig | FetchDataConfig; // TODO: more
|
||||
transform?: string[] | {
|
||||
type: string,
|
||||
[param: string]: unknown // TODO: generate by plugins
|
||||
}[] | TransformerFn[];
|
||||
transform?:
|
||||
| string[]
|
||||
| {
|
||||
type: string;
|
||||
[param: string]: unknown; // TODO: generate by plugins
|
||||
}[]
|
||||
| TransformerFn[];
|
||||
|
||||
/** item */
|
||||
node?: ((data: NodeModel) => NodeDisplayModel) | NodeEncode;
|
||||
@ -51,7 +59,7 @@ export interface Specification<B extends BehaviorRegistry> {
|
||||
};
|
||||
|
||||
/** layout */
|
||||
layout?: LayoutCommonConfig | LayoutCommonConfig[]; // TODO: Config comes from @antv/layout
|
||||
layout?: LayoutOptions | LayoutOptions[];
|
||||
|
||||
/** interaction */
|
||||
modes?: {
|
||||
@ -65,7 +73,7 @@ export interface Specification<B extends BehaviorRegistry> {
|
||||
|
||||
/** free plugins */
|
||||
plugins?: {
|
||||
name: string,
|
||||
name: string;
|
||||
options: any; // TODO: configs from plugins
|
||||
}[]
|
||||
}[];
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Canvas } from '@antv/g';
|
||||
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||
import { Renderer as SVGRenderer } from '@antv/g-svg';
|
||||
// import { Renderer as WebGLRenderer } from '@antv/g-webgl';
|
||||
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
|
||||
import { isString } from '@antv/util';
|
||||
|
||||
/**
|
||||
@ -28,8 +28,7 @@ export const createCanvas = (
|
||||
Renderer = SVGRenderer;
|
||||
break;
|
||||
case 'webgl':
|
||||
// Renderer = WebGLRenderer;
|
||||
// TODO
|
||||
Renderer = WebGLRenderer;
|
||||
break;
|
||||
default:
|
||||
Renderer = CanvasRenderer;
|
||||
@ -44,7 +43,7 @@ export const createCanvas = (
|
||||
canvasTag.style.height = `${height}px`;
|
||||
canvasTag.style.position = 'fixed';
|
||||
const containerDOM = isString(container) ? document.getElementById('container') : container;
|
||||
containerDOM.appendChild(canvasTag);
|
||||
containerDOM!.appendChild(canvasTag);
|
||||
return new Canvas({
|
||||
canvas: canvasTag,
|
||||
devicePixelRatio: pixelRatio,
|
||||
@ -58,4 +57,4 @@ export const createCanvas = (
|
||||
devicePixelRatio: pixelRatio,
|
||||
renderer: new Renderer()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BehaviorRegistry } from '../types/behavior';
|
||||
import Graph from '../runtime/graph';
|
||||
import registery from '../stdlib';
|
||||
import registry from '../stdlib';
|
||||
|
||||
/**
|
||||
* Extend graph class with custom libs (extendLibrary), and extendLibrary will be merged into useLib.
|
||||
@ -20,9 +20,9 @@ export const extend = <B1 extends BehaviorRegistry, B2 extends BehaviorRegistry>
|
||||
): typeof Graph<B1 & B2> => {
|
||||
// merged the extendLibrary to useLib for global usage
|
||||
Object.keys(extendLibrary).forEach((cat) => {
|
||||
registery.useLib[cat] = Object.assign({}, registery.useLib[cat], extendLibrary[cat] || {});
|
||||
Object.keys(registery.useLib[cat]).forEach((type) => {
|
||||
const extension = registery.useLib[cat][type];
|
||||
registry.useLib[cat] = Object.assign({}, registry.useLib[cat], extendLibrary[cat] || {});
|
||||
Object.keys(registry.useLib[cat]).forEach((type) => {
|
||||
const extension = registry.useLib[cat][type];
|
||||
extension.type = type;
|
||||
});
|
||||
});
|
||||
|
841
packages/g6/tests/datasets/dataset1.ts
Normal file
841
packages/g6/tests/datasets/dataset1.ts
Normal file
@ -0,0 +1,841 @@
|
||||
import { GraphData } from '../../src';
|
||||
|
||||
const data: GraphData = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'Argentina',
|
||||
data: {
|
||||
name: 'Argentina',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Australia',
|
||||
data: {
|
||||
name: 'Australia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Belgium',
|
||||
data: {
|
||||
name: 'Belgium',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Brazil',
|
||||
data: {
|
||||
name: 'Brazil',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Colombia',
|
||||
data: {
|
||||
name: 'Colombia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Costa Rica',
|
||||
data: {
|
||||
name: 'Costa Rica',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Croatia',
|
||||
data: {
|
||||
name: 'Croatia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Denmark',
|
||||
data: {
|
||||
name: 'Denmark',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Egypt',
|
||||
data: {
|
||||
name: 'Egypt',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'England',
|
||||
data: {
|
||||
name: 'England',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'France',
|
||||
data: {
|
||||
name: 'France',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Germany',
|
||||
data: {
|
||||
name: 'Germany',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Iceland',
|
||||
data: {
|
||||
name: 'Iceland',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'IR Iran',
|
||||
data: {
|
||||
name: 'IR Iran',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Japan',
|
||||
data: {
|
||||
name: 'Japan',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Korea Republic',
|
||||
data: {
|
||||
name: 'Korea Republic',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Mexico',
|
||||
data: {
|
||||
name: 'Mexico',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Morocco',
|
||||
data: {
|
||||
name: 'Morocco',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Nigeria',
|
||||
data: {
|
||||
name: 'Nigeria',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Panama',
|
||||
data: {
|
||||
name: 'Panama',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Peru',
|
||||
data: {
|
||||
name: 'Peru',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Poland',
|
||||
data: {
|
||||
name: 'Poland',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Portugal',
|
||||
data: {
|
||||
name: 'Portugal',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Russia',
|
||||
data: {
|
||||
name: 'Russia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Saudi Arabia',
|
||||
data: {
|
||||
name: 'Saudi Arabia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Senegal',
|
||||
data: {
|
||||
name: 'Senegal',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Serbia',
|
||||
data: {
|
||||
name: 'Serbia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Spain',
|
||||
data: {
|
||||
name: 'Spain',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Sweden',
|
||||
data: {
|
||||
name: 'Sweden',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Switzerland',
|
||||
data: {
|
||||
name: 'Switzerland',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Tunisia',
|
||||
data: {
|
||||
name: 'Tunisia',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Uruguay',
|
||||
data: {
|
||||
name: 'Uruguay',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: '0',
|
||||
target: 'Russia',
|
||||
source: 'Saudi Arabia',
|
||||
data: {
|
||||
target_score: 5,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
target: 'Uruguay',
|
||||
source: 'Egypt',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
target: 'Russia',
|
||||
source: 'Egypt',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
target: 'Uruguay',
|
||||
source: 'Saudi Arabia',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
target: 'Uruguay',
|
||||
source: 'Russia',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
target: 'Saudi Arabia',
|
||||
source: 'Egypt',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
target: 'IR Iran',
|
||||
source: 'Morocco',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
target: 'Portugal',
|
||||
source: 'Spain',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 3,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
target: 'Portugal',
|
||||
source: 'Morocco',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
target: 'Spain',
|
||||
source: 'IR Iran',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
target: 'IR Iran',
|
||||
source: 'Portugal',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 1,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
target: 'Spain',
|
||||
source: 'Morocco',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 2,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
target: 'France',
|
||||
source: 'Australia',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
target: 'Denmark',
|
||||
source: 'Peru',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
target: 'Denmark',
|
||||
source: 'Australia',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 1,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
target: 'France',
|
||||
source: 'Peru',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
target: 'Denmark',
|
||||
source: 'France',
|
||||
data: {
|
||||
target_score: 0,
|
||||
source_score: 0,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
target: 'Peru',
|
||||
source: 'Australia',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
target: 'Argentina',
|
||||
source: 'Iceland',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
target: 'Croatia',
|
||||
source: 'Nigeria',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
target: 'Croatia',
|
||||
source: 'Argentina',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
target: 'Nigeria',
|
||||
source: 'Iceland',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
target: 'Argentina',
|
||||
source: 'Nigeria',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
target: 'Croatia',
|
||||
source: 'Iceland',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
target: 'Serbia',
|
||||
source: 'Costa Rica',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '25',
|
||||
target: 'Brazil',
|
||||
source: 'Switzerland',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 1,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '26',
|
||||
target: 'Brazil',
|
||||
source: 'Costa Rica',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '27',
|
||||
target: 'Switzerland',
|
||||
source: 'Serbia',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '28',
|
||||
target: 'Brazil',
|
||||
source: 'Serbia',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '29',
|
||||
target: 'Switzerland',
|
||||
source: 'Costa Rica',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 2,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
target: 'Mexico',
|
||||
source: 'Germany',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '31',
|
||||
target: 'Sweden',
|
||||
source: 'Korea Republic',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '32',
|
||||
target: 'Mexico',
|
||||
source: 'Korea Republic',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '33',
|
||||
target: 'Germany',
|
||||
source: 'Sweden',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '34',
|
||||
target: 'Korea Republic',
|
||||
source: 'Germany',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '35',
|
||||
target: 'Sweden',
|
||||
source: 'Mexico',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '36',
|
||||
target: 'Belgium',
|
||||
source: 'Panama',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '37',
|
||||
target: 'England',
|
||||
source: 'Tunisia',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '38',
|
||||
target: 'Belgium',
|
||||
source: 'Tunisia',
|
||||
data: {
|
||||
target_score: 5,
|
||||
source_score: 2,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '39',
|
||||
target: 'England',
|
||||
source: 'Panama',
|
||||
data: {
|
||||
target_score: 6,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '40',
|
||||
target: 'Belgium',
|
||||
source: 'England',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '41',
|
||||
target: 'Tunisia',
|
||||
source: 'Panama',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '42',
|
||||
target: 'Japan',
|
||||
source: 'Colombia',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '43',
|
||||
target: 'Senegal',
|
||||
source: 'Poland',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '44',
|
||||
target: 'Japan',
|
||||
source: 'Senegal',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 2,
|
||||
directed: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '45',
|
||||
target: 'Colombia',
|
||||
source: 'Poland',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '46',
|
||||
target: 'Poland',
|
||||
source: 'Japan',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '47',
|
||||
target: 'Colombia',
|
||||
source: 'Senegal',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '48',
|
||||
target: 'Uruguay',
|
||||
source: 'Portugal',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '49',
|
||||
target: 'France',
|
||||
source: 'Argentina',
|
||||
data: {
|
||||
target_score: 4,
|
||||
source_score: 3,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '50',
|
||||
target: 'Russia',
|
||||
source: 'Spain',
|
||||
data: {
|
||||
target_score: 5,
|
||||
source_score: 4,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '51',
|
||||
target: 'Croatia',
|
||||
source: 'Denmark',
|
||||
data: {
|
||||
target_score: 4,
|
||||
source_score: 3,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '52',
|
||||
target: 'Brazil',
|
||||
source: 'Mexico',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '53',
|
||||
target: 'Belgium',
|
||||
source: 'Japan',
|
||||
data: {
|
||||
target_score: 3,
|
||||
source_score: 2,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '54',
|
||||
target: 'Sweden',
|
||||
source: 'Switzerland',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '55',
|
||||
target: 'England',
|
||||
source: 'Colombia',
|
||||
data: {
|
||||
target_score: 4,
|
||||
source_score: 3,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '56',
|
||||
target: 'France',
|
||||
source: 'Uruguay',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '57',
|
||||
target: 'Belgium',
|
||||
source: 'Brazil',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '58',
|
||||
target: 'Croatia',
|
||||
source: 'Russia',
|
||||
data: {
|
||||
target_score: 6,
|
||||
source_score: 5,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '59',
|
||||
target: 'England',
|
||||
source: 'Sweden',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '60',
|
||||
target: 'France',
|
||||
source: 'Belgium',
|
||||
data: {
|
||||
target_score: 1,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '61',
|
||||
target: 'Croatia',
|
||||
source: 'England',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 1,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '62',
|
||||
target: 'Belgium',
|
||||
source: 'England',
|
||||
data: {
|
||||
target_score: 2,
|
||||
source_score: 0,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '63',
|
||||
target: 'France',
|
||||
source: 'Croatia',
|
||||
data: {
|
||||
target_score: 4,
|
||||
source_score: 2,
|
||||
directed: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export { data };
|
271
packages/g6/tests/unit/layout-spec.ts
Normal file
271
packages/g6/tests/unit/layout-spec.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import { Graph, Layout, LayoutMapping } from '@antv/layout';
|
||||
import G6, { IGraph, stdLib } from '../../src/index';
|
||||
import { data } from '../datasets/dataset1';
|
||||
const container = document.createElement('div');
|
||||
document.querySelector('body').appendChild(container);
|
||||
|
||||
describe('layout', () => {
|
||||
let graph: IGraph<any>;
|
||||
it('should apply circular layout correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger re-layout by calling `layout` method manually.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
// first-time layout
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
// re-layout
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(350);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
graph.layout({
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 100, // change radius here
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should trigger re-layout by calling `changeData` method manually.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
// first-time layout
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
// re-layout
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(250);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
// Only one single node.
|
||||
const newData = {
|
||||
nodes: [{ id: 'node13', data: { x: 50, y: 50 } }],
|
||||
edges: [{ id: 'edge1', source: 'node13', target: 'node13', data: {} }],
|
||||
};
|
||||
graph.changeData(newData);
|
||||
});
|
||||
});
|
||||
|
||||
it('should run layout in WebWorker with `workerEnabled`.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
workerEnabled: true,
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
// re-layout
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].data.x).toBe(350);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
graph.layout({
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 100, // change radius here
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display the layout process with `animated`.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'd3force',
|
||||
animated: true,
|
||||
center: [250, 250],
|
||||
preventOverlap: true,
|
||||
nodeSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData.every((node) => node.data.x > 0 && node.data.y > 0)).toBeTruthy();
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop animated layout process with `stopLayout`.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'd3force',
|
||||
animated: true,
|
||||
center: [250, 250],
|
||||
preventOverlap: true,
|
||||
nodeSize: 20,
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
graph.stopLayout();
|
||||
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData.every((node) => node.data.x > 0 && node.data.y > 0)).toBeTruthy();
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('should manually steps the simulation with `iterations` and `animated` disabled.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'd3force',
|
||||
animated: false,
|
||||
center: [250, 250],
|
||||
preventOverlap: true,
|
||||
nodeSize: 20,
|
||||
iterations: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData.every((node) => node.data.x > 0 && node.data.y > 0)).toBeTruthy();
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async execute(graph: Graph, options?: {}): Promise<LayoutMapping> {
|
||||
const nodes = graph.getAllNodes();
|
||||
return {
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
data: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
})),
|
||||
edges: [],
|
||||
};
|
||||
}
|
||||
options: {};
|
||||
id: 'myCustomLayout';
|
||||
}
|
||||
|
||||
// Register custom layout
|
||||
stdLib.layouts['myCustomLayout'] = MyCustomLayout;
|
||||
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
// @ts-ignore
|
||||
type: 'myCustomLayout',
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData.every((node) => node.data.x === 0 && node.data.y === 0)).toBeTruthy();
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user