* 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:
xiaoiver 2023-03-01 14:45:57 +08:00 committed by GitHub
parent 875a9eb39b
commit 0f23a181db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1457 additions and 133 deletions

View File

@ -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"
}
}
}

View File

@ -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"
},

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
/**

View File

@ -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();
// }
// }),
// );
// });
// });
}
}
}

View File

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

View File

@ -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 =====
/**

View File

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

View File

@ -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';
}

View File

@ -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
}[]
}[];
}

View File

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

View File

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

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

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