feat: tree formatted data rendering; feat: collapse-expand-tree behavior;

This commit is contained in:
Yanyan-Wang 2023-08-08 20:13:11 +08:00
parent 11a9a3016a
commit 5ebc32f86c
74 changed files with 1493 additions and 339 deletions

View File

@ -46,7 +46,7 @@
"test": "jest", "test": "jest",
"test:integration": "node --expose-gc --max-old-space-size=4096 --unhandled-rejections=strict node_modules/jest/bin/jest tests/integration/ --config jest.node.config.js --coverage -i --logHeapUsage --detectOpenHandles", "test:integration": "node --expose-gc --max-old-space-size=4096 --unhandled-rejections=strict node_modules/jest/bin/jest tests/integration/ --config jest.node.config.js --coverage -i --logHeapUsage --detectOpenHandles",
"size": "limit-size", "size": "limit-size",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/edge-spec.ts", "test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/item-animate-spec.ts",
"test-behavior": "DEBUG_MODE=1 jest --watch ./tests/unit/item-3d-spec.ts" "test-behavior": "DEBUG_MODE=1 jest --watch ./tests/unit/item-3d-spec.ts"
}, },
"lint-staged": { "lint-staged": {
@ -58,7 +58,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/colors": "^7.0.0", "@ant-design/colors": "^7.0.0",
"@antv/algorithm": "^0.1.25", "@antv/algorithm": "^0.1.26",
"@antv/dom-util": "^2.0.4", "@antv/dom-util": "^2.0.4",
"@antv/g": "^5.15.7", "@antv/g": "^5.15.7",
"@antv/g-canvas": "^1.9.28", "@antv/g-canvas": "^1.9.28",
@ -75,6 +75,7 @@
"@antv/matrix-util": "^3.0.4", "@antv/matrix-util": "^3.0.4",
"@antv/util": "~2.0.5", "@antv/util": "~2.0.5",
"color": "^4.2.3", "color": "^4.2.3",
"stats-js": "^1.0.1",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typedoc-plugin-markdown": "^3.14.0", "typedoc-plugin-markdown": "^3.14.0",
"typescript": "^5.1.6" "typescript": "^5.1.6"
@ -115,6 +116,7 @@
"rollup-plugin-polyfill-node": "^0.8.0", "rollup-plugin-polyfill-node": "^0.8.0",
"rollup-plugin-typescript": "^1.0.1", "rollup-plugin-typescript": "^1.0.1",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"stats-js": "1.0.1",
"stats.js": "^0.17.0", "stats.js": "^0.17.0",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",
"typedoc": "^0.24.0", "typedoc": "^0.24.0",

View File

@ -74,6 +74,7 @@ export default class Combo extends Node {
displayModel: ComboDisplayModel, displayModel: ComboDisplayModel,
diffData?: { previous: ComboUserModelData; current: ComboUserModelData }, diffData?: { previous: ComboUserModelData; current: ComboUserModelData },
diffState?: { previous: State[]; current: State[] }, diffState?: { previous: State[]; current: State[] },
animate: boolean = true,
onfinish: Function = () => {}, onfinish: Function = () => {},
) { ) {
if (displayModel.data.collapsed) { if (displayModel.data.collapsed) {
@ -90,7 +91,7 @@ export default class Combo extends Node {
} }
}); });
} }
super.draw(displayModel, diffData, diffState, onfinish); super.draw(displayModel, diffData, diffState, animate, onfinish);
} }
/** /**

View File

@ -8,6 +8,7 @@ import { animateShapes } from '../util/animate';
import { EdgeStyleSet } from '../types/theme'; import { EdgeStyleSet } from '../types/theme';
import Item from './item'; import Item from './item';
import Node from './node'; import Node from './node';
import Combo from './combo';
interface IProps { interface IProps {
model: EdgeModel; model: EdgeModel;
@ -50,6 +51,7 @@ export default class Edge extends Item {
displayModel: EdgeDisplayModel, displayModel: EdgeDisplayModel,
diffData?: { previous: EdgeModelData; current: EdgeModelData }, diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[]; current: State[] }, diffState?: { previous: State[]; current: State[] },
animate: boolean = true,
onfinish: Function = () => {}, onfinish: Function = () => {},
) { ) {
// get the end points // get the end points
@ -94,7 +96,7 @@ export default class Edge extends Item {
const timing = firstRendering ? 'buildIn' : 'update'; const timing = firstRendering ? 'buildIn' : 'update';
// handle shape's animate // handle shape's animate
if (!disableAnimate && usingAnimates[timing]?.length) { if (animate && !disableAnimate && usingAnimates[timing]?.length) {
this.animations = animateShapes( this.animations = animateShapes(
usingAnimates, usingAnimates,
targetStyles, // targetStylesMap targetStyles, // targetStylesMap
@ -103,7 +105,7 @@ export default class Edge extends Item {
firstRendering ? 'buildIn' : 'update', firstRendering ? 'buildIn' : 'update',
this.changedStates, this.changedStates,
this.animateFrameListener, this.animateFrameListener,
() => onfinish(displayModel.id), (canceled) => onfinish(displayModel.id, canceled),
); );
} }
} }
@ -140,8 +142,8 @@ export default class Edge extends Item {
public clone( public clone(
containerGroup: Group, containerGroup: Group,
sourceItem: Node, sourceItem: Node | Combo,
targetItem: Node, targetItem: Node | Combo,
onlyKeyShape?: boolean, onlyKeyShape?: boolean,
disableAnimate?: boolean, disableAnimate?: boolean,
) { ) {

View File

@ -68,8 +68,10 @@ export default abstract class Item implements IItem {
/** The flag of transient item. */ /** The flag of transient item. */
public transient = false; public transient = false;
public renderExtensions: any; // TODO public renderExtensions: any; // TODO
/** Cache the animation instances to stop at next lifecycle. */ /** Cache the shape animation instances to stop at next lifecycle. */
public animations: IAnimation[]; public animations: IAnimation[];
/** Cache the group animation instances to stop at next lifecycle. */
public groupAnimations: IAnimation[];
public themeStyles: { public themeStyles: {
default: ItemShapeStyles; default: ItemShapeStyles;
@ -159,6 +161,7 @@ export default abstract class Item implements IItem {
displayModel: ItemDisplayModel, displayModel: ItemDisplayModel,
diffData?: { previous: ItemModelData; current: ItemModelData }, diffData?: { previous: ItemModelData; current: ItemModelData },
diffState?: { previous: State[]; current: State[] }, diffState?: { previous: State[]; current: State[] },
animate: boolean = true,
onfinish: Function = () => {}, onfinish: Function = () => {},
) { ) {
// call this.renderExt.draw in extend implementations // call this.renderExt.draw in extend implementations
@ -198,6 +201,7 @@ export default abstract class Item implements IItem {
lodStrategy: LodStrategyObj; lodStrategy: LodStrategyObj;
}, },
onlyMove?: boolean, onlyMove?: boolean,
animate: boolean = true,
onfinish?: Function, onfinish?: Function,
) { ) {
// 1. merge model into this model // 1. merge model into this model
@ -218,7 +222,7 @@ export default abstract class Item implements IItem {
: itemTheme?.lodStrategy || this.lodStrategy; : itemTheme?.lodStrategy || this.lodStrategy;
if (onlyMove) { if (onlyMove) {
this.updatePosition(displayModel, diffData, onfinish); this.updatePosition(displayModel, diffData, animate, onfinish);
return; return;
} }
@ -242,9 +246,9 @@ export default abstract class Item implements IItem {
} }
// 3. call element update fn from useLib // 3. call element update fn from useLib
if (this.states?.length) { if (this.states?.length) {
this.drawWithStates(this.states, onfinish); this.drawWithStates(this.states, animate, onfinish);
} else { } else {
this.draw(this.displayModel, diffData, undefined, onfinish); this.draw(this.displayModel, diffData, undefined, animate, onfinish);
} }
// 4. tag all the states with 'dirty', for state style regenerating when state changed // 4. tag all the states with 'dirty', for state style regenerating when state changed
this.stateDirtyMap = {}; this.stateDirtyMap = {};
@ -261,6 +265,7 @@ export default abstract class Item implements IItem {
public updatePosition( public updatePosition(
displayModel: ItemDisplayModel, displayModel: ItemDisplayModel,
diffData?: { previous: ItemModelData; current: ItemModelData }, diffData?: { previous: ItemModelData; current: ItemModelData },
animate?: boolean,
onfinish?: Function, onfinish?: Function,
) {} ) {}
@ -395,60 +400,63 @@ export default abstract class Item implements IItem {
/** Show the item. */ /** Show the item. */
public show(animate = true) { public show(animate = true) {
// TODO: utilize graphcore's view Promise.all(this.stopAnimations()).finally(() => {
this.stopAnimations(); if (this.destroyed || this.visible) return;
const { animates = {} } = this.displayModel.data;
const { animates = {} } = this.displayModel.data; if (animate && animates.show?.length) {
if (animate && animates.show?.length) { const showAnimateFieldsMap: any = {};
const showAnimateFieldsMap: any = {}; Object.values(animates.show).forEach((animate) => {
Object.values(animates.show).forEach((animate) => { const { shapeId = 'group' } = animate;
const { shapeId = 'group' } = animate; showAnimateFieldsMap[shapeId] = (
showAnimateFieldsMap[shapeId] = ( showAnimateFieldsMap[shapeId] || []
showAnimateFieldsMap[shapeId] || [] ).concat(animate.fields);
).concat(animate.fields); });
}); const targetStyleMap = {};
const targetStyleMap = {}; Object.keys(this.shapeMap).forEach((id) => {
Object.keys(this.shapeMap).forEach((id) => { const shape = this.shapeMap[id];
const shape = this.shapeMap[id]; if (!this.cacheHiddenShape[id]) {
if (!this.cacheHiddenShape[id]) { // set the animate fields to initial value
// set the animate fields to initial value if (showAnimateFieldsMap[id]) {
if (showAnimateFieldsMap[id]) { targetStyleMap[id] = targetStyleMap[id] || {};
targetStyleMap[id] = targetStyleMap[id] || {}; const beginStyle = getShapeAnimateBeginStyles(shape);
const beginStyle = getShapeAnimateBeginStyles(shape); showAnimateFieldsMap[id].forEach((field) => {
showAnimateFieldsMap[id].forEach((field) => { if (beginStyle.hasOwnProperty(field)) {
if (beginStyle.hasOwnProperty(field)) { targetStyleMap[id][field] = shape.style[field];
targetStyleMap[id][field] = shape.style[field]; shape.style[field] = beginStyle[field];
shape.style[field] = beginStyle[field]; }
} });
}); }
shape.show();
} }
shape.show(); });
if (showAnimateFieldsMap.group) {
showAnimateFieldsMap.group.forEach((field) => {
const usingField = field === 'size' ? 'transform' : field;
if (GROUP_ANIMATE_STYLES[0].hasOwnProperty(usingField)) {
this.group.style[usingField] =
GROUP_ANIMATE_STYLES[0][usingField];
}
});
} }
});
if (showAnimateFieldsMap.group) { this.animations = this.runWithAnimates(
showAnimateFieldsMap.group.forEach((field) => { animates,
const usingField = field === 'size' ? 'transform' : field; 'show',
if (GROUP_ANIMATE_STYLES[0].hasOwnProperty(usingField)) { targetStyleMap,
this.group.style[usingField] = GROUP_ANIMATE_STYLES[0][usingField]; );
} } else {
Object.keys(this.shapeMap).forEach((id) => {
const shape = this.shapeMap[id];
if (!this.cacheHiddenShape[id]) shape.show();
}); });
} }
this.animations = this.runWithAnimates(animates, 'show', targetStyleMap); this.visible = true;
} else { });
Object.keys(this.shapeMap).forEach((id) => {
const shape = this.shapeMap[id];
if (!this.cacheHiddenShape[id]) shape.show();
});
}
this.visible = true;
} }
/** Hides the item. */ /** Hides the item. */
public hide(animate = true) { public hide(animate = true) {
// TODO: utilize graphcore's view
this.stopAnimations();
const func = () => { const func = () => {
Object.keys(this.shapeMap).forEach((id) => { Object.keys(this.shapeMap).forEach((id) => {
const shape = this.shapeMap[id]; const shape = this.shapeMap[id];
@ -457,15 +465,22 @@ export default abstract class Item implements IItem {
shape.hide(); shape.hide();
}); });
}; };
const { animates = {} } = this.displayModel.data; Promise.all(this.stopAnimations()).then(() => {
if (animate && animates.hide?.length) { if (this.destroyed || !this.visible) return;
this.animations = this.runWithAnimates(animates, 'hide', undefined, func); const { animates = {} } = this.displayModel.data;
} else { if (animate && animates.hide?.length) {
// 2. clear group and remove group this.animations = this.runWithAnimates(
func(); animates,
} 'hide',
undefined,
this.visible = false; func,
);
} else {
// 2. clear group and remove group
func();
}
this.visible = false;
});
} }
/** Returns the visibility of the item. */ /** Returns the visibility of the item. */
@ -606,7 +621,11 @@ export default abstract class Item implements IItem {
* @param previousStates previous states * @param previousStates previous states
* @returns * @returns
*/ */
private drawWithStates(previousStates: State[], onfinish?: Function) { private drawWithStates(
previousStates: State[],
animate: boolean = true,
onfinish?: Function,
) {
const { default: _, ...themeStateStyles } = this.themeStyles; const { default: _, ...themeStateStyles } = this.themeStyles;
const { data: displayModelData } = this.displayModel; const { data: displayModelData } = this.displayModel;
let styles = {}; // merged styles let styles = {}; // merged styles
@ -675,6 +694,7 @@ export default abstract class Item implements IItem {
previous: previousStates, previous: previousStates,
current: this.states, current: this.states,
}, },
animate,
onfinish, onfinish,
); );
} }
@ -729,8 +749,27 @@ export default abstract class Item implements IItem {
* Stop all the animations on the item. * Stop all the animations on the item.
*/ */
public stopAnimations() { public stopAnimations() {
this.animations?.forEach(stopAnimate); let promises = [];
this.animations = []; if (this.animations?.length) {
while (this.animations.length) {
const animate = this.animations.pop();
if (animate.playState !== 'running') break;
promises.push(stopAnimate(animate));
// @ts-ignore
animate.onManualCancel?.();
}
}
if (this.groupAnimations?.length) {
while (this.groupAnimations.length) {
const groupAnimate = this.groupAnimations.pop();
if (groupAnimate.playState !== 'running') break;
promises.push(stopAnimate(groupAnimate));
// @ts-ignore
groupAnimate.onManualCancel?.();
}
this.groupAnimations = [];
}
return promises;
} }
/** /**

View File

@ -45,6 +45,7 @@ export default class Node extends Item {
this.displayModel as NodeDisplayModel | ComboDisplayModel, this.displayModel as NodeDisplayModel | ComboDisplayModel,
undefined, undefined,
undefined, undefined,
!this.displayModel.data.disableAnimate,
props.onfinish, props.onfinish,
); );
} }
@ -55,6 +56,7 @@ export default class Node extends Item {
current: NodeModelData | ComboModelData; current: NodeModelData | ComboModelData;
}, },
diffState?: { previous: State[]; current: State[] }, diffState?: { previous: State[]; current: State[] },
animate: boolean = true,
onfinish: Function = () => {}, onfinish: Function = () => {},
) { ) {
const { group, renderExt, shapeMap: prevShapeMap, model } = this; const { group, renderExt, shapeMap: prevShapeMap, model } = this;
@ -78,13 +80,13 @@ export default class Node extends Item {
} else { } else {
// terminate previous animations // terminate previous animations
this.stopAnimations(); this.stopAnimations();
this.updatePosition(displayModel, diffData, onfinish); this.updatePosition(displayModel, diffData, animate, onfinish);
} }
const { haloShape } = this.shapeMap; const { haloShape } = this.shapeMap;
haloShape?.toBack(); haloShape?.toBack();
super.draw(displayModel, diffData, diffState, onfinish); super.draw(displayModel, diffData, diffState, animate, onfinish);
this.anchorPointsCache = undefined; this.anchorPointsCache = undefined;
renderExt.updateCache(this.shapeMap); renderExt.updateCache(this.shapeMap);
@ -94,7 +96,7 @@ export default class Node extends Item {
} }
// handle shape's and group's animate // handle shape's and group's animate
if (!disableAnimate && animates) { if (animate && !disableAnimate && animates) {
const animatesExcludePosition = getAnimatesExcludePosition(animates); const animatesExcludePosition = getAnimatesExcludePosition(animates);
this.animations = animateShapes( this.animations = animateShapes(
animatesExcludePosition, // animates animatesExcludePosition, // animates
@ -104,7 +106,7 @@ export default class Node extends Item {
firstRendering ? 'buildIn' : 'update', firstRendering ? 'buildIn' : 'update',
this.changedStates, this.changedStates,
this.animateFrameListener, this.animateFrameListener,
() => onfinish(model.id), (canceled) => onfinish(model.id, canceled),
); );
} }
} }
@ -121,9 +123,18 @@ export default class Node extends Item {
lodStrategy: LodStrategyObj; lodStrategy: LodStrategyObj;
}, },
onlyMove?: boolean, onlyMove?: boolean,
animate?: boolean,
onfinish?: Function, onfinish?: Function,
) { ) {
super.update(model, diffData, isReplace, theme, onlyMove, onfinish); super.update(
model,
diffData,
isReplace,
theme,
onlyMove,
animate,
onfinish,
);
} }
/** /**
@ -136,19 +147,20 @@ export default class Node extends Item {
previous: NodeModelData | ComboModelData; previous: NodeModelData | ComboModelData;
current: NodeModelData | ComboModelData; current: NodeModelData | ComboModelData;
}, },
animate?: boolean,
onfinish: Function = () => {}, onfinish: Function = () => {},
) { ) {
const { group } = this; const { group } = this;
const { x, y, z = 0, animates, disableAnimate } = displayModel.data; const { x, y, z = 0, animates, disableAnimate } = displayModel.data;
if (isNaN(x as number) || isNaN(y as number) || isNaN(z)) return; if (isNaN(x as number) || isNaN(y as number) || isNaN(z as number)) return;
if (!disableAnimate && animates?.update) { if (animate && !disableAnimate && animates?.update) {
const groupAnimates = animates.update.filter( const groupAnimates = animates.update.filter(
({ shapeId, fields = [] }) => ({ shapeId, fields = [] }) =>
(!shapeId || shapeId === 'group') && (!shapeId || shapeId === 'group') &&
(fields.includes('x') || fields.includes('y')), (fields.includes('x') || fields.includes('y')),
); );
if (groupAnimates.length) { if (groupAnimates.length) {
animateShapes( const animations = animateShapes(
{ update: groupAnimates }, { update: groupAnimates },
{ group: { x, y, z } } as any, // targetStylesMap { group: { x, y, z } } as any, // targetStylesMap
this.shapeMap, // shapeMap this.shapeMap, // shapeMap
@ -156,12 +168,14 @@ export default class Node extends Item {
'update', 'update',
[], [],
this.animateFrameListener, this.animateFrameListener,
() => onfinish(displayModel.id), (canceled) => onfinish(displayModel.id, canceled),
); );
this.groupAnimations = animations;
return; return;
} }
} }
group.setLocalPosition(x, y, z); group.setLocalPosition(x, y, z);
onfinish(displayModel.id, !animate);
} }
public clone( public clone(
@ -294,6 +308,12 @@ export default class Node extends Item {
} }
public getPosition(): Point { public getPosition(): Point {
const initiated =
this.shapeMap.keyShape && this.group.attributes.x !== undefined;
if (initiated) {
const { center } = this.shapeMap.keyShape.getRenderBounds();
return { x: center[0], y: center[1], z: center[2] };
}
const { x = 0, y = 0, z = 0 } = this.model.data; const { x = 0, y = 0, z = 0 } = this.model.data;
return { x: x as number, y: y as number, z: z as number }; return { x: x as number, y: y as number, z: z as number };
} }

View File

@ -1,16 +1,16 @@
import { Graph as GraphLib, GraphView, ID } from '@antv/graphlib'; import { Graph as GraphLib, GraphView, ID } from '@antv/graphlib';
import { import { clone, isArray, isObject } from '@antv/util';
clone,
isArray,
isFunction,
isNumber,
isObject,
isString,
} from '@antv/util';
import { registery as registry } from '../../stdlib'; import { registery as registry } from '../../stdlib';
import { ComboModel, ComboUserModel, GraphData, IGraph } from '../../types'; import { ComboModel, ComboUserModel, GraphData, IGraph } from '../../types';
import { ComboUserModelData } from '../../types/combo'; import { ComboUserModelData } from '../../types/combo';
import { DataChangeType, GraphCore } from '../../types/data'; import {
DataChangeType,
DataConfig,
FetchDataConfig,
GraphCore,
InlineGraphDataConfig,
InlineTreeDataConfig,
} from '../../types/data';
import { import {
EdgeModel, EdgeModel,
EdgeModelData, EdgeModelData,
@ -28,9 +28,10 @@ import { getExtension } from '../../util/extension';
import { import {
deconstructData, deconstructData,
graphComboTreeDfs, graphComboTreeDfs,
graphCoreTreeDfs, graphData2TreeData,
isSucceed, treeData2GraphData,
validateComboStrucutre, validateComboStrucutre,
traverse,
} from '../../util/data'; } from '../../util/data';
/** /**
@ -49,8 +50,6 @@ export class DataController {
*/ */
public graphCore: GraphCore; public graphCore: GraphCore;
private comboTreeView: GraphView<any, any>;
constructor(graph: IGraph<any, any>) { constructor(graph: IGraph<any, any>) {
this.graph = graph; this.graph = graph;
this.tap(); this.tap();
@ -142,6 +141,9 @@ export class DataController {
private tap() { private tap() {
this.extensions = this.getExtensions(); this.extensions = this.getExtensions();
this.graph.hooks.datachange.tap(this.onDataChange.bind(this)); this.graph.hooks.datachange.tap(this.onDataChange.bind(this));
this.graph.hooks.treecollapseexpand.tap(
this.onTreeCollapseExpand.bind(this),
);
} }
/** /**
@ -161,24 +163,24 @@ export class DataController {
* Listener of graph's datachange hook. * Listener of graph's datachange hook.
* @param param contains new graph data and type of data change * @param param contains new graph data and type of data change
*/ */
private onDataChange(param: { data: GraphData; type: DataChangeType }) { private onDataChange(param: { data: DataConfig; type: DataChangeType }) {
const { data, type: changeType } = param; const { data, type: changeType } = param;
const change = () => { const change = () => {
switch (changeType) { switch (changeType) {
case 'remove': case 'remove':
this.removeData(data); this.removeData(data as GraphData);
break; break;
case 'update': case 'update':
this.updateData(data); this.updateData(data as GraphData);
break; break;
case 'moveCombo': case 'moveCombo':
this.moveCombo(data); this.moveCombo(data as GraphData);
break; break;
case 'addCombo': case 'addCombo':
this.addCombo(data); this.addCombo(data as GraphData);
break; break;
default: default:
// 'replace' | 'mergeReplace' | 'union' // changeType is 'replace' | 'mergeReplace' | 'union'
this.changeData(data, changeType); this.changeData(data, changeType);
break; break;
} }
@ -191,16 +193,30 @@ export class DataController {
} }
} }
private onTreeCollapseExpand(params: {
ids: ID[];
action: 'collapse' | 'expand';
}) {
const { ids, action } = params;
ids.forEach((id) => {
this.userGraphCore.mergeNodeData(id, {
collapsed: action === 'collapse',
});
});
}
/** /**
* Change data by replace, merge repalce, or union. * Change data by replace, merge repalce, or union.
* @param data new data * @param data new data
* @param changeType type of data change, 'replace' means discard the old data. 'mergeReplace' means merge the common part. 'union' means merge whole sets of old and new one * @param changeType type of data change, 'replace' means discard the old data. 'mergeReplace' means merge the common part. 'union' means merge whole sets of old and new one
*/ */
private changeData( private changeData(
data: GraphData, dataConfig: DataConfig,
changeType: 'replace' | 'mergeReplace' | 'union', changeType: 'replace' | 'mergeReplace' | 'union',
) { ) {
const { userGraphCore } = this; const { type: dataType, data } = this.formatData(dataConfig) || {};
if (!dataType) return;
if (changeType === 'replace') { if (changeType === 'replace') {
this.userGraphCore = new GraphLib<NodeUserModelData, EdgeUserModelData>({ this.userGraphCore = new GraphLib<NodeUserModelData, EdgeUserModelData>({
nodes: data.nodes.concat( nodes: data.nodes.concat(
@ -224,10 +240,6 @@ export class DataController {
edges, edges,
}); });
if (combos?.length) { if (combos?.length) {
this.comboTreeView = new GraphView<NodeModelData, EdgeModelData>({
graph: this.graphCore,
cache: 'manual',
});
this.graphCore.attachTreeStructure('combo'); this.graphCore.attachTreeStructure('combo');
nodes.forEach((node) => { nodes.forEach((node) => {
if (node.data.parentId) { if (node.data.parentId) {
@ -257,9 +269,10 @@ export class DataController {
}); });
} }
} else { } else {
const { userGraphCore } = this;
const prevData = deconstructData({ const prevData = deconstructData({
nodes: userGraphCore.getAllNodes(), nodes: userGraphCore.getAllNodes(),
edges: userGraphCore.getAllEdges(), edges: [],
}); });
const { nodes = [], edges = [], combos = [] } = data; const { nodes = [], edges = [], combos = [] } = data;
const nodesAndCombos = nodes.concat( const nodesAndCombos = nodes.concat(
@ -294,6 +307,7 @@ export class DataController {
} }
// =========== edge ============ // =========== edge ============
prevData.edges = userGraphCore.getAllEdges();
if (!prevData.edges.length) { if (!prevData.edges.length) {
userGraphCore.addEdges(edges); userGraphCore.addEdges(edges);
} else { } else {
@ -317,6 +331,15 @@ export class DataController {
}); });
} }
} }
if (data.edges?.length) {
const { userGraphCore } = this;
// convert and store tree structure to graphCore
this.updateTreeGraph(dataType, {
nodes: userGraphCore.getAllNodes(),
edges: userGraphCore.getAllEdges(),
});
}
} }
/** /**
@ -331,12 +354,14 @@ export class DataController {
const prevEdges = userGraphCore.getAllEdges(); const prevEdges = userGraphCore.getAllEdges();
if (prevNodesAndCombos.length && nodesAndCombos.length) { if (prevNodesAndCombos.length && nodesAndCombos.length) {
// update the parentId // update the parentId
nodesAndCombos.forEach((item) => { if (this.graphCore.hasTreeStructure('combo')) {
const { parentId } = item.data; nodesAndCombos.forEach((item) => {
this.graphCore.getChildren(item.id, 'combo').forEach((child) => { const { parentId } = item.data;
userGraphCore.mergeNodeData(child.id, { parentId }); this.graphCore.getChildren(item.id, 'combo').forEach((child) => {
userGraphCore.mergeNodeData(child.id, { parentId });
});
}); });
}); }
// remove the node // remove the node
userGraphCore.removeNodes(nodesAndCombos.map((node) => node.id)); userGraphCore.removeNodes(nodesAndCombos.map((node) => node.id));
} }
@ -353,9 +378,11 @@ export class DataController {
* Update part of old data. * Update part of old data.
* @param data data to be updated which is part of old one * @param data data to be updated which is part of old one
*/ */
private updateData(data: GraphData) { private updateData(dataConfig: DataConfig) {
const { userGraphCore } = this; const { userGraphCore } = this;
const { nodes = [], edges = [], combos = [] } = data; const { type: dataType, data } = this.formatData(dataConfig);
if (!dataType) return;
const { nodes = [], edges = [], combos = [] } = data as GraphData;
const { const {
nodes: prevNodes, nodes: prevNodes,
edges: prevEdges, edges: prevEdges,
@ -453,6 +480,38 @@ export class DataController {
userGraphCore.mergeNodeData(id, data); userGraphCore.mergeNodeData(id, data);
}); });
} }
if (edges.length) {
// convert and store tree structure to graphCore
this.updateTreeGraph(dataType, {
nodes: this.userGraphCore.getAllNodes(),
edges: this.userGraphCore.getAllEdges(),
});
}
}
private formatData(dataConfig: DataConfig): {
data: GraphData;
type: 'graphData' | 'treeData' | 'fetch';
} {
const { type, value } = dataConfig as
| InlineGraphDataConfig
| InlineTreeDataConfig
| FetchDataConfig;
let data = value;
if (!type) {
data = dataConfig as GraphData;
} else if (type === 'treeData') {
data = treeData2GraphData(value);
} else if (type === 'fetch') {
// TODO: fetch
} else {
console.warn(
'Input data type is invalid, the type shuold be "graphData", "treeData", or "fetch".',
);
return;
}
return { type: type || 'graphData', data: data as GraphData };
} }
/** /**
@ -592,9 +651,22 @@ export class DataController {
const changeMap: { const changeMap: {
[id: string]: boolean; [id: string]: boolean;
} = {}; } = {};
const treeChanges = [];
event.changes.forEach((change) => { event.changes.forEach((change) => {
const { value, id } = change; const id = change.id || change.value?.id;
changeMap[id || value.id] = true; if (id !== undefined) {
changeMap[id] = true;
return;
}
if (
[
'TreeStructureAttached',
'TreeStructureChanged',
'TreeStructureChanged',
].includes(change.type)
) {
treeChanges.push(change);
}
}); });
nodes.forEach((model) => { nodes.forEach((model) => {
newModelMap[model.id] = { type: 'node', model }; newModelMap[model.id] = { type: 'node', model };
@ -633,6 +705,7 @@ export class DataController {
const { model: newModel } = newModelMap[id] || {}; const { model: newModel } = newModelMap[id] || {};
// remove // remove
if (!newModel) { if (!newModel) {
// remove a combo, put the children to upper parent
if (prevModel.data._isCombo) { if (prevModel.data._isCombo) {
graphCore.getChildren(id, 'combo').forEach((child) => { graphCore.getChildren(id, 'combo').forEach((child) => {
parentMap[child.id] = { parentMap[child.id] = {
@ -641,9 +714,43 @@ export class DataController {
}; };
}); });
} }
// if it has combo parent, remove it from the parent's children list
if (prevModel.data.parentId) { if (prevModel.data.parentId) {
graphCore.setParent(id, undefined, 'combo'); graphCore.setParent(id, undefined, 'combo');
} }
// for tree graph view, show the succeed nodes and edges
const succeedIds = [];
graphCore.dfsTree(
id,
(child) => {
succeedIds.push(child.id);
},
'tree',
);
const succeedEdgeIds = graphCore
.getAllEdges()
.filter(
({ source, target }) =>
succeedIds.includes(source) && succeedIds.includes(target),
)
.map((edge) => edge.id);
this.graph.showItem(
succeedIds
.filter((succeedId) => succeedId !== id)
.concat(succeedEdgeIds),
);
// for tree graph view, remove the node from the parent's children list
graphCore.setParent(id, undefined, 'tree');
// for tree graph view, make the its children to be roots
graphCore
.getChildren(id, 'tree')
.forEach((child) =>
graphCore.setParent(child.id, undefined, 'tree'),
);
// remove the node data
graphCore.removeNode(id); graphCore.removeNode(id);
delete parentMap[prevModel.id]; delete parentMap[prevModel.id];
} }
@ -682,6 +789,21 @@ export class DataController {
graphCore.mergeNodeData(id, { parentId: parentMap[id].new }); graphCore.mergeNodeData(id, { parentId: parentMap[id].new });
graphCore.setParent(id, parentMap[id].new, 'combo'); graphCore.setParent(id, parentMap[id].new, 'combo');
}); });
// update tree structure
treeChanges.forEach((change) => {
const { type, treeKey, nodeId, newParentId } = change;
if (type === 'TreeStructureAttached') {
graphCore.attachTreeStructure(treeKey);
return;
} else if (type === 'TreeStructureChanged') {
graphCore.setParent(nodeId, newParentId, treeKey);
return;
} else if (type === 'TreeStructureDetached') {
graphCore.detachTreeStructure(treeKey);
return;
}
});
} else { } else {
// situation 2: idMaps is complete // situation 2: idMaps is complete
// calculate the final idMap which maps the ids from final transformed data to their comes from ids in userData // calculate the final idMap which maps the ids from final transformed data to their comes from ids in userData
@ -704,15 +826,17 @@ export class DataController {
const { changes } = event; const { changes } = event;
changes.forEach((change) => { changes.forEach((change) => {
const { value, id, type } = change; const { value, id, type } = change;
// TODO: temporary skip. how to handle tree change events? if (type === 'TreeStructureAttached') {
if ( graphCore.attachTreeStructure(change.treeKey);
[
'TreeStructureAttached',
'TreeStructureDetached',
'TreeStructureChanged',
].includes(type)
)
return; return;
} else if (type === 'TreeStructureChanged') {
const { newParentId, nodeId, treeKey } = change;
graphCore.setParent(nodeId, newParentId, treeKey);
return;
} else if (type === 'TreeStructureDetached') {
graphCore.detachTreeStructure(change.treeKey);
return;
}
const dataId = id || value.id; const dataId = id || value.id;
changeMap[dataId] = changeMap[dataId] || []; changeMap[dataId] = changeMap[dataId] || [];
changeMap[dataId].push(type.toLawerCase()); changeMap[dataId].push(type.toLawerCase());
@ -727,15 +851,14 @@ export class DataController {
const oldValue = prevModelMap[newId]; const oldValue = prevModelMap[newId];
const isNodeOrCombo = graphCore.hasNode(newId); const isNodeOrCombo = graphCore.hasNode(newId);
if (newValue && !oldValue) { if (newValue && !oldValue) {
const addFunc = isNodeOrCombo isNodeOrCombo
? graphCore.addNode ? graphCore.addNode(newValue)
: graphCore.addEdge; : graphCore.addEdge(newValue);
addFunc(newValue);
} else if (!newValue && oldValue) { } else if (!newValue && oldValue) {
const removeFunc = isNodeOrCombo isNodeOrCombo
? graphCore.removeNode ? graphCore.removeNode(newId)
: graphCore.removeEdge; : graphCore.removeEdge(newId);
removeFunc(newId); // TODO: update combo tree and tree graph
} else { } else {
if (!comesFromIds?.length) { if (!comesFromIds?.length) {
// no comesForm, find same id in userGraphCore to follow the change, if it not found, diff new and old data value of graphCore (inner data) // no comesForm, find same id in userGraphCore to follow the change, if it not found, diff new and old data value of graphCore (inner data)
@ -748,11 +871,9 @@ export class DataController {
isNodeOrCombo, isNodeOrCombo,
diff, diff,
); );
} else { } else if (changeMap[comesFromIds[0]]?.length) {
// follow the corresponding data event in userGraphCore // follow the corresponding data event in userGraphCore
const comesFromChanges = changeMap[comesFromIds[0]]; syncUpdateToGraphCore(newId, newValue, oldValue, isNodeOrCombo);
if (comesFromChanges?.length)
syncUpdateToGraphCore(newId, newValue, oldValue, isNodeOrCombo);
} }
} }
}); });
@ -771,7 +892,6 @@ export class DataController {
graphCore.setParent(node.id, node.data.parentId as ID, 'combo'); graphCore.setParent(node.id, node.data.parentId as ID, 'combo');
}); });
} }
this.comboTreeView?.refreshCache();
}); });
} }
@ -798,6 +918,34 @@ export class DataController {
}); });
return { data: dataCloned, idMaps }; return { data: dataCloned, idMaps };
} }
/**
* convert and store tree structure to graphCore
* @param dataType
* @param data
*/
private updateTreeGraph(dataType, data) {
this.userGraphCore.attachTreeStructure('tree');
if (dataType === 'treeData') {
// tree structure storing
data.edges.forEach((edge) => {
const { source, target } = edge;
this.userGraphCore.setParent(target, source, 'tree');
});
} else {
// graph data to tree structure and storing
const rootIds = data.nodes
.filter((node) => node.data.isRoot)
.map((node) => node.id);
graphData2TreeData({}, data, rootIds).forEach((tree) => {
traverse(tree, (node) => {
node.children?.forEach((child) => {
this.userGraphCore.setParent(child.id, node.id, 'tree');
});
});
});
}
}
} }
/** /**

View File

@ -126,8 +126,6 @@ export class ItemController {
[id: string]: Node | Edge | Combo | Group; [id: string]: Node | Edge | Combo | Group;
} = {}; } = {};
// private comboTreeView: GraphView<any, any>;
constructor(graph: IGraph<any, any>) { constructor(graph: IGraph<any, any>) {
this.graph = graph; this.graph = graph;
// get mapper for node / edge / combo // get mapper for node / edge / combo
@ -168,6 +166,9 @@ export class ItemController {
this.graph.hooks.transientupdate.tap(this.onTransientUpdate.bind(this)); this.graph.hooks.transientupdate.tap(this.onTransientUpdate.bind(this));
this.graph.hooks.viewportchange.tap(this.onViewportChange.bind(this)); this.graph.hooks.viewportchange.tap(this.onViewportChange.bind(this));
this.graph.hooks.themechange.tap(this.onThemeChange.bind(this)); this.graph.hooks.themechange.tap(this.onThemeChange.bind(this));
this.graph.hooks.treecollapseexpand.tap(
this.onTreeCollapseExpand.bind(this),
);
this.graph.hooks.destroy.tap(this.onDestroy.bind(this)); this.graph.hooks.destroy.tap(this.onDestroy.bind(this));
} }
@ -258,6 +259,7 @@ export class ItemController {
this.renderCombos(combos, theme.combo, graphCore); this.renderCombos(combos, theme.combo, graphCore);
this.renderEdges(edges, theme.edge); this.renderEdges(edges, theme.edge);
this.sortByComboTree(graphCore); this.sortByComboTree(graphCore);
// collapse the combos which has 'collapsed' in initial data
if (graphCore.hasTreeStructure('combo')) { if (graphCore.hasTreeStructure('combo')) {
graphCoreTreeDfs( graphCoreTreeDfs(
graphCore, graphCore,
@ -266,8 +268,21 @@ export class ItemController {
if (child.data.collapsed) this.collapseCombo(graphCore, child); if (child.data.collapsed) this.collapseCombo(graphCore, child);
}, },
'BT', 'BT',
'combo',
); );
} }
// collapse the sub tree which has 'collapsed' in initial data
const collapseNodes = [];
graphCoreTreeDfs(
graphCore,
graphCore.getRoots('tree'),
(child) => {
if (child.data.collapsed) collapseNodes.push(child);
},
'BT',
'tree',
);
this.collapseSubTree(collapseNodes, graphCore, false);
} }
/** /**
@ -280,14 +295,21 @@ export class ItemController {
graphCore: GraphCore; graphCore: GraphCore;
theme: ThemeSpecification; theme: ThemeSpecification;
upsertAncestors?: boolean; upsertAncestors?: boolean;
animate?: boolean;
action?: 'updatePosition'; action?: 'updatePosition';
callback?: (
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void;
}) { }) {
const { const {
changes, changes,
graphCore, graphCore,
action, action,
animate = true,
upsertAncestors = true, upsertAncestors = true,
theme = {}, theme = {},
callback = () => {},
} = param; } = param;
const groupedChanges = getGroupedChanges(graphCore, changes); const groupedChanges = getGroupedChanges(graphCore, changes);
const { itemMap } = this; const { itemMap } = this;
@ -298,10 +320,9 @@ export class ItemController {
({ value }) => { ({ value }) => {
const { id } = value; const { id } = value;
const item = itemMap[id]; const item = itemMap[id];
if (item) { if (!item) return;
item.destroy(); item.destroy();
delete itemMap[id]; delete itemMap[id];
}
}, },
); );
@ -381,8 +402,16 @@ export class ItemController {
const onlyMove = action === 'updatePosition'; const onlyMove = action === 'updatePosition';
const item = itemMap[id] as Node | Combo; const item = itemMap[id] as Node | Combo;
const type = item.getType(); const type = item.getType();
if (onlyMove && type === 'node' && isNaN(current.x) && isNaN(current.y)) const innerModel = graphCore.getNode(id);
if (
onlyMove &&
type === 'node' &&
isNaN(current.x) &&
isNaN(current.y)
) {
callback(innerModel, true);
return; return;
}
// update the theme if the dataType value is changed // update the theme if the dataType value is changed
let itemTheme; let itemTheme;
if ( if (
@ -396,7 +425,6 @@ export class ItemController {
nodeTheme, nodeTheme,
); );
} }
const innerModel = graphCore.getNode(id);
const relatedEdgeInnerModels = graphCore.getRelatedEdges(id); const relatedEdgeInnerModels = graphCore.getRelatedEdges(id);
const nodeRelatedIdsToUpdate: ID[] = []; const nodeRelatedIdsToUpdate: ID[] = [];
relatedEdgeInnerModels.forEach((edge) => { relatedEdgeInnerModels.forEach((edge) => {
@ -415,11 +443,13 @@ export class ItemController {
isReplace, isReplace,
itemTheme, itemTheme,
onlyMove, onlyMove,
animate,
// call after updating finished // call after updating finished
() => { (_, canceled) => {
item.onframe(); item.onframe?.();
// @ts-ignore // @ts-ignore
item.onframe = undefined; item.onframe = undefined;
callback(innerModel, canceled);
}, },
); );
@ -476,7 +506,7 @@ export class ItemController {
this.graph.hideItem(innerModel.id); this.graph.hideItem(innerModel.id);
} }
}); });
updateRelates(); updateRelatesThrottle();
} }
// === 6. update edges' data === // === 6. update edges' data ===
@ -513,7 +543,15 @@ export class ItemController {
} }
const item = itemMap[id]; const item = itemMap[id];
const innerModel = graphCore.getEdge(id); const innerModel = graphCore.getEdge(id);
item.update(innerModel, { current, previous }, isReplace, itemTheme); item.update(
innerModel,
{ current, previous },
isReplace,
itemTheme,
undefined,
animate,
(_, canceled) => callback(innerModel, canceled),
);
}); });
} }
@ -538,9 +576,25 @@ export class ItemController {
} }
// === 8. combo tree structure change, resort the shapes === // === 8. combo tree structure change, resort the shapes ===
if (groupedChanges.TreeStructureChanged.length) { if (groupedChanges.ComboStructureChanged.length) {
this.sortByComboTree(graphCore); this.sortByComboTree(graphCore);
} }
// === 9. tree data structure change, hide the new node and edge while one of the ancestor is collapsed ===
if (groupedChanges.TreeStructureChanged.length) {
groupedChanges.TreeStructureChanged.forEach((change) => {
const { nodeId } = change;
// hide it when an ancestor is collapsed
let parent = graphCore.getParent(nodeId, 'tree');
while (parent) {
if (parent.data.collapsed) {
this.graph.hideItem(nodeId, true);
break;
}
parent = graphCore.getParent(parent.id, 'tree');
}
});
}
} }
/** /**
@ -593,21 +647,20 @@ export class ItemController {
if (type === 'edge') { if (type === 'edge') {
item.show(animate); item.show(animate);
} else { } else {
let anccestorCollapsed = false;
if (graphCore.hasTreeStructure('combo')) { if (graphCore.hasTreeStructure('combo')) {
let anccestorCollapsed = false;
traverseAncestors(graphCore, [item.model], (model) => { traverseAncestors(graphCore, [item.model], (model) => {
if (model.data.collapsed) anccestorCollapsed = true; if (model.data.collapsed) anccestorCollapsed = true;
return anccestorCollapsed; return anccestorCollapsed;
}); });
if (anccestorCollapsed) return; if (anccestorCollapsed) return;
} }
const relatedEdges = graphCore.getRelatedEdges(id); const relatedEdges = graphCore.getRelatedEdges(id);
item.show(animate); item.show(animate);
relatedEdges.forEach(({ id: edgeId, source, target }) => { relatedEdges.forEach(({ id: edgeId, source, target }) => {
if (this.getItemVisible(source) && this.getItemVisible(target)) if (this.getItemVisible(source) && this.getItemVisible(target))
this.itemMap[edgeId]?.show(); this.itemMap[edgeId]?.show(animate);
}); });
} }
} else { } else {
@ -615,7 +668,7 @@ export class ItemController {
if (type !== 'edge') { if (type !== 'edge') {
const relatedEdges = graphCore.getRelatedEdges(id); const relatedEdges = graphCore.getRelatedEdges(id);
relatedEdges.forEach(({ id: edgeId }) => { relatedEdges.forEach(({ id: edgeId }) => {
this.itemMap[edgeId]?.hide(); this.itemMap[edgeId]?.hide(animate);
}); });
} }
} }
@ -979,11 +1032,13 @@ export class ItemController {
console.warn( console.warn(
`The source node ${source} is not exist in the graph for edge ${id}, please add the node first`, `The source node ${source} is not exist in the graph for edge ${id}, please add the node first`,
); );
return;
} }
if (!targetItem) { if (!targetItem) {
console.warn( console.warn(
`The source node ${source} is not exist in the graph for edge ${id}, please add the node first`, `The source node ${source} is not exist in the graph for edge ${id}, please add the node first`,
); );
return;
} }
// get the base styles from theme // get the base styles from theme
let dataType; let dataType;
@ -1164,6 +1219,134 @@ export class ItemController {
// remove related virtual edges // remove related virtual edges
this.graph.removeData('edge', uniq(relatedVirtualEdgeIds)); this.graph.removeData('edge', uniq(relatedVirtualEdgeIds));
} }
/**
* Collapse or expand a sub tree according to action
* @param params
*/
private onTreeCollapseExpand(params: {
ids: ID[];
animate: boolean;
action: 'collapse' | 'expand';
graphCore: GraphCore;
}) {
const { ids, animate, action, graphCore } = params;
const rootModels = ids.map((id) => graphCore.getNode(id));
switch (action) {
case 'collapse':
this.collapseSubTree(rootModels, graphCore, animate);
break;
case 'expand':
default:
this.expandSubTree(rootModels, graphCore, animate);
break;
}
}
/**
* Collapse sub tree(s).
* @param rootModels The root node models of sub trees
* @param graphCore
* @param animate Whether enable animations for expanding, true by default
* @returns
*/
private collapseSubTree(
rootModels: NodeModel[],
graphCore: GraphCore,
animate: boolean = true,
) {
let positions = [];
rootModels.forEach((root) => {
let shouldCollapse = true;
const nodes = [];
graphCoreTreeDfs(
graphCore,
[root],
(node) => {
if (node.id === root.id) return;
const neighbors = graphCore.getNeighbors(node.id);
if (
neighbors.length > 2 ||
(!graphCore.getChildren(node.id, 'tree')?.length &&
neighbors.length > 1)
) {
shouldCollapse = false;
}
nodes.push(node);
},
'TB',
'tree',
{
stopAllFn: () => !shouldCollapse,
},
);
if (shouldCollapse) {
positions = positions.concat(
nodes.map((node) => ({
id: node.id,
data: { x: root.data.x, y: root.data.y },
})),
);
}
});
if (!positions.length) return;
this.graph.updateNodePosition(
positions,
undefined,
!animate,
(model, canceled) => {
this.graph.hideItem(model.id, canceled);
},
undefined,
);
}
/**
* Expand sub tree(s).
* @param rootModels The root node models of sub trees.
* @param graphCore
* @param animate Whether enable animations for expanding, true by default.
* @returns
*/
private expandSubTree(
rootModels: NodeModel[],
graphCore: GraphCore,
animate: boolean = true,
) {
let allNodeIds = [];
let allEdgeIds = [];
rootModels.forEach((root) => {
const nodeIds = [];
graphCoreTreeDfs(
graphCore,
[root],
(node) => nodeIds.push(node.id),
'TB',
'tree',
{
stopBranchFn: (node) => {
const shouldStop =
node.id !== root.id && (node.data.collapsed as boolean);
if (shouldStop) nodeIds.push(node.id);
return shouldStop;
},
},
);
allEdgeIds = allEdgeIds.concat(
graphCore
.getAllEdges()
.filter(
(edge) =>
nodeIds.includes(edge.source) && nodeIds.includes(edge.target),
)
.map((edge) => edge.id),
);
allNodeIds = allNodeIds.concat(nodeIds.filter((id) => id !== root.id));
});
const ids = uniq(allNodeIds.concat(allEdgeIds));
this.graph.showItem(ids, !animate);
this.graph.layout(undefined, !animate);
}
} }
const getItemTheme = ( const getItemTheme = (

View File

@ -1,4 +1,5 @@
import { Animation, DisplayObject, IAnimationEffectTiming } from '@antv/g'; import { Animation, DisplayObject, IAnimationEffectTiming } from '@antv/g';
import Hierarchy from '@antv/hierarchy';
import { Graph as GraphLib } from '@antv/graphlib'; import { Graph as GraphLib } from '@antv/graphlib';
import { import {
isLayoutWithIterations, isLayoutWithIterations,
@ -17,6 +18,7 @@ import {
} from '../../types'; } from '../../types';
import { GraphCore } from '../../types/data'; import { GraphCore } from '../../types/data';
import { EdgeModelData } from '../../types/edge'; import { EdgeModelData } from '../../types/edge';
import { layoutOneTree } from '../../util/layout';
/** /**
* Manages layout extensions and graph layout. * Manages layout extensions and graph layout.
@ -48,6 +50,7 @@ export class LayoutController {
private async onLayout(params: { private async onLayout(params: {
graphCore: GraphCore; graphCore: GraphCore;
options: LayoutOptions; options: LayoutOptions;
animate?: boolean;
}) { }) {
/** /**
* The final calculated result. * The final calculated result.
@ -57,13 +60,15 @@ export class LayoutController {
// Stop currentLayout if any. // Stop currentLayout if any.
this.stopLayout(); this.stopLayout();
const { graphCore, options } = params; const { graphCore, options, animate = true } = params;
const layoutNodes = graphCore const layoutNodes = graphCore
.getAllNodes() .getAllNodes()
.filter((node) => node.data.visible !== false && !node.data._isCombo); .filter(
(node) => this.graph.getItemVisible(node.id) && !node.data._isCombo,
);
const layoutNodesIdMap = {}; const layoutNodesIdMap = {};
layoutNodes.forEach((node) => (layoutNodesIdMap[node.id] = true)); layoutNodes.forEach((node) => (layoutNodesIdMap[node.id] = true));
const layoutGraphCore = new GraphLib<NodeModelData, EdgeModelData>({ const layoutData = {
nodes: layoutNodes, nodes: layoutNodes,
edges: graphCore edges: graphCore
.getAllEdges() .getAllEdges()
@ -71,7 +76,10 @@ export class LayoutController {
(edge) => (edge) =>
layoutNodesIdMap[edge.source] && layoutNodesIdMap[edge.target], layoutNodesIdMap[edge.source] && layoutNodesIdMap[edge.target],
), ),
}); };
const layoutGraphCore = new GraphLib<NodeModelData, EdgeModelData>(
layoutData,
);
this.graph.emit('startlayout'); this.graph.emit('startlayout');
@ -112,6 +120,19 @@ export class LayoutController {
throw new Error(`Unknown layout algorithm: ${type}`); throw new Error(`Unknown layout algorithm: ${type}`);
} }
if (Hierarchy[type]) {
// tree layout type
await this.handleTreeLayout(
type,
options,
animationEffectTiming,
graphCore,
layoutData,
animate,
);
return;
}
// Initialize layout. // Initialize layout.
const layout = new layoutCtor(rest); const layout = new layoutCtor(rest);
this.currentLayout = layout; this.currentLayout = layout;
@ -170,7 +191,57 @@ export class LayoutController {
this.graph.emit('endlayout'); this.graph.emit('endlayout');
// Update nodes' positions. // Update nodes' positions.
this.updateNodesPosition(positions); this.updateNodesPosition(positions, animate);
}
async handleTreeLayout(
type,
options,
animationEffectTiming,
graphCore,
layoutData,
animate,
) {
const { animated = false, rootIds = [], begin = [0, 0] } = options;
const nodePositions = [];
const nodeMap = {};
// tree layout with tree data
const trees = graphCore
.getRoots('tree')
.filter(
(node) => !node.data._isCombo, // this.graph.getItemVisible(node.id) &&
)
.map((node) => ({ id: node.id, children: [] }));
trees.forEach((tree) => {
nodeMap[tree.id] = tree;
graphCore.dfsTree(
tree.id,
(node) => {
nodeMap[node.id].children = graphCore
.getChildren(node.id, 'tree')
.filter((node) => !node.data._isCombo)
.map((child) => {
nodeMap[child.id] = { id: child.id, children: [] };
return nodeMap[child.id];
});
},
'tree',
);
layoutOneTree(tree, type, options, nodeMap, nodePositions, begin);
});
if (animated) {
await this.animateLayoutWithoutIterations(
{ nodes: nodePositions, edges: [] },
animationEffectTiming,
);
}
this.graph.emit('endlayout');
this.updateNodesPosition(
{ nodes: nodePositions, edges: [] },
animated || animate,
);
return;
} }
stopLayout() { stopLayout() {
@ -202,8 +273,11 @@ export class LayoutController {
} }
} }
private updateNodesPosition(positions: LayoutMapping) { private updateNodesPosition(
this.graph.updateNodePosition(positions.nodes); positions: LayoutMapping,
animate: boolean = true,
) {
this.graph.updateNodePosition(positions.nodes, undefined, !animate);
} }
/** /**

View File

@ -14,7 +14,7 @@ import { CameraAnimationOptions } from '../types/animate';
import { BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior'; import { BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
import { ComboModel } from '../types/combo'; import { ComboModel } from '../types/combo';
import { Padding, Point } from '../types/common'; import { Padding, Point } from '../types/common';
import { DataChangeType, GraphCore } from '../types/data'; import { DataChangeType, DataConfig, GraphCore } from '../types/data';
import { EdgeModel, EdgeModelData } from '../types/edge'; import { EdgeModel, EdgeModelData } from '../types/edge';
import { Hooks, ViewportChangeHookParams } from '../types/hook'; import { Hooks, ViewportChangeHookParams } from '../types/hook';
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../types/item'; import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../types/item';
@ -250,6 +250,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
changes: GraphChange<NodeModelData, EdgeModelData>[]; changes: GraphChange<NodeModelData, EdgeModelData>[];
graphCore: GraphCore; graphCore: GraphCore;
theme: ThemeSpecification; theme: ThemeSpecification;
animate?: boolean;
upsertAncestors?: boolean; upsertAncestors?: boolean;
}>({ name: 'itemchange' }), }>({ name: 'itemchange' }),
render: new Hook<{ render: new Hook<{
@ -259,7 +260,11 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
}>({ }>({
name: 'render', name: 'render',
}), }),
layout: new Hook<{ graphCore: GraphCore }>({ name: 'layout' }), layout: new Hook<{
graphCore: GraphCore;
options?: LayoutOptions;
animate?: boolean;
}>({ name: 'layout' }),
viewportchange: new Hook<ViewportChangeHookParams>({ name: 'viewport' }), viewportchange: new Hook<ViewportChangeHookParams>({ name: 'viewport' }),
modechange: new Hook<{ mode: string }>({ name: 'modechange' }), modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
behaviorchange: new Hook<{ behaviorchange: new Hook<{
@ -309,6 +314,12 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
transient: Canvas; transient: Canvas;
}; };
}>({ name: 'init' }), }>({ name: 'init' }),
treecollapseexpand: new Hook<{
ids: ID[];
animate: boolean;
action: 'collapse' | 'expand';
graphCore: GraphCore;
}>({ name: 'treecollapseexpand' }),
destroy: new Hook<{}>({ name: 'destroy' }), destroy: new Hook<{}>({ name: 'destroy' }),
}; };
} }
@ -355,7 +366,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns * @returns
* @group Data * @group Data
*/ */
public async read(data: GraphData) { public async read(data: DataConfig) {
this.hooks.datachange.emit({ data, type: 'replace' }); this.hooks.datachange.emit({ data, type: 'replace' });
const emitRender = async () => { const emitRender = async () => {
this.hooks.render.emit({ this.hooks.render.emit({
@ -387,7 +398,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @group Data * @group Data
*/ */
public async changeData( public async changeData(
data: GraphData, data: DataConfig,
type: 'replace' | 'mergeReplace' = 'mergeReplace', type: 'replace' | 'mergeReplace' = 'mergeReplace',
) { ) {
this.hooks.datachange.emit({ data, type }); this.hooks.datachange.emit({ data, type });
@ -1028,9 +1039,21 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[] ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>, >,
upsertAncestors?: boolean, upsertAncestors?: boolean,
disableAnimate: boolean = false,
callback?: (
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean, stack?: boolean,
) { ) {
return this.updatePosition('node', models, upsertAncestors, stack); return this.updatePosition(
'node',
models,
upsertAncestors,
disableAnimate,
callback,
stack,
);
} }
/** /**
@ -1048,9 +1071,18 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[] ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>, >,
upsertAncestors?: boolean, upsertAncestors?: boolean,
disableAnimate: boolean = false,
callback?: (model: NodeModel | EdgeModel | ComboModel) => void,
stack?: boolean, stack?: boolean,
) { ) {
return this.updatePosition('combo', models, upsertAncestors, stack); return this.updatePosition(
'combo',
models,
upsertAncestors,
disableAnimate,
callback,
stack,
);
} }
private updatePosition( private updatePosition(
@ -1061,6 +1093,11 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[] ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>, >,
upsertAncestors?: boolean, upsertAncestors?: boolean,
disableAnimate: boolean = false,
callback?: (
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean, stack?: boolean,
) { ) {
const modelArr = isArray(models) ? models : [models]; const modelArr = isArray(models) ? models : [models];
@ -1075,6 +1112,8 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
theme: specification, theme: specification,
upsertAncestors, upsertAncestors,
action: 'updatePosition', action: 'updatePosition',
animate: !disableAnimate,
callback,
}); });
this.emit('afteritemchange', { this.emit('afteritemchange', {
type, type,
@ -1105,13 +1144,13 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns * @returns
* @group Item * @group Item
*/ */
public showItem(ids: ID | ID[], disableAniamte?: boolean) { public showItem(ids: ID | ID[], disableAnimate: boolean = false) {
const idArr = isArray(ids) ? ids : [ids]; const idArr = isArray(ids) ? ids : [ids];
this.hooks.itemvisibilitychange.emit({ this.hooks.itemvisibilitychange.emit({
ids: idArr as ID[], ids: idArr as ID[],
value: true, value: true,
graphCore: this.dataController.graphCore, graphCore: this.dataController.graphCore,
animate: !disableAniamte, animate: !disableAnimate,
}); });
} }
/** /**
@ -1120,13 +1159,13 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns * @returns
* @group Item * @group Item
*/ */
public hideItem(ids: ID | ID[], disableAniamte?: boolean) { public hideItem(ids: ID | ID[], disableAnimate: boolean = false) {
const idArr = isArray(ids) ? ids : [ids]; const idArr = isArray(ids) ? ids : [ids];
this.hooks.itemvisibilitychange.emit({ this.hooks.itemvisibilitychange.emit({
ids: idArr as ID[], ids: idArr as ID[],
value: false, value: false,
graphCore: this.dataController.graphCore, graphCore: this.dataController.graphCore,
animate: !disableAniamte, animate: !disableAnimate,
}); });
} }
@ -1364,7 +1403,10 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
/** /**
* Layout the graph (with current configurations if cfg is not assigned). * Layout the graph (with current configurations if cfg is not assigned).
*/ */
public async layout(options?: LayoutOptions) { public async layout(
options?: LayoutOptions,
disableAnimate: boolean = false,
) {
const { graphCore } = this.dataController; const { graphCore } = this.dataController;
const formattedOptions = { const formattedOptions = {
...this.getSpecification().layout, ...this.getSpecification().layout,
@ -1400,6 +1442,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
await this.hooks.layout.emitLinearAsync({ await this.hooks.layout.emitLinearAsync({
graphCore, graphCore,
options: formattedOptions, options: formattedOptions,
animate: !disableAnimate,
}); });
this.emit('afterlayout'); this.emit('afterlayout');
} }
@ -1653,6 +1696,47 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
return this.itemController.getTransient(String(id)); return this.itemController.getTransient(String(id));
} }
/**
* Collapse sub tree(s).
* @param ids Root id(s) of the sub trees.
* @param disableAnimate Whether disable the animations for this operation.
* @param stack Whether push this operation to stack.
* @returns
* @group Tree
*/
public collapse(
ids: ID | ID[],
disableAnimate: boolean = false,
stack?: boolean,
) {
this.hooks.treecollapseexpand.emit({
ids: isArray(ids) ? ids : [ids],
action: 'collapse',
animate: !disableAnimate,
graphCore: this.dataController.graphCore,
});
}
/**
* Expand sub tree(s).
* @param ids Root id(s) of the sub trees.
* @param disableAnimate Whether disable the animations for this operation.
* @param stack Whether push this operation to stack.
* @returns
* @group Tree
*/
public expand(
ids: ID | ID[],
disableAnimate: boolean = false,
stack?: boolean,
) {
this.hooks.treecollapseexpand.emit({
ids: isArray(ids) ? ids : [ids],
action: 'expand',
animate: !disableAnimate,
graphCore: this.dataController.graphCore,
});
}
/** /**
* Destroy the graph instance and remove the related canvases. * Destroy the graph instance and remove the related canvases.
* @returns * @returns

View File

@ -0,0 +1,106 @@
import { Behavior } from '../../types/behavior';
import { IG6GraphEvent } from '../../types/event';
const ALLOWED_TRIGGERS = ['click', 'dblclick'] as const;
type Trigger = (typeof ALLOWED_TRIGGERS)[number];
interface Options {
/**
* The key to pressed with mouse click to apply multiple selection.
* Defaults to `"shift"`.
* Could be "shift", "ctrl", "alt", or "meta".
*/
trigger: Trigger;
/**
* The event name to trigger when select/unselect an item.
*/
eventName: string;
/**
* Whether disable the collapse / expand animation triggered by this behavior.
*/
disableAnimate: boolean;
/**
* Whether allow the behavior happen on the current item.
*/
shouldBegin: (event: IG6GraphEvent) => boolean;
/**
* Whether to update item state.
* If it returns false, you may probably listen to `eventName` and
* manage states or data manually
*/
shouldUpdate: (event: IG6GraphEvent) => boolean;
}
const DEFAULT_OPTIONS: Options = {
trigger: 'click',
eventName: '',
disableAnimate: false,
shouldBegin: () => true,
shouldUpdate: () => true,
};
export default class CollapseExpandTree extends Behavior {
options: Options;
private timeout: NodeJS.Timeout = undefined;
constructor(options: Partial<Options>) {
super(Object.assign({}, DEFAULT_OPTIONS, options));
// Validate options
if (options.trigger && !ALLOWED_TRIGGERS.includes(options.trigger)) {
console.warn(
`G6: Invalid trigger option "${options.trigger}" for collapse-expand-tree behavior!`,
);
this.options.trigger = DEFAULT_OPTIONS.trigger;
}
}
getEvents = () => {
return this.options.trigger === 'dblclick'
? {
'node:dblclick': this.onClick,
}
: {
'node:click': this.onClickBesideDblClick,
};
};
public onClickBesideDblClick(event: IG6GraphEvent) {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
return;
}
this.timeout = setTimeout(() => {
this.timeout = undefined;
this.onClick(event);
}, 200);
}
public onClick(event: IG6GraphEvent) {
if (!this.options.shouldBegin(event)) return;
const { itemId, itemType } = event;
const { disableAnimate } = this.options;
const model = this.graph.getNodeData(itemId);
if (!model) {
console.warn(`Node with id ${itemId} is not exist`);
return;
}
let action = 'expand';
if (model.data.collapsed) {
this.graph.expand(itemId, disableAnimate);
} else {
this.graph.collapse(itemId, disableAnimate);
action = 'collapse';
}
// Emit an event.
if (this.options.eventName) {
this.graph.emit(this.options.eventName, {
action,
itemId,
itemType,
});
}
}
}

View File

@ -1,4 +1,5 @@
import { registry as layoutRegistry } from '@antv/layout'; import { registry as layoutRegistry } from '@antv/layout';
import Hierarchy from '@antv/hierarchy';
import { Lib } from '../types/stdlib'; import { Lib } from '../types/stdlib';
import ActivateRelations from './behavior/activate-relations'; import ActivateRelations from './behavior/activate-relations';
import BrushSelect from './behavior/brush-select'; import BrushSelect from './behavior/brush-select';
@ -30,6 +31,7 @@ import TrackCanvas3D from './behavior/track-canvas-3d';
import OrbitCanvas3D from './behavior/orbit-canvas-3d'; import OrbitCanvas3D from './behavior/orbit-canvas-3d';
import HoverActivate from './behavior/hover-activate'; import HoverActivate from './behavior/hover-activate';
import CollapseExpandCombo from './behavior/collapse-expand-combo'; import CollapseExpandCombo from './behavior/collapse-expand-combo';
import CollapseExpandTree from './behavior/collapse-expand-tree';
import { CubicEdge } from './item/edge/cubic'; import { CubicEdge } from './item/edge/cubic';
import { CubicHorizonEdge } from './item/edge/cubic-horizon'; import { CubicHorizonEdge } from './item/edge/cubic-horizon';
import { CubicVerticalEdge } from './item/edge/cubic-vertical'; import { CubicVerticalEdge } from './item/edge/cubic-vertical';
@ -48,7 +50,10 @@ const stdLib = {
spec: SpecThemeSolver, spec: SpecThemeSolver,
subject: SubjectThemeSolver, subject: SubjectThemeSolver,
}, },
layouts: layoutRegistry, layouts: {
...layoutRegistry,
...Hierarchy,
},
behaviors: { behaviors: {
'activate-relations': ActivateRelations, 'activate-relations': ActivateRelations,
'drag-canvas': DragCanvas, 'drag-canvas': DragCanvas,
@ -57,6 +62,7 @@ const stdLib = {
'drag-node': DragNode, 'drag-node': DragNode,
'drag-combo': DragCombo, 'drag-combo': DragCombo,
'collapse-expand-combo': CollapseExpandCombo, 'collapse-expand-combo': CollapseExpandCombo,
'collapse-expand-tree': CollapseExpandTree,
'click-select': ClickSelect, 'click-select': ClickSelect,
'brush-select': BrushSelect, 'brush-select': BrushSelect,
'lasso-select': LassoSelect, 'lasso-select': LassoSelect,

View File

@ -35,8 +35,6 @@ export default class Grid extends Base {
public init(graph: IGraph) { public init(graph: IGraph) {
super.init(graph); super.init(graph);
const minZoom = graph.getZoom(); const minZoom = graph.getZoom();
// console.log('minZoom', minZoom);
// console.log('graph', graph.canvas);
const graphContainer = graph.container; const graphContainer = graph.container;
const canvas = this.canvas || graphContainer.firstChild.nextSibling; const canvas = this.canvas || graphContainer.firstChild.nextSibling;
const [width, height] = graph.getSize(); const [width, height] = graph.getSize();

View File

@ -1,7 +1,8 @@
import { Graph as GraphLib } from '@antv/graphlib'; import { Graph as GraphLib, TreeData } from '@antv/graphlib';
import { ComboUserModel } from './combo'; import { ComboUserModel } from './combo';
import { NodeDisplayModelData, NodeModelData, NodeUserModel } from './node'; import { NodeDisplayModelData, NodeModelData, NodeUserModel } from './node';
import { EdgeDisplayModelData, EdgeModelData, EdgeUserModel } from './edge'; import { EdgeDisplayModelData, EdgeModelData, EdgeUserModel } from './edge';
import { NodeUserModelData } from './node';
export interface GraphData { export interface GraphData {
nodes?: NodeUserModel[]; nodes?: NodeUserModel[];
@ -9,16 +10,26 @@ export interface GraphData {
combos?: ComboUserModel[]; combos?: ComboUserModel[];
} }
export interface InlineDataConfig { export interface InlineGraphDataConfig {
type: 'inline'; type: 'graphData';
value: GraphData; value: GraphData;
} }
export interface InlineTreeDataConfig {
type: 'treeData';
value: TreeData<NodeUserModelData> | TreeData<NodeUserModelData>[];
}
export interface FetchDataConfig { export interface FetchDataConfig {
type: 'fetch'; type: 'fetch';
value: string; value: string;
} }
export type DataConfig =
| GraphData
| InlineGraphDataConfig
| InlineTreeDataConfig
| FetchDataConfig;
export type GraphCore = GraphLib<NodeModelData, EdgeModelData>; export type GraphCore = GraphLib<NodeModelData, EdgeModelData>;
export type DisplayGraphCore = GraphLib< export type DisplayGraphCore = GraphLib<
NodeDisplayModelData, NodeDisplayModelData,

View File

@ -232,6 +232,11 @@ export interface IGraph<
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[] ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>, >,
upsertAncestors?: boolean, upsertAncestors?: boolean,
disableAnimate?: boolean,
callback?: (
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean, stack?: boolean,
) => NodeModel | ComboModel | NodeModel[] | ComboModel[]; ) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
@ -249,6 +254,8 @@ export interface IGraph<
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[] ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
>, >,
upsertAncestors?: boolean, upsertAncestors?: boolean,
disableAnimate?: boolean,
callback?: (model: NodeModel | EdgeModel | ComboModel) => void,
stack?: boolean, stack?: boolean,
) => NodeModel | ComboModel | NodeModel[] | ComboModel[]; ) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
@ -444,14 +451,14 @@ export interface IGraph<
* @returns * @returns
* @group Data * @group Data
*/ */
showItem: (ids: ID | ID[], disableAniamte?: boolean) => void; showItem: (ids: ID | ID[], disableAnimate?: boolean) => void;
/** /**
* Hide the item(s). * Hide the item(s).
* @param ids the item id(s) to be hidden * @param ids the item id(s) to be hidden
* @returns * @returns
* @group Item * @group Item
*/ */
hideItem: (ids: ID | ID[], disableAniamte?: boolean) => void; hideItem: (ids: ID | ID[], disableAnimate?: boolean) => void;
/** /**
* Make the item(s) to the front. * Make the item(s) to the front.
* @param ids the item id(s) to front * @param ids the item id(s) to front
@ -543,7 +550,7 @@ export interface IGraph<
/** /**
* Layout the graph (with current configurations if cfg is not assigned). * Layout the graph (with current configurations if cfg is not assigned).
*/ */
layout: (options?: LayoutOptions) => Promise<void>; layout: (options?: LayoutOptions, disableAnimate?: boolean) => Promise<void>;
stopLayout: () => void; stopLayout: () => void;
// ===== interaction ===== // ===== interaction =====
@ -627,4 +634,24 @@ export interface IGraph<
type: string; type: string;
[cfgName: string]: unknown; [cfgName: string]: unknown;
}) => void; }) => void;
// ===== tree operations =====
/**
* Collapse sub tree(s).
* @param ids Root id(s) of the sub trees.
* @param disableAnimate Whether disable the animations for this operation.
* @param stack Whether push this operation to stack.
* @returns
* @group Tree
*/
collapse: (ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) => void;
/**
* Expand sub tree(s).
* @param ids Root id(s) of the sub trees.
* @param disableAnimate Whether disable the animations for this operation.
* @param stack Whether push this operation to stack.
* @returns
* @group Tree
*/
expand: (ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) => void;
} }

View File

@ -2,13 +2,14 @@ import { Canvas } from '@antv/g';
import { GraphChange, ID } from '@antv/graphlib'; import { GraphChange, ID } from '@antv/graphlib';
import { CameraAnimationOptions } from './animate'; import { CameraAnimationOptions } from './animate';
import { BehaviorOptionsOf } from './behavior'; import { BehaviorOptionsOf } from './behavior';
import { DataChangeType, GraphCore, GraphData } from './data'; import { DataChangeType, DataConfig, GraphCore } from './data';
import { EdgeModelData } from './edge'; import { EdgeModel, EdgeModelData } from './edge';
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from './item'; import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from './item';
import { LayoutOptions } from './layout'; import { LayoutOptions } from './layout';
import { NodeModelData } from './node'; import { NodeModel, NodeModelData } from './node';
import { ThemeSpecification } from './theme'; import { ThemeSpecification } from './theme';
import { GraphTransformOptions } from './view'; import { GraphTransformOptions } from './view';
import { ComboModel } from './combo';
export interface IHook<T> { export interface IHook<T> {
name: string; name: string;
@ -35,7 +36,7 @@ export interface Hooks {
// data // data
datachange: IHook<{ datachange: IHook<{
type: DataChangeType; type: DataChangeType;
data: GraphData; data: DataConfig;
}>; }>;
itemchange: IHook<{ itemchange: IHook<{
type: ITEM_TYPE; type: ITEM_TYPE;
@ -43,14 +44,20 @@ export interface Hooks {
graphCore: GraphCore; graphCore: GraphCore;
theme: ThemeSpecification; theme: ThemeSpecification;
upsertAncestors?: boolean; upsertAncestors?: boolean;
animate?: boolean;
action?: 'updatePosition'; action?: 'updatePosition';
callback?: (model: NodeModel | EdgeModel | ComboModel) => void;
}>; }>;
render: IHook<{ render: IHook<{
graphCore: GraphCore; graphCore: GraphCore;
theme: ThemeSpecification; theme: ThemeSpecification;
transientCanvas: Canvas; transientCanvas: Canvas;
}>; // TODO: define param template }>; // TODO: define param template
layout: IHook<{ graphCore: GraphCore; options?: LayoutOptions }>; // TODO: define param template layout: IHook<{
graphCore: GraphCore;
options?: LayoutOptions;
animate?: boolean;
}>; // TODO: define param template
// 'updatelayout': IHook<any>; // TODO: define param template // 'updatelayout': IHook<any>; // TODO: define param template
modechange: IHook<{ mode: string }>; modechange: IHook<{ mode: string }>;
behaviorchange: IHook<{ behaviorchange: IHook<{
@ -101,6 +108,11 @@ export interface Hooks {
transient: Canvas; transient: Canvas;
}; };
}>; }>;
treecollapseexpand: IHook<{
ids: ID[];
action: 'collapse' | 'expand';
graphCore: GraphCore;
animate?: boolean;
}>;
destroy: IHook<{}>; destroy: IHook<{}>;
// TODO: more timecycles here
} }

View File

@ -44,6 +44,7 @@ import {
NodeShapeMap, NodeShapeMap,
NodeUserModel, NodeUserModel,
} from './node'; } from './node';
import { ComboStyleSet, EdgeStyleSet, NodeStyleSet } from './theme';
export type GShapeStyle = CircleStyleProps & export type GShapeStyle = CircleStyleProps &
RectStyleProps & RectStyleProps &
@ -248,6 +249,8 @@ export interface IItem {
displayModel: ItemDisplayModel, displayModel: ItemDisplayModel,
diffData?: { previous: ItemModelData; current: ItemModelData }, diffData?: { previous: ItemModelData; current: ItemModelData },
diffState?: { previous: State[]; current: State[] }, diffState?: { previous: State[]; current: State[] },
animate?: boolean,
onfinish?: Function,
) => void; ) => void;
/** /**
* Updates the shapes. * Updates the shapes.
@ -256,7 +259,14 @@ export interface IItem {
update: ( update: (
model: ItemModel, model: ItemModel,
diffData: { previous: ItemModelData; current: ItemModelData }, diffData: { previous: ItemModelData; current: ItemModelData },
isUpdate?: boolean, isReplace?: boolean,
itemTheme?: {
styles: NodeStyleSet | EdgeStyleSet | ComboStyleSet;
lodStrategy: LodStrategyObj;
},
onlyMove?: boolean,
animate?: boolean,
onfinish?: Function,
) => void; ) => void;
/** /**
@ -269,6 +279,7 @@ export interface IItem {
updatePosition: ( updatePosition: (
displayModel: ItemDisplayModel, displayModel: ItemDisplayModel,
diffData?: { previous: ItemModelData; current: ItemModelData }, diffData?: { previous: ItemModelData; current: ItemModelData },
animate?: boolean,
onfinish?: Function, onfinish?: Function,
) => void; ) => void;

View File

@ -45,6 +45,10 @@ export interface NodeUserModelData extends PlainObject {
* Reserved for combo. * Reserved for combo.
*/ */
parentId?: ID; parentId?: ID;
/**
* Whether to be a root at when used as a tree.
*/
isRoot?: boolean;
/** /**
* The icon to show on the node. * The icon to show on the node.
* More styles should be configured in node mapper. * More styles should be configured in node mapper.

View File

@ -1,12 +1,7 @@
import { Canvas } from '@antv/g'; import { Canvas } from '@antv/g';
import { AnimateCfg } from './animate'; import { AnimateCfg } from './animate';
import { Point } from './common'; import { Point } from './common';
import { import { DataConfig, TransformerFn } from './data';
FetchDataConfig,
GraphData,
InlineDataConfig,
TransformerFn,
} from './data';
import { import {
EdgeDisplayModel, EdgeDisplayModel,
EdgeEncode, EdgeEncode,
@ -36,7 +31,6 @@ export interface Specification<
B extends BehaviorRegistry, B extends BehaviorRegistry,
T extends ThemeRegistry, T extends ThemeRegistry,
> { > {
type: 'graph' | 'tree';
container?: string | HTMLElement; container?: string | HTMLElement;
backgroundCanvas?: Canvas; backgroundCanvas?: Canvas;
canvas?: Canvas; canvas?: Canvas;
@ -61,7 +55,7 @@ export interface Specification<
optimizeThreshold?: number; optimizeThreshold?: number;
/** data */ /** data */
data: GraphData | InlineDataConfig | FetchDataConfig; // TODO: more data?: DataConfig;
transform?: transform?:
| string[] | string[]
| { | {

View File

@ -248,7 +248,7 @@ const runAnimateGroupOnShapes = (
maxDurationIdx = i; maxDurationIdx = i;
} }
if (animation) { if (animation) {
animation.oncancel = () => { animation.onManualCancel = () => {
hasCanceled = true; hasCanceled = true;
cancelAnimations(); cancelAnimations();
}; };
@ -293,10 +293,37 @@ const runAnimateOnShape = (
} }
}); });
} }
if (JSON.stringify(animateArr[0]) === JSON.stringify(animateArr[1])) return; if (!checkFrames(animateArr, shape)) return;
return shape.animate(animateArr, animateConfig); return shape.animate(animateArr, animateConfig);
}; };
/**
* Check and format the frames. If the frames are same, return false. If frames contains undefined x or y, format them.
* @param frames
* @param shape
* @returns
*/
const checkFrames = (frames, shape) => {
if (JSON.stringify(frames[0]) === JSON.stringify(frames[1])) return false;
['x', 'y'].forEach((dim) => {
if (!frames[0].hasOwnProperty(dim)) return;
let val;
const formatted = [...frames];
if (frames[0][dim] === undefined && frames[0][dim] !== frames[1][dim])
val = frames[1][dim];
if (frames[1][dim] === undefined && frames[0][dim] !== frames[1][dim])
val = frames[1][dim];
if (val !== undefined) {
shape.style[dim] = val;
delete formatted[0][dim];
delete formatted[1][dim];
}
});
if (JSON.stringify(frames[0]) === JSON.stringify(frames[1])) return false;
return true;
};
/** /**
* Handle shape and group animations. * Handle shape and group animations.
* Should be called after canvas ready and shape appended. * Should be called after canvas ready and shape appended.
@ -318,7 +345,7 @@ export const animateShapes = (
onAnimatesEnd: Function = () => {}, onAnimatesEnd: Function = () => {},
): IAnimation[] => { ): IAnimation[] => {
if (!animates?.[timing]) { if (!animates?.[timing]) {
onAnimatesEnd(); onAnimatesEnd(false);
return; return;
} }
const segmentedTiming = const segmentedTiming =
@ -335,7 +362,7 @@ export const animateShapes = (
let canceled = false; let canceled = false;
const onfinish = () => { const onfinish = () => {
if (i >= groupKeys.length) { if (i >= groupKeys.length) {
onAnimatesEnd(); !canceled && onAnimatesEnd(canceled);
return; return;
} }
const groupAnimations = runAnimateGroupOnShapes( const groupAnimations = runAnimateGroupOnShapes(
@ -345,7 +372,10 @@ export const animateShapes = (
mergedStyles, mergedStyles,
timing, timing,
onfinish, // execute next order group onfinish, // execute next order group
() => (canceled = true), () => {
canceled = true;
onAnimatesEnd(canceled);
},
canceled, canceled,
).filter(Boolean); ).filter(Boolean);
groupAnimations.forEach((animation) => { groupAnimations.forEach((animation) => {
@ -429,8 +459,9 @@ export const fadeOut = (id, shape, hiddenShape, animateConfig) => {
* Make the animation to the end frame and clear it from the target shape. * Make the animation to the end frame and clear it from the target shape.
* @param animation * @param animation
*/ */
export const stopAnimate = (animation) => { export const stopAnimate = (animation: IAnimation): Promise<any> => {
const timing = animation.effect.getTiming(); const timing = animation.effect.getTiming();
animation.currentTime = Number(timing.duration) + Number(timing.delay || 0); animation.currentTime = Number(timing.duration) + Number(timing.delay || 0);
animation.cancel(); animation.finish();
return animation.finished;
}; };

View File

@ -1,6 +1,14 @@
import { NodeUserModel } from 'types'; import { NodeUserModel } from 'types';
import { IGraph } from '../types/graph'; import { IGraph } from '../types/graph';
import { GraphCore } from '../types/data'; import { GraphCore, GraphData } from '../types/data';
import { TreeData } from '@antv/graphlib';
import { NodeUserModelData } from 'types/node';
import { isArray } from '@antv/util';
import {
depthFirstSearch,
breadthFirstSearch,
connectedComponent,
} from '@antv/algorithm';
/** /**
* Deconstruct data and distinguish nodes and combos from graphcore data. * Deconstruct data and distinguish nodes and combos from graphcore data.
@ -35,19 +43,30 @@ export const graphCoreTreeDfs = (
nodes: NodeUserModel[], nodes: NodeUserModel[],
fn, fn,
mode: 'TB' | 'BT' = 'TB', mode: 'TB' | 'BT' = 'TB',
treeKey = 'combo',
stopFns: {
stopBranchFn?: (node: NodeUserModel) => boolean;
stopAllFn?: (node: NodeUserModel) => boolean;
} = {},
) => { ) => {
if (!nodes?.length) return; if (!nodes?.length) return;
nodes.forEach((node) => { const { stopBranchFn, stopAllFn } = stopFns;
if (!graphCore.hasNode(node.id)) return; for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!graphCore.hasNode(node.id)) continue;
if (stopBranchFn?.(node)) continue; // Stop this branch
if (stopAllFn?.(node)) return; // Stop all
if (mode === 'TB') fn(node); // Traverse from top to bottom if (mode === 'TB') fn(node); // Traverse from top to bottom
graphCoreTreeDfs( graphCoreTreeDfs(
graphCore, graphCore,
graphCore.getChildren(node.id, 'combo'), graphCore.getChildren(node.id, treeKey),
fn, fn,
mode, mode,
treeKey,
stopFns,
); );
if (mode !== 'TB') fn(node); // Traverse from bottom to top if (mode !== 'TB') fn(node); // Traverse from bottom to top
}); }
}; };
/** /**
@ -176,3 +195,105 @@ export const validateComboStrucutre = (
} }
return true; return true;
}; };
/**
* Transform tree graph data into graph data. Edges from parent-child structure.
* @param treeData Tree structured data or an array of it.
* @returns Graph formatted data object with nodes, edges and combos.
*/
export const treeData2GraphData = (
treeData: TreeData<NodeUserModelData> | TreeData<NodeUserModelData>[],
) => {
const graphData = {
nodes: [],
edges: [],
combos: [],
};
const trees = isArray(treeData) ? treeData : [treeData];
trees.forEach((tree) => {
traverse(tree, (child) => {
graphData.nodes.push({
id: child.id,
data: child.data,
});
child.children?.forEach((subChild) => {
graphData.edges.push({
id: `tree-edge-${child.id}-${subChild.id}`,
source: child.id,
target: subChild.id,
data: {},
});
});
});
});
return graphData;
};
/**
* Transform graph data into tree graph data.
* @param nodeMap
* @param graphData Graph data.
* @param propRootIds Ids of root nodes. There should be at least one node for each connected component, or the first node in a connected component will be added to the roots array.
* @param algo
* @returns
*/
export const graphData2TreeData = (
nodeMap: { [id: string]: any },
graphData: GraphData,
propRootIds = [],
) => {
const trees = [];
const connectedComponents = connectedComponent(graphData as any, false);
const rootIds = [];
const componentsNodeIds = [];
connectedComponents.forEach((com, i) => {
componentsNodeIds[i] = com.map((node) => node.id);
if (propRootIds.length) {
const root = componentsNodeIds[0].find((id) => propRootIds.includes(id));
rootIds.push(root !== undefined ? root : com[0].id);
} else {
rootIds.push(com[0].id);
}
});
rootIds.forEach((id, i) => {
nodeMap[id] = { id, children: [] };
trees.push(nodeMap[id]);
depthFirstSearch(
graphData as any,
id,
{
enter: ({ previous, current }) => {
if (
!previous ||
current === id ||
!componentsNodeIds[i].includes(current)
)
return;
nodeMap[previous] = nodeMap[previous] || {
id: previous,
children: [],
};
nodeMap[current] = { id: current, children: [] };
nodeMap[previous].children.push(nodeMap[current]);
},
},
false,
);
});
return trees;
};
/**
* Travere a tree data from top to bottom.
* @param treeData
* @param callback
*/
export const traverse = (treeData, callback) => {
callback(treeData);
if (treeData.children) {
treeData.children.forEach((child) => {
if (child) traverse(child, callback);
});
}
};

View File

@ -83,6 +83,7 @@ type GroupedChanges = {
EdgeUpdated: EdgeUpdated<EdgeModelData>[]; EdgeUpdated: EdgeUpdated<EdgeModelData>[];
EdgeDataUpdated: EdgeDataUpdated<EdgeModelData>[]; EdgeDataUpdated: EdgeDataUpdated<EdgeModelData>[];
TreeStructureChanged: TreeStructureChanged[]; TreeStructureChanged: TreeStructureChanged[];
ComboStructureChanged: TreeStructureChanged[];
}; };
/** /**
@ -104,6 +105,7 @@ export const getGroupedChanges = (
EdgeUpdated: [], EdgeUpdated: [],
EdgeDataUpdated: [], EdgeDataUpdated: [],
TreeStructureChanged: [], TreeStructureChanged: [],
ComboStructureChanged: [],
}; };
changes.forEach((change) => { changes.forEach((change) => {
const { type: changeType } = change; const { type: changeType } = change;
@ -119,7 +121,10 @@ export const getGroupedChanges = (
return; return;
} }
} else if (changeType === 'TreeStructureChanged') { } else if (changeType === 'TreeStructureChanged') {
groupedChanges[changeType].push(change); if (change.treeKey === 'combo')
groupedChanges.ComboStructureChanged.push(change);
else if (change.treeKey === 'tree')
groupedChanges.TreeStructureChanged.push(change);
return; return;
} else { } else {
const { id: oid } = change.value; const { id: oid } = change.value;

View File

@ -0,0 +1,65 @@
import Hierarchy from '@antv/hierarchy';
import { traverse } from './data';
import { TreeGraphData } from '@antv/g6';
/**
* Judge the direction according to options of a tree layout.
* @param type Tree layout type.
* @param options Tree layout options.
* @returns
*/
export const isTreeLayoutHorizontal = (type, options) => {
const { direction } = options;
switch (type) {
case 'compactBox':
case 'dendrogram':
return direction !== 'TB' && direction !== ' BT' && direction !== ' V';
case 'indented':
return true;
case 'mindmap':
return direction !== 'V';
}
};
/**
* Layout nodes on a single tree.
* @param treeData
* @param layoutType Tree layout type.
* @param layoutOptions Tree layout options.
* @param nodeMap
* @param nodePositions An array to store the result.
* @param begin The begin position for this tree, might be calculated from last tree.
* @returns Positions array.
*/
export const layoutOneTree = (
treeData: TreeGraphData,
layoutType: string,
layoutOptions,
nodeMap,
nodePositions,
begin = [0, 0],
) => {
const { treeGap = 50 } = layoutOptions;
const isHorizontal = isTreeLayoutHorizontal(layoutType, layoutOptions);
const layoutData = Hierarchy[layoutType](treeData, layoutOptions);
const range = [Infinity, -Infinity];
const treeNodeIds = [];
traverse(layoutData, (child) => {
const { id, x, y } = child;
treeNodeIds.push(id);
const dim = isHorizontal ? 'y' : 'x';
if (range[0] > child[dim]) range[0] = child[dim];
if (range[1] < child[dim]) range[1] = child[dim];
nodeMap[id].data = { x, y };
});
const diff = begin[isHorizontal ? 1 : 0] - range[0];
treeNodeIds.forEach((id) => {
const { x, y } = nodeMap[id].data;
nodePositions.push({
id,
data: isHorizontal ? { x, y: y + diff } : { x: x + diff, y },
});
});
begin[isHorizontal ? 1 : 0] += range[1] + diff + treeGap;
return nodePositions;
};

View File

@ -5,7 +5,6 @@ export default () => {
container, container,
width, width,
height, height,
type: 'graph',
plugins: ['grid'], plugins: ['grid'],
layout: { layout: {
type: 'grid', type: 'grid',

View File

@ -5,7 +5,6 @@ export default () => {
container, container,
width, width,
height, height,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },

View File

@ -5,7 +5,6 @@ export default () => {
container, container,
width, width,
height, height,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },

View File

@ -58,7 +58,6 @@ export default () => {
width, width,
height, height,
data, data,
type: 'graph',
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
}, },

View File

@ -124,7 +124,6 @@ const create2DGraph = (
container: container as HTMLElement, container: container as HTMLElement,
width, width,
height: 1400, height: 1400,
type: 'graph',
renderer: rendererType, renderer: rendererType,
data: dataFor2D, data: dataFor2D,
modes: { modes: {
@ -223,7 +222,6 @@ const create3DGraph = async () => {
container: container as HTMLDivElement, container: container as HTMLDivElement,
width, width,
height: 1400, height: 1400,
type: 'graph',
renderer: 'webgl-3d', renderer: 'webgl-3d',
data: dataFor3D, data: dataFor3D,
// layout: { // layout: {

View File

@ -44,7 +44,6 @@ export default () => {
width, width,
height, height,
data, data,
type: 'graph',
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
}, },

View File

@ -66,7 +66,6 @@ export default () => {
width, width,
height, height,
data, data,
type: 'graph',
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
}, },

View File

@ -51,7 +51,6 @@ export default () => {
width, width,
height, height,
data, data,
type: 'graph',
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
}, },

View File

@ -44,7 +44,6 @@ export default () => {
width, width,
height, height,
data, data,
type: 'graph',
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
}, },

View File

@ -32,6 +32,7 @@ import fisheye from './plugins/fisheye';
import tooltip from './demo/tooltip'; import tooltip from './demo/tooltip';
import comboBasic from './combo/combo-basic'; import comboBasic from './combo/combo-basic';
import animations_node_build_in from './animations/node-build-in'; import animations_node_build_in from './animations/node-build-in';
import treeGraph from './tree/tree-graph';
export { export {
behaviors_activateRelations, behaviors_activateRelations,
@ -68,4 +69,5 @@ export {
tooltip, tooltip,
comboBasic, comboBasic,
animations_node_build_in, animations_node_build_in,
treeGraph,
}; };

View File

@ -231,7 +231,6 @@ export default () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
// supported behavior // supported behavior

View File

@ -234,7 +234,6 @@ export default () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
// 支持的 behavior // 支持的 behavior

View File

@ -231,7 +231,6 @@ export default () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
// supported behavior // supported behavior

View File

@ -227,7 +227,6 @@ export default (context: TestCaseContext) => {
// 2.create graph // 2.create graph
graph = new Graph({ graph = new Graph({
...context, ...context,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
// supported behavior // supported behavior

View File

@ -6,7 +6,6 @@ export default (context: TestCaseContext) => {
const { width, height } = context; const { width, height } = context;
return new G6.Graph({ return new G6.Graph({
...context, ...context,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'd3force', type: 'd3force',

View File

@ -17,7 +17,6 @@ export default async () => {
container, container,
width, width,
height, height,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
renderer: 'webgl-3d', renderer: 'webgl-3d',
modes: { modes: {

View File

@ -13,7 +13,6 @@ export default async () => {
container, container,
width, width,
height, height,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'force-wasm', type: 'force-wasm',

View File

@ -17,7 +17,6 @@ export default async () => {
container, container,
width, width,
height, height,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'forceatlas2-wasm', type: 'forceatlas2-wasm',

View File

@ -10,7 +10,6 @@ export default async () => {
container, container,
width, width,
height, height,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'fruchterman-gpu', type: 'fruchterman-gpu',

View File

@ -17,7 +17,6 @@ export default async () => {
container, container,
width, width,
height, height,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'fruchterman-wasm', type: 'fruchterman-wasm',

View File

@ -77,7 +77,6 @@ export default async () => {
container: $container1, container: $container1,
width: WIDTH, width: WIDTH,
height: HEIGHT, height: HEIGHT,
type: 'graph',
renderer: 'webgl-3d', renderer: 'webgl-3d',
modes: { modes: {
default: [ default: [
@ -144,7 +143,6 @@ export default async () => {
container: $container2, container: $container2,
width: WIDTH, width: WIDTH,
height: HEIGHT, height: HEIGHT,
type: 'graph',
renderer: 'webgl-3d', renderer: 'webgl-3d',
modes: { modes: {
default: [ default: [

View File

@ -135,7 +135,6 @@ export default async () => {
container: $container1, container: $container1,
width: WIDTH, width: WIDTH,
height: HEIGHT, height: HEIGHT,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'force-wasm', type: 'force-wasm',
@ -171,7 +170,6 @@ export default async () => {
container: $container2, container: $container2,
width: WIDTH, width: WIDTH,
height: HEIGHT, height: HEIGHT,
type: 'graph',
data: JSON.parse(JSON.stringify(data)), data: JSON.parse(JSON.stringify(data)),
layout: { layout: {
type: 'force', type: 'force',

View File

@ -1944,7 +1944,6 @@ const createGraph = async () => {
container: container as HTMLElement, container: container as HTMLElement,
width, width,
height: 1200, height: 1200,
type: 'graph',
// renderer: 'webgl', // renderer: 'webgl',
data: { nodes, edges }, data: { nodes, edges },
layout: { layout: {

View File

@ -139,7 +139,6 @@ export default async () => {
container: 'container', container: 'container',
width, width,
height, height,
type: 'graph',
layout: { layout: {
type: 'force', type: 'force',
rankdir: 'LR', rankdir: 'LR',

View File

@ -0,0 +1,266 @@
import Stats from 'stats-js';
import G6 from '../../../src/index';
import { container, height, width } from '../../datasets/const';
import { CanvasEvent } from '@antv/g';
const treeDataCfg = {
type: 'treeData',
value: [
{
id: 'root',
data: {
// collapsed: true,
},
children: [
{
id: 'c1',
data: {},
children: [
{
id: 'c1-c1',
data: {},
},
],
},
{
id: 'c2',
data: {},
},
],
},
{
id: 'root2',
data: {},
children: [
{
id: 't2c1',
data: {},
children: [
{
id: 't2c1-c1',
data: {},
},
],
},
{
id: 't2c2',
data: {},
},
],
},
],
};
const graphDataCfg = {
type: 'graphData',
value: {
nodes: [
{ id: 'node1', data: { isRoot: true, collapsed: true } },
{ id: 'node2', data: {} },
{ id: 'node3', data: {} },
{ id: 'node4', data: {} },
{ id: 'node5', data: {} },
],
edges: [
{ id: 'edge1', source: 'node1', target: 'node2', data: {} },
{ id: 'edge2', source: 'node1', target: 'node3', data: {} },
{ id: 'edge3', source: 'node1', target: 'node4', data: {} },
// { id: 'edge4', source: 'node2', target: 'node3', data: {} },
// { id: 'edge5', source: 'node3', target: 'node4', data: {} },
{ id: 'edge6', source: 'node4', target: 'node5', data: {} },
],
},
};
const dataGenerator = (nodeNum, edgeNum) => {
const nodes: any = [];
const edges: any = [];
for (let i = 0; i < nodeNum; i++) {
nodes.push({
id: `node-${i}`,
data: {
x: Math.random() * 1000,
y: Math.random() * 1000,
},
});
}
for (let i = 0; i < edgeNum; i++) {
edges.push({
id: `edge-${i}`,
source: nodes[Math.floor(Math.random() * nodeNum)].id,
target: nodes[Math.floor(Math.random() * nodeNum)].id,
data: {},
});
}
return { nodes, edges };
};
export default async () => {
// const data = dataGenerator(90000, 10000);
// console.log('data', data);
const graph = new G6.Graph({
container,
width,
height,
layout: {
type: 'compactBox',
// type: 'grid',
},
node: (innerModel) => {
const { x, y, labelShape } = innerModel.data;
return {
...innerModel,
data: {
x,
y,
animates: {
update: [
{
fields: ['x', 'y'],
duration: 500,
shapeId: 'group',
order: 0,
},
],
hide: [
{
fields: ['opacity'],
duration: 200,
shapeId: 'keyShape',
},
{
fields: ['opacity'],
duration: 200,
shapeId: 'labelShape',
},
],
show: [
{
fields: ['opacity'],
duration: 1000,
shapeId: 'keyShape',
},
{
fields: ['opacity'],
duration: 1000,
shapeId: 'labelShape',
},
],
},
// animate in shapes, unrelated to each other, excuted parallely
labelShape: {
text: innerModel.id,
...labelShape,
},
},
};
},
edge: (innerModel) => {
return {
...innerModel,
data: {
...innerModel.data,
animates: {
// hide: [
// {
// fields: ['opacity'],
// duration: 200,
// shapeId: 'keyShape',
// },
// {
// fields: ['opacity'],
// duration: 200,
// shapeId: 'labelShape',
// },
// ],
// show: [
// {
// fields: ['opacity'],
// duration: 1000,
// shapeId: 'keyShape',
// },
// {
// fields: ['opacity'],
// duration: 1000,
// shapeId: 'labelShape',
// },
// ],
},
},
};
},
// node: {
// animates: {
// update: [
// {
// fields: ['x', 'y'],
// duration: 2000,
// shapeId: 'group',
// },
// ],
// },
// labelShape: {
// text: {
// fields: ['id'],
// formatter: (model) => model.id,
// },
// },
// },
data: treeDataCfg,
// data: graphDataCfg,
modes: {
default: [
'drag-canvas',
'zoom-canvas',
{
type: 'collapse-expand-tree',
trigger: 'click',
},
],
},
});
const stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
graph.canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => {
if (stats) {
stats.update();
}
});
let collapsed = true;
graph.translateTo({ x: 100, y: 100 });
graph.on('canvas:click', (e) => {
/** === change graph data / tree data === */
// graph.changeData(treeDataCfg);
/** === change layout === */
// graph.layout({ type: 'compactBox' });
/** === collapse / expand === */
const ids = ['root2', 'root']; //['root']; //['node1']; //
collapsed ? graph.expand(ids) : graph.collapse(ids);
collapsed = !collapsed;
});
graph.on('canvas:contextmenu', (e) => {
console.log('contextmenu');
/** === updateData === */
// graph.updateData('node', {
// id: 'node2',
// data: { labelShape: { text: 'updated!!' } },
// });
/** === remove a child / subtree root === */
// graph.removeData('node', ['node4']);
/** === add a child === */
// graph.addData('node', [{ id: 'newnode', data: {} }]);
// graph.addData('edge', [
// { id: 'newedge', source: 'node1', target: 'newnode', data: {} },
// ]);
});
return graph;
};

View File

@ -10,7 +10,6 @@ const createGraph = () => {
container: container as HTMLElement, container: container as HTMLElement,
width, width,
height: 1200, height: 1200,
type: 'graph',
// renderer: 'webgl', // renderer: 'webgl',
data: { data: {
nodes: [ nodes: [

View File

@ -0,0 +1,16 @@
import G6 from '../../../src/index';
import { container, data, height, width } from '../../datasets/const';
export default () => {
return new G6.Graph({
container,
width,
height,
data,
layout: {
type: 'circular',
center: [250, 250],
radius: 200,
},
});
};

View File

@ -0,0 +1,56 @@
import G6 from '../../../src/index';
import { container, data, height, width } from '../../datasets/const';
export default () => {
return new G6.Graph({
container,
width,
height,
renderer: 'webgl-3d',
modes: {
default: [
{
type: 'orbit-canvas-3d',
trigger: 'drag',
},
'zoom-canvas-3d',
],
},
data: JSON.parse(JSON.stringify(data)),
layout: {
type: 'force',
dimensions: 3,
iterations: 100,
center: [width / 2, height / 2, 0],
},
edge: {
type: 'line-edge',
keyShape: {
lineWidth: 2,
stroke: 'grey',
},
},
node: {
type: 'sphere-node',
keyShape: {
opacity: 0.6,
},
// labelShape: {
// text: 'node-label',
// },
// iconShape: {
// img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
// },
},
nodeState: {
selected: {
keyShape: {
fill: '#f00',
},
labelShape: {
fontSize: 20,
},
},
},
});
};

View File

@ -8,7 +8,6 @@ describe('behavior', () => {
it('behavior in spec, add / remove / update a behavior in defualt mode', () => { it('behavior in spec, add / remove / update a behavior in defualt mode', () => {
const graph = new G6.Graph({ const graph = new G6.Graph({
container, container,
type: 'graph',
data: { nodes: [], edges: [] }, data: { nodes: [], edges: [] },
modes: { modes: {
default: [ default: [
@ -116,7 +115,6 @@ describe('behavior', () => {
}); });
const graph = new CustomGraph({ const graph = new CustomGraph({
container, container,
type: 'graph',
data: { nodes: [], edges: [] }, data: { nodes: [], edges: [] },
modes: { modes: {
default: [ default: [

View File

@ -9,7 +9,6 @@ describe('brush-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -142,7 +141,6 @@ describe('brush-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -253,7 +251,6 @@ describe('brush-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -368,7 +365,6 @@ describe('brush-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -527,7 +523,6 @@ describe('brush-select behavior with itemTypes', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -660,7 +655,6 @@ describe('brush-select behavior with itemTypes', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -746,7 +740,6 @@ describe('brush-select behavior with itemTypes', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -836,7 +829,6 @@ describe('brush-select behavior with trigger', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -937,7 +929,6 @@ describe('brush-select behavior with trigger', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -1074,7 +1065,6 @@ describe('brush-select behavior with shouldBegin and shouldUpdate', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -1210,7 +1200,6 @@ describe('brush-select behavior with brushStyle', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },

View File

@ -8,7 +8,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -105,7 +104,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -172,7 +170,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -232,7 +229,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -283,7 +279,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -355,7 +350,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -426,7 +420,6 @@ describe('drag-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },

View File

@ -9,7 +9,6 @@ describe('lasso-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -158,7 +157,6 @@ describe('lasso-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -277,7 +275,6 @@ describe('lasso-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -400,7 +397,6 @@ describe('lasso-select behavior with selectSetMode', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -579,7 +575,6 @@ describe('lasso-select behavior with itemTypes', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -724,7 +719,6 @@ describe('lasso-select behavior with itemTypes', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -818,7 +812,6 @@ describe('lasso-select behavior with itemTypes', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -912,7 +905,6 @@ describe('lasso-select behavior with trigger', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -1017,7 +1009,6 @@ describe('lasso-select behavior with trigger', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -1170,7 +1161,6 @@ describe('lasso-select behavior with shouldBegin and shouldUpdate', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -1303,7 +1293,6 @@ describe('lasso-select behavior with brushStyle', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },

View File

@ -8,7 +8,6 @@ describe('zoom-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -93,7 +92,6 @@ describe('zoom-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -149,7 +147,6 @@ describe('zoom-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },
@ -226,7 +223,6 @@ describe('zoom-canvas behavior', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
layout: { layout: {
type: 'grid', type: 'grid',
}, },

View File

@ -10,7 +10,6 @@ describe('click-select', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200, keyShape: { fill: '#0f0' } } }, { id: 'node1', data: { x: 100, y: 200, keyShape: { fill: '#0f0' } } },

View File

@ -24,7 +24,6 @@ describe('data', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, // with data, graph will be rendered in constructor data, // with data, graph will be rendered in constructor
}); });
graph.on('afterrender', () => { graph.on('afterrender', () => {

View File

@ -11,7 +11,6 @@ const createGraph = (dragNodeOptions: DragNodeOptions): IGraph => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200, keyShape: { fill: '#0f0' } } }, { id: 'node1', data: { x: 100, y: 200, keyShape: { fill: '#0f0' } } },

View File

@ -25,7 +25,6 @@ describe('edge item', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
modes: { modes: {
default: ['drag-node'], default: ['drag-node'],
}, },
@ -265,7 +264,6 @@ describe('edge mapper', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
}; };
it('function mapper', (done) => { it('function mapper', (done) => {
const graph = new G6.Graph({ const graph = new G6.Graph({
@ -373,7 +371,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -507,7 +504,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -769,7 +765,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -897,7 +892,6 @@ describe('cubic-edge unit test', () => {
container, container,
width, width,
height, height,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
@ -1137,7 +1131,6 @@ describe('cubic-edge unit test', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: stateData, data: stateData,
modes: { modes: {
// Supported behavior // Supported behavior
@ -1272,7 +1265,6 @@ describe('cubic-horizon-edge unit test', () => {
container, container,
width, width,
height, height,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
@ -1415,7 +1407,6 @@ describe('cubic-horizon-edge unit test', () => {
container, container,
width, width,
height, height,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
@ -1654,7 +1645,6 @@ describe('cubic-horizon-edge unit test', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: stateData, data: stateData,
modes: { modes: {
default: ['activate-relations'], default: ['activate-relations'],
@ -1788,7 +1778,6 @@ describe('cubic-vertical-edge unit test', () => {
container, container,
width, width,
height, height,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
@ -2027,7 +2016,6 @@ describe('cubic-vertical-edge unit test', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: stateData, data: stateData,
modes: { modes: {
default: ['activate-relations'], default: ['activate-relations'],
@ -2161,7 +2149,6 @@ describe('cubic-vertical-edge unit test', () => {
container, container,
width, width,
height, height,
type: 'graph',
data: defaultData, data: defaultData,
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
@ -2400,7 +2387,6 @@ describe('cubic-vertical-edge unit test', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: stateData, data: stateData,
modes: { modes: {
default: ['activate-relations'], default: ['activate-relations'],

View File

@ -31,7 +31,6 @@ describe('node item', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
renderer: 'webgl-3d', renderer: 'webgl-3d',
// renderer: 'canvas', // renderer: 'canvas',
modes: { modes: {

View File

@ -124,7 +124,6 @@ const createGraph = (props) => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: clonedData, data: clonedData,
...props, ...props,
}); });

View File

@ -12,7 +12,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
}); });
@ -35,7 +34,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -123,7 +121,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -147,7 +144,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -185,7 +181,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -224,7 +219,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -262,7 +256,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'd3force', type: 'd3force',
@ -289,7 +282,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -342,7 +334,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -388,7 +379,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'd3force', type: 'd3force',
@ -417,7 +407,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'd3force', type: 'd3force',
@ -470,7 +459,6 @@ describe('layout', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'myCustomLayout', type: 'myCustomLayout',

View File

@ -26,7 +26,6 @@ describe('node item', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -138,7 +137,6 @@ describe('node mapper', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
}; };
it('function mapper', (done) => { it('function mapper', (done) => {
const graph = new G6.Graph({ const graph = new G6.Graph({
@ -323,7 +321,6 @@ describe('register node', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -465,7 +462,6 @@ describe('register node', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -533,7 +529,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -677,7 +672,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -920,7 +914,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {

View File

@ -10,7 +10,6 @@ const createGraph = (plugins) => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } }, { id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } },
@ -65,9 +64,9 @@ const createGraph = (plugins) => {
describe('grid plugin', () => { describe('grid plugin', () => {
test('grid with string config', () => { test('grid with string config', () => {
const graph = createGraph([ const graph = createGraph([
{ {
img: 'url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2UwZTBlMCIgb3BhY2l0eT0iMC4yIiBzdHJva2Utd2lkdGg9IjEiLz48cGF0aCBkPSJNIDQwIDAgTCAwIDAgMCA0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIiBzdHJva2Utd2lkdGg9IjEiLz48L3BhdHRlcm4+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JpZCkiLz48L3N2Zz4=)' img: 'url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2UwZTBlMCIgb3BhY2l0eT0iMC4yIiBzdHJva2Utd2lkdGg9IjEiLz48cGF0aCBkPSJNIDQwIDAgTCAwIDAgMCA0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIiBzdHJva2Utd2lkdGg9IjEiLz48L3BhdHRlcm4+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JpZCkiLz48L3N2Zz4=)',
} },
]); ]);
}); });
}); });

View File

@ -10,7 +10,6 @@ const createGraph = (plugins) => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } }, { id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } },

View File

@ -9,7 +9,6 @@ const createGraph = (plugins) => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200 } }, { id: 'node1', data: { x: 100, y: 200 } },

View File

@ -9,7 +9,6 @@ const createGraph = (plugins) => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200 } }, { id: 'node1', data: { x: 100, y: 200 } },

View File

@ -9,7 +9,6 @@ const createGraph = (plugins) => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ id: 'node1', data: { x: 100, y: 200 } }, { id: 'node1', data: { x: 100, y: 200 } },

View File

@ -82,7 +82,6 @@ describe('edge item', () => {
width, width,
height, height,
data, data,
type: 'graph',
modes: { modes: {
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'], default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
}, },
@ -282,7 +281,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -416,7 +414,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -550,7 +547,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -684,7 +680,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {
@ -818,7 +813,6 @@ describe('state', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: { data: {
nodes: [ nodes: [
{ {

View File

@ -8,7 +8,6 @@ describe('behavior', () => {
it('behavior in spec, add / remove / update a behavior in defualt mode', () => { it('behavior in spec, add / remove / update a behavior in defualt mode', () => {
const graph = new G6.Graph({ const graph = new G6.Graph({
container, container,
type: 'graph',
data: { nodes: [], edges: [] }, data: { nodes: [], edges: [] },
modes: { modes: {
default: ['drag-canvas', 'click-select', 'drag-canvas', 'zoom-canvas'], default: ['drag-canvas', 'click-select', 'drag-canvas', 'zoom-canvas'],

View File

@ -1887,7 +1887,6 @@ const createGraph = (props) => {
// renderer: 'webgl', // renderer: 'webgl',
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: clonedData, data: clonedData,
// data: { // data: {
// nodes: [clonedData.nodes[0], clonedData.nodes[10]], // nodes: [clonedData.nodes[0], clonedData.nodes[10]],

View File

@ -88,7 +88,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'grid', type: 'grid',
@ -203,7 +202,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: clone(data), data: clone(data),
layout: { layout: {
type: 'grid', type: 'grid',
@ -430,7 +428,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data: clone(data), data: clone(data),
layout: { layout: {
type: 'grid', type: 'grid',
@ -712,7 +709,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'grid', type: 'grid',

View File

@ -27,7 +27,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'grid', type: 'grid',
@ -126,7 +125,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'grid', type: 'grid',
@ -255,7 +253,6 @@ describe('theme', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'grid', type: 'grid',

View File

@ -12,7 +12,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -42,7 +41,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -85,7 +83,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -131,7 +128,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -208,7 +204,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -259,7 +254,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -385,7 +379,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -422,7 +415,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -473,7 +465,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',
@ -542,7 +533,6 @@ describe('viewport', () => {
container, container,
width: 500, width: 500,
height: 500, height: 500,
type: 'graph',
data, data,
layout: { layout: {
type: 'circular', type: 'circular',