49
.github/workflows/build.yml
vendored
@ -1,22 +1,49 @@
|
||||
name: build
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}}-${{github.event_name}}-${{github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macOS-latest
|
||||
lint:
|
||||
runs-on: macos-latest
|
||||
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.5
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
version: 8.0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
|
||||
# postInstall will build the product
|
||||
- name: Install dependencies and Build the product
|
||||
run: pnpm install
|
||||
|
||||
- name: Run CI
|
||||
run: |
|
||||
cd packages/g6
|
||||
npm install
|
||||
npm run ci
|
||||
npm run ci
|
||||
|
||||
- name: Workflow failed alert
|
||||
if: ${{ failure() }}
|
||||
uses: actions-cool/maintain-one-comment@main
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: |
|
||||
你好, @${{ github.event.pull_request.user.login }} CI 执行失败, 请点击 [Details] 按钮查看, 并根据日志修复
|
||||
|
||||
Hello, @${{ github.event.pull_request.user.login }} CI run failed, please click the [Details] button for detailed log information and fix it.
|
||||
<!-- Created by actions-cool/maintain-one-comment -->
|
||||
emojis: 'eyes'
|
||||
body-include: '<!-- Created by actions-cool/maintain-one-comment -->'
|
||||
|
11
.prettierrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": { "parser": "json" }
|
||||
}
|
||||
]
|
||||
}
|
@ -140,12 +140,13 @@ const graph = new G6.Graph({
|
||||
## 开发 (5.0 Alpha)
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
|
||||
$ pnpm install
|
||||
|
||||
# 从项目根目录进入到 g6 包文件目录下
|
||||
$ cd packages/g6
|
||||
|
||||
# 安装依赖
|
||||
$ npm install
|
||||
|
||||
# 构建
|
||||
$ npm run build
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.8.8",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@commitlint/cli": "^17.5.0",
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
@ -15,5 +16,10 @@
|
||||
"vite": "^4.2.1",
|
||||
"stats.js": "^0.17.0"
|
||||
},
|
||||
"pnpm":{
|
||||
"overrides":{
|
||||
"prettier": "^2.8.8"
|
||||
}
|
||||
},
|
||||
"repository": "https://github.com/antvis/G6.git"
|
||||
}
|
@ -7,7 +7,8 @@
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
"plugin:import/typescript",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-constant-condition": [
|
||||
|
@ -14,7 +14,7 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@g6/types': '<rootDir>/src/types',
|
||||
'@g6/(.*)': '<rootDir>/src/$1',
|
||||
'^d3-(.*)$': '<rootDir>/../../node_modules/d3-$1/dist/d3-$1.min.js',
|
||||
'^d3-(.*)$': '<rootDir>/node_modules/d3-$1/dist/d3-$1.min.js',
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
|
@ -46,7 +46,7 @@
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"lint-staged": {
|
||||
@ -57,10 +57,11 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/hierarchy": "latest",
|
||||
"@antv/event-emitter": "latest",
|
||||
"insert-css": "^2.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/g": "^5.15.7",
|
||||
"@antv/g-canvas": "^1.9.28",
|
||||
@ -77,6 +78,7 @@
|
||||
"@antv/matrix-util": "^3.0.4",
|
||||
"@antv/util": "~2.0.5",
|
||||
"color": "^4.2.3",
|
||||
"stats-js": "^1.0.1",
|
||||
"tslib": "^2.5.0",
|
||||
"typedoc-plugin-markdown": "^3.14.0",
|
||||
"typescript": "^5.1.6"
|
||||
@ -108,13 +110,14 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pixelmatch": "5.3.0",
|
||||
"pngjs": "^6.0.0",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^3.0.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"rollup-plugin-typescript": "^1.0.1",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"stats-js": "1.0.1",
|
||||
"stats.js": "^0.17.0",
|
||||
"ts-jest": "^28.0.8",
|
||||
"typedoc": "^0.24.0",
|
||||
|
@ -15,8 +15,8 @@ interface IProps {
|
||||
model: ComboModel;
|
||||
renderExtensions: any;
|
||||
containerGroup: Group;
|
||||
mapper: DisplayMapper;
|
||||
stateMapper: {
|
||||
mapper?: DisplayMapper;
|
||||
stateMapper?: {
|
||||
[stateName: string]: DisplayMapper;
|
||||
};
|
||||
zoom?: number;
|
||||
@ -74,6 +74,7 @@ export default class Combo extends Node {
|
||||
displayModel: ComboDisplayModel,
|
||||
diffData?: { previous: ComboUserModelData; current: ComboUserModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
animate = true,
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,13 +8,14 @@ import { animateShapes } from '../util/animate';
|
||||
import { EdgeStyleSet } from '../types/theme';
|
||||
import Item from './item';
|
||||
import Node from './node';
|
||||
import Combo from './combo';
|
||||
|
||||
interface IProps {
|
||||
model: EdgeModel;
|
||||
renderExtensions: any; // TODO: type
|
||||
containerGroup: Group;
|
||||
mapper: DisplayMapper;
|
||||
stateMapper: {
|
||||
mapper?: DisplayMapper;
|
||||
stateMapper?: {
|
||||
[stateName: string]: DisplayMapper;
|
||||
};
|
||||
sourceItem: Node;
|
||||
@ -50,6 +51,7 @@ export default class Edge extends Item {
|
||||
displayModel: EdgeDisplayModel,
|
||||
diffData?: { previous: EdgeModelData; current: EdgeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
animate = true,
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
// get the end points
|
||||
@ -94,7 +96,7 @@ export default class Edge extends Item {
|
||||
|
||||
const timing = firstRendering ? 'buildIn' : 'update';
|
||||
// handle shape's animate
|
||||
if (!disableAnimate && usingAnimates[timing]?.length) {
|
||||
if (animate && !disableAnimate && usingAnimates[timing]?.length) {
|
||||
this.animations = animateShapes(
|
||||
usingAnimates,
|
||||
targetStyles, // targetStylesMap
|
||||
@ -103,7 +105,7 @@ export default class Edge extends Item {
|
||||
firstRendering ? 'buildIn' : 'update',
|
||||
this.changedStates,
|
||||
this.animateFrameListener,
|
||||
() => onfinish(displayModel.id),
|
||||
(canceled) => onfinish(displayModel.id, canceled),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -140,8 +142,8 @@ export default class Edge extends Item {
|
||||
|
||||
public clone(
|
||||
containerGroup: Group,
|
||||
sourceItem: Node,
|
||||
targetItem: Node,
|
||||
sourceItem: Node | Combo,
|
||||
targetItem: Node | Combo,
|
||||
onlyKeyShape?: boolean,
|
||||
disableAnimate?: boolean,
|
||||
) {
|
||||
|
@ -68,11 +68,13 @@ export default abstract class Item implements IItem {
|
||||
/** The flag of transient item. */
|
||||
public transient = false;
|
||||
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[];
|
||||
/** Cache the group animation instances to stop at next lifecycle. */
|
||||
public groupAnimations: IAnimation[];
|
||||
|
||||
public themeStyles: {
|
||||
default?: ItemShapeStyles;
|
||||
default: ItemShapeStyles;
|
||||
[stateName: string]: ItemShapeStyles;
|
||||
};
|
||||
/** The zoom strategy to show and hide shapes according to their lod. */
|
||||
@ -159,6 +161,7 @@ export default abstract class Item implements IItem {
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
animate = true,
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
// call this.renderExt.draw in extend implementations
|
||||
@ -198,6 +201,7 @@ export default abstract class Item implements IItem {
|
||||
lodStrategy: LodStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
animate = true,
|
||||
onfinish?: Function,
|
||||
) {
|
||||
// 1. merge model into this model
|
||||
@ -218,7 +222,7 @@ export default abstract class Item implements IItem {
|
||||
: itemTheme?.lodStrategy || this.lodStrategy;
|
||||
|
||||
if (onlyMove) {
|
||||
this.updatePosition(displayModel, diffData, onfinish);
|
||||
this.updatePosition(displayModel, diffData, animate, onfinish);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -242,9 +246,9 @@ export default abstract class Item implements IItem {
|
||||
}
|
||||
// 3. call element update fn from useLib
|
||||
if (this.states?.length) {
|
||||
this.drawWithStates(this.states, onfinish);
|
||||
this.drawWithStates(this.states, animate, onfinish);
|
||||
} 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
|
||||
this.stateDirtyMap = {};
|
||||
@ -261,6 +265,7 @@ export default abstract class Item implements IItem {
|
||||
public updatePosition(
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
animate?: boolean,
|
||||
onfinish?: Function,
|
||||
) {}
|
||||
|
||||
@ -395,60 +400,63 @@ export default abstract class Item implements IItem {
|
||||
|
||||
/** Show the item. */
|
||||
public show(animate = true) {
|
||||
// TODO: utilize graphcore's view
|
||||
this.stopAnimations();
|
||||
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.show?.length) {
|
||||
const showAnimateFieldsMap: any = {};
|
||||
Object.values(animates.show).forEach((animate) => {
|
||||
const { shapeId = 'group' } = animate;
|
||||
showAnimateFieldsMap[shapeId] = (
|
||||
showAnimateFieldsMap[shapeId] || []
|
||||
).concat(animate.fields);
|
||||
});
|
||||
const targetStyleMap = {};
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
if (!this.cacheHiddenShape[id]) {
|
||||
// set the animate fields to initial value
|
||||
if (showAnimateFieldsMap[id]) {
|
||||
targetStyleMap[id] = targetStyleMap[id] || {};
|
||||
const beginStyle = getShapeAnimateBeginStyles(shape);
|
||||
showAnimateFieldsMap[id].forEach((field) => {
|
||||
if (beginStyle.hasOwnProperty(field)) {
|
||||
targetStyleMap[id][field] = shape.style[field];
|
||||
shape.style[field] = beginStyle[field];
|
||||
}
|
||||
});
|
||||
Promise.all(this.stopAnimations()).finally(() => {
|
||||
if (this.destroyed || this.visible) return;
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.show?.length) {
|
||||
const showAnimateFieldsMap: any = {};
|
||||
Object.values(animates.show).forEach((animate) => {
|
||||
const { shapeId = 'group' } = animate;
|
||||
showAnimateFieldsMap[shapeId] = (
|
||||
showAnimateFieldsMap[shapeId] || []
|
||||
).concat(animate.fields);
|
||||
});
|
||||
const targetStyleMap = {};
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
if (!this.cacheHiddenShape[id]) {
|
||||
// set the animate fields to initial value
|
||||
if (showAnimateFieldsMap[id]) {
|
||||
targetStyleMap[id] = targetStyleMap[id] || {};
|
||||
const beginStyle = getShapeAnimateBeginStyles(shape);
|
||||
showAnimateFieldsMap[id].forEach((field) => {
|
||||
if (beginStyle.hasOwnProperty(field)) {
|
||||
targetStyleMap[id][field] = shape.style[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) {
|
||||
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];
|
||||
}
|
||||
|
||||
this.animations = this.runWithAnimates(
|
||||
animates,
|
||||
'show',
|
||||
targetStyleMap,
|
||||
);
|
||||
} 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);
|
||||
} else {
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
if (!this.cacheHiddenShape[id]) shape.show();
|
||||
});
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
this.visible = true;
|
||||
});
|
||||
}
|
||||
|
||||
/** Hides the item. */
|
||||
public hide(animate = true) {
|
||||
// TODO: utilize graphcore's view
|
||||
this.stopAnimations();
|
||||
const func = () => {
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
@ -457,15 +465,22 @@ export default abstract class Item implements IItem {
|
||||
shape.hide();
|
||||
});
|
||||
};
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.hide?.length) {
|
||||
this.animations = this.runWithAnimates(animates, 'hide', undefined, func);
|
||||
} else {
|
||||
// 2. clear group and remove group
|
||||
func();
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
Promise.all(this.stopAnimations()).then(() => {
|
||||
if (this.destroyed || !this.visible) return;
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.hide?.length) {
|
||||
this.animations = this.runWithAnimates(
|
||||
animates,
|
||||
'hide',
|
||||
undefined,
|
||||
func,
|
||||
);
|
||||
} else {
|
||||
// 2. clear group and remove group
|
||||
func();
|
||||
}
|
||||
this.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns the visibility of the item. */
|
||||
@ -606,7 +621,11 @@ export default abstract class Item implements IItem {
|
||||
* @param previousStates previous states
|
||||
* @returns
|
||||
*/
|
||||
private drawWithStates(previousStates: State[], onfinish?: Function) {
|
||||
private drawWithStates(
|
||||
previousStates: State[],
|
||||
animate = true,
|
||||
onfinish?: Function,
|
||||
) {
|
||||
const { default: _, ...themeStateStyles } = this.themeStyles;
|
||||
const { data: displayModelData } = this.displayModel;
|
||||
let styles = {}; // merged styles
|
||||
@ -675,6 +694,7 @@ export default abstract class Item implements IItem {
|
||||
previous: previousStates,
|
||||
current: this.states,
|
||||
},
|
||||
animate,
|
||||
onfinish,
|
||||
);
|
||||
}
|
||||
@ -729,8 +749,27 @@ export default abstract class Item implements IItem {
|
||||
* Stop all the animations on the item.
|
||||
*/
|
||||
public stopAnimations() {
|
||||
this.animations?.forEach(stopAnimate);
|
||||
this.animations = [];
|
||||
const promises = [];
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,8 +20,8 @@ interface IProps {
|
||||
model: NodeModel | ComboModel;
|
||||
renderExtensions: any;
|
||||
containerGroup: Group;
|
||||
mapper: DisplayMapper;
|
||||
stateMapper: {
|
||||
mapper?: DisplayMapper;
|
||||
stateMapper?: {
|
||||
[stateName: string]: DisplayMapper;
|
||||
};
|
||||
zoom?: number;
|
||||
@ -36,7 +36,7 @@ interface IProps {
|
||||
}
|
||||
export default class Node extends Item {
|
||||
public type: 'node' | 'combo';
|
||||
private anchorPointsCache: Point[];
|
||||
private anchorPointsCache: Point[] | undefined;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
@ -45,6 +45,7 @@ export default class Node extends Item {
|
||||
this.displayModel as NodeDisplayModel | ComboDisplayModel,
|
||||
undefined,
|
||||
undefined,
|
||||
!this.displayModel.data.disableAnimate,
|
||||
props.onfinish,
|
||||
);
|
||||
}
|
||||
@ -55,6 +56,7 @@ export default class Node extends Item {
|
||||
current: NodeModelData | ComboModelData;
|
||||
},
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
animate = true,
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
const { group, renderExt, shapeMap: prevShapeMap, model } = this;
|
||||
@ -78,13 +80,13 @@ export default class Node extends Item {
|
||||
} else {
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
this.updatePosition(displayModel, diffData, onfinish);
|
||||
this.updatePosition(displayModel, diffData, animate, onfinish);
|
||||
}
|
||||
|
||||
const { haloShape } = this.shapeMap;
|
||||
haloShape?.toBack();
|
||||
|
||||
super.draw(displayModel, diffData, diffState, onfinish);
|
||||
super.draw(displayModel, diffData, diffState, animate, onfinish);
|
||||
this.anchorPointsCache = undefined;
|
||||
renderExt.updateCache(this.shapeMap);
|
||||
|
||||
@ -94,7 +96,7 @@ export default class Node extends Item {
|
||||
}
|
||||
|
||||
// handle shape's and group's animate
|
||||
if (!disableAnimate && animates) {
|
||||
if (animate && !disableAnimate && animates) {
|
||||
const animatesExcludePosition = getAnimatesExcludePosition(animates);
|
||||
this.animations = animateShapes(
|
||||
animatesExcludePosition, // animates
|
||||
@ -104,7 +106,7 @@ export default class Node extends Item {
|
||||
firstRendering ? 'buildIn' : 'update',
|
||||
this.changedStates,
|
||||
this.animateFrameListener,
|
||||
() => onfinish(model.id),
|
||||
(canceled) => onfinish(model.id, canceled),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -121,9 +123,18 @@ export default class Node extends Item {
|
||||
lodStrategy: LodStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
animate?: boolean,
|
||||
onfinish?: Function,
|
||||
) {
|
||||
super.update(model, diffData, isReplace, theme, onlyMove, onfinish);
|
||||
super.update(
|
||||
model,
|
||||
diffData,
|
||||
isReplace,
|
||||
theme,
|
||||
onlyMove,
|
||||
animate,
|
||||
onfinish,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,32 +147,54 @@ export default class Node extends Item {
|
||||
previous: NodeModelData | ComboModelData;
|
||||
current: NodeModelData | ComboModelData;
|
||||
},
|
||||
animate?: boolean,
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
const { group } = this;
|
||||
const { x, y, z = 0, animates, disableAnimate } = displayModel.data;
|
||||
if (isNaN(x) || isNaN(y) || isNaN(z)) return;
|
||||
if (!disableAnimate && animates?.update) {
|
||||
const {
|
||||
fx,
|
||||
fy,
|
||||
fz,
|
||||
x,
|
||||
y,
|
||||
z = 0,
|
||||
animates,
|
||||
disableAnimate,
|
||||
} = displayModel.data;
|
||||
const position = {
|
||||
x: fx === undefined ? x : (fx as number),
|
||||
y: fy === undefined ? y : (fy as number),
|
||||
z: fz === undefined ? z : (fz as number),
|
||||
};
|
||||
if (
|
||||
isNaN(position.x as number) ||
|
||||
isNaN(position.y as number) ||
|
||||
isNaN(position.z as number)
|
||||
)
|
||||
return;
|
||||
if (animate && !disableAnimate && animates?.update) {
|
||||
const groupAnimates = animates.update.filter(
|
||||
({ shapeId, fields }) =>
|
||||
({ shapeId, fields = [] }) =>
|
||||
(!shapeId || shapeId === 'group') &&
|
||||
(fields.includes('x') || fields.includes('y')),
|
||||
);
|
||||
if (groupAnimates.length) {
|
||||
animateShapes(
|
||||
const animations = animateShapes(
|
||||
{ update: groupAnimates },
|
||||
{ group: { x, y, z } } as any, // targetStylesMap
|
||||
{ group: position } as any, // targetStylesMap
|
||||
this.shapeMap, // shapeMap
|
||||
group,
|
||||
'update',
|
||||
[],
|
||||
this.animateFrameListener,
|
||||
() => onfinish(displayModel.id),
|
||||
(canceled) => onfinish(displayModel.id, canceled),
|
||||
);
|
||||
this.groupAnimations = animations;
|
||||
return;
|
||||
}
|
||||
}
|
||||
group.setLocalPosition(x, y, z);
|
||||
group.setLocalPosition(position.x, position.y, position.z);
|
||||
onfinish(displayModel.id, !animate);
|
||||
}
|
||||
|
||||
public clone(
|
||||
@ -247,8 +280,8 @@ export default class Node extends Item {
|
||||
keyShape.getLocalBounds();
|
||||
intersectPoint = getRectIntersectByPoint(
|
||||
{
|
||||
x: bbox.halfExtents[0],
|
||||
y: bbox.halfExtents[1],
|
||||
x: x + bbox.min[0],
|
||||
y: y + bbox.min[1],
|
||||
width: bbox.max[0] - bbox.min[0],
|
||||
height: bbox.max[1] - bbox.min[1],
|
||||
},
|
||||
@ -294,7 +327,16 @@ export default class Node extends Item {
|
||||
}
|
||||
|
||||
public getPosition(): Point {
|
||||
const { x = 0, y = 0, z = 0 } = this.model.data;
|
||||
return { x: x as number, y: y as number, z: z as number };
|
||||
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, fx, fy, fz } = this.model.data;
|
||||
return {
|
||||
x: (fx === undefined ? x : fx) as number,
|
||||
y: (fy === undefined ? y : fy) as number,
|
||||
z: (fz === undefined ? z : fz) as number,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { Graph as GraphLib, GraphView, ID } from '@antv/graphlib';
|
||||
import {
|
||||
clone,
|
||||
isArray,
|
||||
isFunction,
|
||||
isNumber,
|
||||
isObject,
|
||||
isString,
|
||||
} from '@antv/util';
|
||||
import { clone, isArray, isObject } from '@antv/util';
|
||||
import { registery as registry } from '../../stdlib';
|
||||
import { ComboModel, ComboUserModel, GraphData, IGraph } from '../../types';
|
||||
import { ComboUserModelData } from '../../types/combo';
|
||||
import { DataChangeType, GraphCore } from '../../types/data';
|
||||
import {
|
||||
DataChangeType,
|
||||
DataConfig,
|
||||
FetchDataConfig,
|
||||
GraphCore,
|
||||
InlineGraphDataConfig,
|
||||
InlineTreeDataConfig,
|
||||
} from '../../types/data';
|
||||
import {
|
||||
EdgeModel,
|
||||
EdgeModelData,
|
||||
@ -28,9 +28,10 @@ import { getExtension } from '../../util/extension';
|
||||
import {
|
||||
deconstructData,
|
||||
graphComboTreeDfs,
|
||||
graphCoreTreeDfs,
|
||||
isSucceed,
|
||||
graphData2TreeData,
|
||||
treeData2GraphData,
|
||||
validateComboStrucutre,
|
||||
traverse,
|
||||
} from '../../util/data';
|
||||
|
||||
/**
|
||||
@ -49,8 +50,6 @@ export class DataController {
|
||||
*/
|
||||
public graphCore: GraphCore;
|
||||
|
||||
private comboTreeView: GraphView<any, any>;
|
||||
|
||||
constructor(graph: IGraph<any, any>) {
|
||||
this.graph = graph;
|
||||
this.tap();
|
||||
@ -142,6 +141,9 @@ export class DataController {
|
||||
private tap() {
|
||||
this.extensions = this.getExtensions();
|
||||
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.
|
||||
* @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 change = () => {
|
||||
switch (changeType) {
|
||||
case 'remove':
|
||||
this.removeData(data);
|
||||
this.removeData(data as GraphData);
|
||||
break;
|
||||
case 'update':
|
||||
this.updateData(data);
|
||||
this.updateData(data as GraphData);
|
||||
break;
|
||||
case 'moveCombo':
|
||||
this.moveCombo(data);
|
||||
this.moveCombo(data as GraphData);
|
||||
break;
|
||||
case 'addCombo':
|
||||
this.addCombo(data);
|
||||
this.addCombo(data as GraphData);
|
||||
break;
|
||||
default:
|
||||
// 'replace' | 'mergeReplace' | 'union'
|
||||
// changeType is 'replace' | 'mergeReplace' | 'union'
|
||||
this.changeData(data, changeType);
|
||||
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.
|
||||
* @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
|
||||
*/
|
||||
private changeData(
|
||||
data: GraphData,
|
||||
dataConfig: DataConfig,
|
||||
changeType: 'replace' | 'mergeReplace' | 'union',
|
||||
) {
|
||||
const { userGraphCore } = this;
|
||||
const { type: dataType, data } = this.formatData(dataConfig) || {};
|
||||
if (!dataType) return;
|
||||
|
||||
if (changeType === 'replace') {
|
||||
this.userGraphCore = new GraphLib<NodeUserModelData, EdgeUserModelData>({
|
||||
nodes: data.nodes.concat(
|
||||
@ -224,10 +240,6 @@ export class DataController {
|
||||
edges,
|
||||
});
|
||||
if (combos?.length) {
|
||||
this.comboTreeView = new GraphView<NodeModelData, EdgeModelData>({
|
||||
graph: this.graphCore,
|
||||
cache: 'manual',
|
||||
});
|
||||
this.graphCore.attachTreeStructure('combo');
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.parentId) {
|
||||
@ -257,9 +269,10 @@ export class DataController {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { userGraphCore } = this;
|
||||
const prevData = deconstructData({
|
||||
nodes: userGraphCore.getAllNodes(),
|
||||
edges: userGraphCore.getAllEdges(),
|
||||
edges: [],
|
||||
});
|
||||
const { nodes = [], edges = [], combos = [] } = data;
|
||||
const nodesAndCombos = nodes.concat(
|
||||
@ -294,6 +307,7 @@ export class DataController {
|
||||
}
|
||||
|
||||
// =========== edge ============
|
||||
prevData.edges = userGraphCore.getAllEdges();
|
||||
if (!prevData.edges.length) {
|
||||
userGraphCore.addEdges(edges);
|
||||
} 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();
|
||||
if (prevNodesAndCombos.length && nodesAndCombos.length) {
|
||||
// update the parentId
|
||||
nodesAndCombos.forEach((item) => {
|
||||
const { parentId } = item.data;
|
||||
this.graphCore.getChildren(item.id, 'combo').forEach((child) => {
|
||||
userGraphCore.mergeNodeData(child.id, { parentId });
|
||||
if (this.graphCore.hasTreeStructure('combo')) {
|
||||
nodesAndCombos.forEach((item) => {
|
||||
const { parentId } = item.data;
|
||||
this.graphCore.getChildren(item.id, 'combo').forEach((child) => {
|
||||
userGraphCore.mergeNodeData(child.id, { parentId });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// remove the node
|
||||
userGraphCore.removeNodes(nodesAndCombos.map((node) => node.id));
|
||||
}
|
||||
@ -353,9 +378,11 @@ export class DataController {
|
||||
* Update part of old data.
|
||||
* @param data data to be updated which is part of old one
|
||||
*/
|
||||
private updateData(data: GraphData) {
|
||||
private updateData(dataConfig: DataConfig) {
|
||||
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 {
|
||||
nodes: prevNodes,
|
||||
edges: prevEdges,
|
||||
@ -453,6 +480,38 @@ export class DataController {
|
||||
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 if (!(data as GraphData).nodes) {
|
||||
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: {
|
||||
[id: string]: boolean;
|
||||
} = {};
|
||||
const treeChanges = [];
|
||||
event.changes.forEach((change) => {
|
||||
const { value, id } = change;
|
||||
changeMap[id || value.id] = true;
|
||||
const id = change.id || change.value?.id;
|
||||
if (id !== undefined) {
|
||||
changeMap[id] = true;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
[
|
||||
'TreeStructureAttached',
|
||||
'TreeStructureChanged',
|
||||
'TreeStructureChanged',
|
||||
].includes(change.type)
|
||||
) {
|
||||
treeChanges.push(change);
|
||||
}
|
||||
});
|
||||
nodes.forEach((model) => {
|
||||
newModelMap[model.id] = { type: 'node', model };
|
||||
@ -633,6 +705,7 @@ export class DataController {
|
||||
const { model: newModel } = newModelMap[id] || {};
|
||||
// remove
|
||||
if (!newModel) {
|
||||
// remove a combo, put the children to upper parent
|
||||
if (prevModel.data._isCombo) {
|
||||
graphCore.getChildren(id, 'combo').forEach((child) => {
|
||||
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) {
|
||||
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);
|
||||
delete parentMap[prevModel.id];
|
||||
}
|
||||
@ -682,6 +789,21 @@ export class DataController {
|
||||
graphCore.mergeNodeData(id, { parentId: parentMap[id].new });
|
||||
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 {
|
||||
// situation 2: idMaps is complete
|
||||
// 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;
|
||||
changes.forEach((change) => {
|
||||
const { value, id, type } = change;
|
||||
// TODO: temporary skip. how to handle tree change events?
|
||||
if (
|
||||
[
|
||||
'TreeStructureAttached',
|
||||
'TreeStructureDetached',
|
||||
'TreeStructureChanged',
|
||||
].includes(type)
|
||||
)
|
||||
if (type === 'TreeStructureAttached') {
|
||||
graphCore.attachTreeStructure(change.treeKey);
|
||||
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;
|
||||
changeMap[dataId] = changeMap[dataId] || [];
|
||||
changeMap[dataId].push(type.toLawerCase());
|
||||
@ -727,15 +851,14 @@ export class DataController {
|
||||
const oldValue = prevModelMap[newId];
|
||||
const isNodeOrCombo = graphCore.hasNode(newId);
|
||||
if (newValue && !oldValue) {
|
||||
const addFunc = isNodeOrCombo
|
||||
? graphCore.addNode
|
||||
: graphCore.addEdge;
|
||||
addFunc(newValue);
|
||||
isNodeOrCombo
|
||||
? graphCore.addNode(newValue)
|
||||
: graphCore.addEdge(newValue);
|
||||
} else if (!newValue && oldValue) {
|
||||
const removeFunc = isNodeOrCombo
|
||||
? graphCore.removeNode
|
||||
: graphCore.removeEdge;
|
||||
removeFunc(newId);
|
||||
isNodeOrCombo
|
||||
? graphCore.removeNode(newId)
|
||||
: graphCore.removeEdge(newId);
|
||||
// TODO: update combo tree and tree graph
|
||||
} else {
|
||||
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)
|
||||
@ -748,11 +871,9 @@ export class DataController {
|
||||
isNodeOrCombo,
|
||||
diff,
|
||||
);
|
||||
} else {
|
||||
} else if (changeMap[comesFromIds[0]]?.length) {
|
||||
// follow the corresponding data event in userGraphCore
|
||||
const comesFromChanges = changeMap[comesFromIds[0]];
|
||||
if (comesFromChanges?.length)
|
||||
syncUpdateToGraphCore(newId, newValue, oldValue, isNodeOrCombo);
|
||||
syncUpdateToGraphCore(newId, newValue, oldValue, isNodeOrCombo);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -771,7 +892,6 @@ export class DataController {
|
||||
graphCore.setParent(node.id, node.data.parentId as ID, 'combo');
|
||||
});
|
||||
}
|
||||
this.comboTreeView?.refreshCache();
|
||||
});
|
||||
}
|
||||
|
||||
@ -798,6 +918,34 @@ export class DataController {
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Animation, DisplayObject, IAnimationEffectTiming } from '@antv/g';
|
||||
import Hierarchy from '@antv/hierarchy';
|
||||
import { Graph as GraphLib } from '@antv/graphlib';
|
||||
import {
|
||||
isLayoutWithIterations,
|
||||
@ -17,6 +18,7 @@ import {
|
||||
} from '../../types';
|
||||
import { GraphCore } from '../../types/data';
|
||||
import { EdgeModelData } from '../../types/edge';
|
||||
import { layoutOneTree } from '../../util/layout';
|
||||
|
||||
/**
|
||||
* Manages layout extensions and graph layout.
|
||||
@ -48,6 +50,7 @@ export class LayoutController {
|
||||
private async onLayout(params: {
|
||||
graphCore: GraphCore;
|
||||
options: LayoutOptions;
|
||||
animate?: boolean;
|
||||
}) {
|
||||
/**
|
||||
* The final calculated result.
|
||||
@ -57,13 +60,15 @@ export class LayoutController {
|
||||
// Stop currentLayout if any.
|
||||
this.stopLayout();
|
||||
|
||||
const { graphCore, options } = params;
|
||||
const { graphCore, options, animate = true } = params;
|
||||
const layoutNodes = graphCore
|
||||
.getAllNodes()
|
||||
.filter((node) => node.data.visible !== false && !node.data._isCombo);
|
||||
.filter(
|
||||
(node) => this.graph.getItemVisible(node.id) && !node.data._isCombo,
|
||||
);
|
||||
const layoutNodesIdMap = {};
|
||||
layoutNodes.forEach((node) => (layoutNodesIdMap[node.id] = true));
|
||||
const layoutGraphCore = new GraphLib<NodeModelData, EdgeModelData>({
|
||||
const layoutData = {
|
||||
nodes: layoutNodes,
|
||||
edges: graphCore
|
||||
.getAllEdges()
|
||||
@ -71,7 +76,10 @@ export class LayoutController {
|
||||
(edge) =>
|
||||
layoutNodesIdMap[edge.source] && layoutNodesIdMap[edge.target],
|
||||
),
|
||||
});
|
||||
};
|
||||
const layoutGraphCore = new GraphLib<NodeModelData, EdgeModelData>(
|
||||
layoutData,
|
||||
);
|
||||
|
||||
this.graph.emit('startlayout');
|
||||
|
||||
@ -112,6 +120,19 @@ export class LayoutController {
|
||||
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.
|
||||
const layout = new layoutCtor(rest);
|
||||
this.currentLayout = layout;
|
||||
@ -170,7 +191,57 @@ export class LayoutController {
|
||||
this.graph.emit('endlayout');
|
||||
|
||||
// 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() {
|
||||
@ -202,8 +273,8 @@ export class LayoutController {
|
||||
}
|
||||
}
|
||||
|
||||
private updateNodesPosition(positions: LayoutMapping) {
|
||||
this.graph.updateNodePosition(positions.nodes);
|
||||
private updateNodesPosition(positions: LayoutMapping, animate = true) {
|
||||
this.graph.updateNodePosition(positions.nodes, undefined, !animate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,10 +14,10 @@ import { CameraAnimationOptions } from '../types/animate';
|
||||
import { BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
|
||||
import { ComboModel } from '../types/combo';
|
||||
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 { Hooks, ViewportChangeHookParams } from '../types/hook';
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../types/item';
|
||||
import { ITEM_TYPE, SHAPE_TYPE, ShapeStyle } from '../types/item';
|
||||
import {
|
||||
ImmediatelyInvokedLayoutOptions,
|
||||
LayoutOptions,
|
||||
@ -152,10 +152,12 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
this.canvas = canvas;
|
||||
this.backgroundCanvas = backgroundCanvas;
|
||||
this.transientCanvas = transientCanvas;
|
||||
this.container = container as HTMLDivElement;
|
||||
} else {
|
||||
const containerDOM = isString(container)
|
||||
? document.getElementById(container as string)
|
||||
: (container as HTMLElement);
|
||||
|
||||
if (!containerDOM) {
|
||||
console.error(
|
||||
`Create graph failed. The container for graph ${containerDOM} is not exist.`,
|
||||
@ -163,6 +165,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.container = containerDOM;
|
||||
const size = [width, height];
|
||||
if (size[0] === undefined) {
|
||||
@ -250,6 +253,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
changes: GraphChange<NodeModelData, EdgeModelData>[];
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
animate?: boolean;
|
||||
upsertAncestors?: boolean;
|
||||
}>({ name: 'itemchange' }),
|
||||
render: new Hook<{
|
||||
@ -259,7 +263,11 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
}>({
|
||||
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' }),
|
||||
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
|
||||
behaviorchange: new Hook<{
|
||||
@ -309,6 +317,12 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
transient: Canvas;
|
||||
};
|
||||
}>({ name: 'init' }),
|
||||
treecollapseexpand: new Hook<{
|
||||
ids: ID[];
|
||||
animate: boolean;
|
||||
action: 'collapse' | 'expand';
|
||||
graphCore: GraphCore;
|
||||
}>({ name: 'treecollapseexpand' }),
|
||||
destroy: new Hook<{}>({ name: 'destroy' }),
|
||||
};
|
||||
}
|
||||
@ -355,7 +369,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns
|
||||
* @group Data
|
||||
*/
|
||||
public async read(data: GraphData) {
|
||||
public async read(data: DataConfig) {
|
||||
this.hooks.datachange.emit({ data, type: 'replace' });
|
||||
const emitRender = async () => {
|
||||
this.hooks.render.emit({
|
||||
@ -387,7 +401,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @group Data
|
||||
*/
|
||||
public async changeData(
|
||||
data: GraphData,
|
||||
data: DataConfig,
|
||||
type: 'replace' | 'mergeReplace' = 'mergeReplace',
|
||||
) {
|
||||
this.hooks.datachange.emit({ data, type });
|
||||
@ -1028,9 +1042,21 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
upsertAncestors?: boolean,
|
||||
disableAnimate = false,
|
||||
callback?: (
|
||||
model: NodeModel | EdgeModel | ComboModel,
|
||||
canceled?: boolean,
|
||||
) => void,
|
||||
stack?: boolean,
|
||||
) {
|
||||
return this.updatePosition('node', models, upsertAncestors, stack);
|
||||
return this.updatePosition(
|
||||
'node',
|
||||
models,
|
||||
upsertAncestors,
|
||||
disableAnimate,
|
||||
callback,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1048,9 +1074,18 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
upsertAncestors?: boolean,
|
||||
disableAnimate = false,
|
||||
callback?: (model: NodeModel | EdgeModel | ComboModel) => void,
|
||||
stack?: boolean,
|
||||
) {
|
||||
return this.updatePosition('combo', models, upsertAncestors, stack);
|
||||
return this.updatePosition(
|
||||
'combo',
|
||||
models,
|
||||
upsertAncestors,
|
||||
disableAnimate,
|
||||
callback,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
|
||||
private updatePosition(
|
||||
@ -1061,6 +1096,11 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
upsertAncestors?: boolean,
|
||||
disableAnimate = false,
|
||||
callback?: (
|
||||
model: NodeModel | EdgeModel | ComboModel,
|
||||
canceled?: boolean,
|
||||
) => void,
|
||||
stack?: boolean,
|
||||
) {
|
||||
const modelArr = isArray(models) ? models : [models];
|
||||
@ -1075,6 +1115,8 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
theme: specification,
|
||||
upsertAncestors,
|
||||
action: 'updatePosition',
|
||||
animate: !disableAnimate,
|
||||
callback,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type,
|
||||
@ -1105,13 +1147,13 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns
|
||||
* @group Item
|
||||
*/
|
||||
public showItem(ids: ID | ID[], disableAniamte?: boolean) {
|
||||
public showItem(ids: ID | ID[], disableAnimate = false) {
|
||||
const idArr = isArray(ids) ? ids : [ids];
|
||||
this.hooks.itemvisibilitychange.emit({
|
||||
ids: idArr as ID[],
|
||||
value: true,
|
||||
graphCore: this.dataController.graphCore,
|
||||
animate: !disableAniamte,
|
||||
animate: !disableAnimate,
|
||||
});
|
||||
}
|
||||
/**
|
||||
@ -1120,13 +1162,13 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns
|
||||
* @group Item
|
||||
*/
|
||||
public hideItem(ids: ID | ID[], disableAniamte?: boolean) {
|
||||
public hideItem(ids: ID | ID[], disableAnimate = false) {
|
||||
const idArr = isArray(ids) ? ids : [ids];
|
||||
this.hooks.itemvisibilitychange.emit({
|
||||
ids: idArr as ID[],
|
||||
value: false,
|
||||
graphCore: this.dataController.graphCore,
|
||||
animate: !disableAniamte,
|
||||
animate: !disableAnimate,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1364,7 +1406,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
/**
|
||||
* Layout the graph (with current configurations if cfg is not assigned).
|
||||
*/
|
||||
public async layout(options?: LayoutOptions) {
|
||||
public async layout(options?: LayoutOptions, disableAnimate = false) {
|
||||
const { graphCore } = this.dataController;
|
||||
const formattedOptions = {
|
||||
...this.getSpecification().layout,
|
||||
@ -1400,6 +1442,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
await this.hooks.layout.emitLinearAsync({
|
||||
graphCore,
|
||||
options: formattedOptions,
|
||||
animate: !disableAnimate,
|
||||
});
|
||||
this.emit('afterlayout');
|
||||
}
|
||||
@ -1653,6 +1696,39 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
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 = 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 = 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.
|
||||
* @returns
|
||||
|
@ -67,7 +67,7 @@ export default class ClickSelect extends Behavior {
|
||||
*/
|
||||
private canvasPointerDown: Point | undefined = undefined;
|
||||
private canvasPointerMove = false;
|
||||
private timeout: NodeJS.Timeout = undefined;
|
||||
private timeout: ReturnType<typeof setTimeout> = undefined;
|
||||
|
||||
constructor(options: Partial<ClickSelectOptions>) {
|
||||
super(Object.assign({}, DEFAULT_OPTIONS, options));
|
||||
|
106
packages/g6/src/stdlib/behavior/collapse-expand-tree.ts
Normal 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 {
|
||||
private timeout: ReturnType<typeof setTimeout> = 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;
|
||||
}
|
||||
this.graph.frontItem(itemId);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -206,7 +206,7 @@ export default class DragNode extends Behavior {
|
||||
this.originPositions = selectedNodeIds
|
||||
.map((id) => {
|
||||
if (!this.graph.getNodeData(id)) {
|
||||
console.log('node does not exist', id);
|
||||
console.warn('node does not exist', id);
|
||||
return;
|
||||
}
|
||||
const { x, y } = this.graph.getNodeData(id).data as {
|
||||
|
@ -74,7 +74,7 @@ export default class ZoomCanvas extends Behavior {
|
||||
private speedupKeydown: boolean;
|
||||
private hiddenEdgeIds: ID[];
|
||||
private hiddenNodeIds: ID[];
|
||||
private zoomTimer: NodeJS.Timeout;
|
||||
private zoomTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(options: Partial<ZoomCanvasOptions>) {
|
||||
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options);
|
||||
@ -91,7 +91,7 @@ export default class ZoomCanvas extends Behavior {
|
||||
this.graph.canvas
|
||||
.getContextService()
|
||||
.getDomElement()
|
||||
.addEventListener(
|
||||
.addEventListener?.(
|
||||
'wheel',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -1,40 +1,50 @@
|
||||
import Hierarchy from '@antv/hierarchy';
|
||||
import { registry as layoutRegistry } from '@antv/layout';
|
||||
import { Lib } from '../types/stdlib';
|
||||
import ActivateRelations from './behavior/activate-relations';
|
||||
import BrushSelect from './behavior/brush-select';
|
||||
import ClickSelect from './behavior/click-select';
|
||||
import DragCanvas from './behavior/drag-canvas';
|
||||
import LassoSelect from './behavior/lasso-select';
|
||||
import DragNode from './behavior/drag-node';
|
||||
import DragCombo from './behavior/drag-combo';
|
||||
import DragNode from './behavior/drag-node';
|
||||
import LassoSelect from './behavior/lasso-select';
|
||||
import { comboFromNode } from './data/comboFromNode';
|
||||
import { LineEdge } from './item/edge';
|
||||
import { CircleNode, SphereNode, RectNode } from './item/node';
|
||||
import {
|
||||
CircleNode,
|
||||
EllipseNode,
|
||||
RectNode,
|
||||
SphereNode,
|
||||
TriangleNode,
|
||||
} from './item/node';
|
||||
import DarkTheme from './theme/dark';
|
||||
import LightTheme from './theme/light';
|
||||
import SpecThemeSolver from './themeSolver/spec';
|
||||
import SubjectThemeSolver from './themeSolver/subject';
|
||||
|
||||
import lassoSelector from './selector/lasso';
|
||||
import rectSelector from './selector/rect';
|
||||
import Minimap from './plugin/minimap';
|
||||
import Fisheye from './plugin/fisheye';
|
||||
import Legend from './plugin/legend';
|
||||
import Grid from './plugin/grid';
|
||||
import Tooltip from './plugin/tooltip';
|
||||
import Menu from './plugin/menu';
|
||||
import ZoomCanvas from './behavior/zoom-canvas';
|
||||
import ZoomCanvas3D from './behavior/zoom-canvas-3d';
|
||||
import CollapseExpandCombo from './behavior/collapse-expand-combo';
|
||||
import HoverActivate from './behavior/hover-activate';
|
||||
import OrbitCanvas3D from './behavior/orbit-canvas-3d';
|
||||
import RotateCanvas3D from './behavior/rotate-canvas-3d';
|
||||
import TrackCanvas3D from './behavior/track-canvas-3d';
|
||||
import OrbitCanvas3D from './behavior/orbit-canvas-3d';
|
||||
import HoverActivate from './behavior/hover-activate';
|
||||
import CollapseExpandCombo from './behavior/collapse-expand-combo';
|
||||
import ZoomCanvas from './behavior/zoom-canvas';
|
||||
import ZoomCanvas3D from './behavior/zoom-canvas-3d';
|
||||
import { CircleCombo } from './item/combo/circle';
|
||||
|
||||
import CollapseExpandTree from './behavior/collapse-expand-tree';
|
||||
import { CubicEdge } from './item/edge/cubic';
|
||||
import { CubicHorizonEdge } from './item/edge/cubic-horizon';
|
||||
import { CubicVerticalEdge } from './item/edge/cubic-vertical';
|
||||
import { Quadratic } from './item/edge/quadratic';
|
||||
import { CircleCombo } from './item/combo/circle';
|
||||
import Fisheye from './plugin/fisheye';
|
||||
import Grid from './plugin/grid';
|
||||
import Legend from './plugin/legend';
|
||||
import Menu from './plugin/menu';
|
||||
import Minimap from './plugin/minimap';
|
||||
import toolbar from './plugin/toolbar';
|
||||
import Tooltip from './plugin/tooltip';
|
||||
import lassoSelector from './selector/lasso';
|
||||
import rectSelector from './selector/rect';
|
||||
|
||||
const stdLib = {
|
||||
transforms: {
|
||||
@ -48,7 +58,10 @@ const stdLib = {
|
||||
spec: SpecThemeSolver,
|
||||
subject: SubjectThemeSolver,
|
||||
},
|
||||
layouts: layoutRegistry,
|
||||
layouts: {
|
||||
...layoutRegistry,
|
||||
...Hierarchy,
|
||||
},
|
||||
behaviors: {
|
||||
'activate-relations': ActivateRelations,
|
||||
'drag-canvas': DragCanvas,
|
||||
@ -57,6 +70,7 @@ const stdLib = {
|
||||
'drag-node': DragNode,
|
||||
'drag-combo': DragCombo,
|
||||
'collapse-expand-combo': CollapseExpandCombo,
|
||||
'collapse-expand-tree': CollapseExpandTree,
|
||||
'click-select': ClickSelect,
|
||||
'brush-select': BrushSelect,
|
||||
'lasso-select': LassoSelect,
|
||||
@ -72,11 +86,14 @@ const stdLib = {
|
||||
grid: Grid,
|
||||
tooltip: Tooltip,
|
||||
menu: Menu,
|
||||
toolbar,
|
||||
},
|
||||
nodes: {
|
||||
'circle-node': CircleNode,
|
||||
'sphere-node': SphereNode,
|
||||
'rect-node': RectNode,
|
||||
'triangle-node': TriangleNode,
|
||||
'ellipse-node': EllipseNode,
|
||||
},
|
||||
edges: {
|
||||
'line-edge': LineEdge,
|
||||
@ -108,4 +125,4 @@ const utils = {
|
||||
|
||||
const registery = { useLib };
|
||||
export default registery;
|
||||
export { stdLib, registery, utils };
|
||||
export { registery, stdLib, utils };
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IPoint } from '@antv/g6';
|
||||
import { vec2 } from '@antv/matrix-util';
|
||||
import { Point } from '../../../types/common';
|
||||
|
||||
import { CubicEdge } from './cubic';
|
||||
|
||||
export class CubicHorizonEdge extends CubicEdge {
|
||||
@ -49,13 +49,13 @@ export class CubicHorizonEdge extends CubicEdge {
|
||||
offset = -offset;
|
||||
}
|
||||
|
||||
const controlPoint1: IPoint = this.getControlPoint(
|
||||
const controlPoint1: Point = this.getControlPoint(
|
||||
startPoint,
|
||||
endPoint,
|
||||
percent,
|
||||
offset,
|
||||
);
|
||||
const controlPoint2: IPoint = this.getControlPoint(
|
||||
const controlPoint2: Point = this.getControlPoint(
|
||||
startPoint,
|
||||
endPoint,
|
||||
percent,
|
||||
@ -67,24 +67,24 @@ export class CubicHorizonEdge extends CubicEdge {
|
||||
|
||||
/**
|
||||
* control point calculated according to startPoint, endPoint, percent, and offset
|
||||
* @param {IPoint} startPoint source point position of edge (x, y)
|
||||
* @param {IPoint} endPoint target point position of edge (x, y)
|
||||
* @param {Point} startPoint source point position of edge (x, y)
|
||||
* @param {Point} endPoint target point position of edge (x, y)
|
||||
* @param {Number} percent the proportion of control points' in the segment, Range 0 to 1
|
||||
* @param {Number} offset the curveOffset
|
||||
* @return {IPoint} control point (x,y)
|
||||
* @return {Point} control point (x,y)
|
||||
*/
|
||||
protected getControlPoint: (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
percent: number,
|
||||
offset: number,
|
||||
) => IPoint = (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
) => Point = (
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
percent = 0,
|
||||
offset = 0,
|
||||
) => {
|
||||
const point: IPoint = {
|
||||
const point: Point = {
|
||||
x: (1 - percent) * startPoint.x + percent * endPoint.x,
|
||||
y: 0,
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { IPoint } from '@antv/g6';
|
||||
import { vec2 } from '@antv/matrix-util';
|
||||
import { Point } from '../../../types/common';
|
||||
import { CubicEdge } from './cubic';
|
||||
@ -25,8 +24,8 @@ export class CubicVerticalEdge extends CubicEdge {
|
||||
|
||||
/**
|
||||
* control point calculated according to startPoint, endPoint, percent, and offset
|
||||
* @param {IPoint} startPoint source point position of edge (x, y)
|
||||
* @param {IPoint} endPoint target point position of edge (x, y)
|
||||
* @param {Point} startPoint source point position of edge (x, y)
|
||||
* @param {Point} endPoint target point position of edge (x, y)
|
||||
* @param {Number} percent the proportion of control points' in the segment, Range 0 to 1
|
||||
* @param {Number} offset the curveOffset
|
||||
* @param controlPoints the control point position
|
||||
@ -49,13 +48,13 @@ export class CubicVerticalEdge extends CubicEdge {
|
||||
offset = -offset;
|
||||
}
|
||||
|
||||
const controlPoint1: IPoint = this.getControlPoint(
|
||||
const controlPoint1: Point = this.getControlPoint(
|
||||
startPoint,
|
||||
endPoint,
|
||||
percent,
|
||||
offset,
|
||||
);
|
||||
const controlPoint2: IPoint = this.getControlPoint(
|
||||
const controlPoint2: Point = this.getControlPoint(
|
||||
startPoint,
|
||||
endPoint,
|
||||
percent,
|
||||
@ -67,24 +66,24 @@ export class CubicVerticalEdge extends CubicEdge {
|
||||
|
||||
/**
|
||||
* control point calculated according to startPoint, endPoint, percent, and offset
|
||||
* @param {IPoint} startPoint source point position of edge (x, y)
|
||||
* @param {IPoint} endPoint target point position of edge (x, y)
|
||||
* @param {Point} startPoint source point position of edge (x, y)
|
||||
* @param {Point} endPoint target point position of edge (x, y)
|
||||
* @param {Number} percent the proportion of control points' in the segment, Range 0 to 1
|
||||
* @param {Number} offset the curveOffset
|
||||
* @return {IPoint} control point (x,y)
|
||||
* @return {Point} control point (x,y)
|
||||
*/
|
||||
protected getControlPoint: (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
percent: number,
|
||||
offset: number,
|
||||
) => IPoint = (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
) => Point = (
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
percent = 0,
|
||||
offset = 0,
|
||||
) => {
|
||||
const point: IPoint = {
|
||||
const point: Point = {
|
||||
x: 0,
|
||||
y: (1 - percent) * startPoint.y + percent * endPoint.y,
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { IPoint } from '@antv/g6';
|
||||
import { vec2 } from '@antv/matrix-util';
|
||||
import { State } from '../../../types/item';
|
||||
import { EdgeModelData, EdgeShapeMap } from '../../../types/edge';
|
||||
import { Point } from '../../../types/common';
|
||||
import { EdgeModelData, EdgeShapeMap } from '../../../types/edge';
|
||||
import { State } from '../../../types/item';
|
||||
|
||||
import { EdgeDisplayModel } from '../../../types';
|
||||
// eslint-disable-next-line import/namespace
|
||||
import { BaseEdge } from './base';
|
||||
@ -141,13 +141,13 @@ export class CubicEdge extends BaseEdge {
|
||||
controlPoints,
|
||||
offset = 20,
|
||||
) => {
|
||||
const controlPoint1: IPoint = this.getControlPoint(
|
||||
const controlPoint1: Point = this.getControlPoint(
|
||||
startPoint,
|
||||
endPoint,
|
||||
percent,
|
||||
offset,
|
||||
);
|
||||
const controlPoint2: IPoint = this.getControlPoint(
|
||||
const controlPoint2: Point = this.getControlPoint(
|
||||
startPoint,
|
||||
endPoint,
|
||||
percent,
|
||||
@ -159,24 +159,24 @@ export class CubicEdge extends BaseEdge {
|
||||
|
||||
/**
|
||||
* control point calculated according to startPoint, endPoint, percent, and offset
|
||||
* @param {IPoint} startPoint source point position of edge (x, y)
|
||||
* @param {IPoint} endPoint target point position of edge (x, y)
|
||||
* @param {Point} startPoint source point position of edge (x, y)
|
||||
* @param {Point} endPoint target point position of edge (x, y)
|
||||
* @param {Number} percent the proportion of control points' in the segment, Range 0 to 1
|
||||
* @param {Number} offset the curveOffset
|
||||
* @return {IPoint} control point (x,y)
|
||||
* @return {Point} control point (x,y)
|
||||
*/
|
||||
protected getControlPoint: (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
percent: number,
|
||||
offset: number,
|
||||
) => IPoint = (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
) => Point = (
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
percent = 0,
|
||||
offset = 0,
|
||||
) => {
|
||||
const point: IPoint = {
|
||||
const point: Point = {
|
||||
x: (1 - percent) * startPoint.x + percent * endPoint.x,
|
||||
y: (1 - percent) * startPoint.y + percent * endPoint.y,
|
||||
};
|
||||
|
@ -416,8 +416,9 @@ export abstract class BaseNode {
|
||||
nodeName as SHAPE_TYPE,
|
||||
'haloShape',
|
||||
{
|
||||
...attributes,
|
||||
...keyShapeStyle,
|
||||
// actual attributes in the keyShape has higher priority than the style config props of keyShape
|
||||
...attributes,
|
||||
stroke: attributes.fill,
|
||||
...haloShapeStyle,
|
||||
batchKey: 'halo',
|
||||
|
114
packages/g6/src/stdlib/item/node/ellipse.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { NodeDisplayModel } from '../../../types';
|
||||
import { State } from '../../../types/item';
|
||||
import {
|
||||
NodeModelData,
|
||||
NodeShapeMap,
|
||||
NodeShapeStyles,
|
||||
} from '../../../types/node';
|
||||
import { BaseNode } from './base';
|
||||
|
||||
export class EllipseNode extends BaseNode {
|
||||
override defaultStyles = {
|
||||
keyShape: {
|
||||
rx: 30,
|
||||
ry: 20,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
mergedStyles: NodeShapeStyles;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// suggest to merge default styles like this to avoid style value missing
|
||||
// this.defaultStyles = mergeStyles([this.baseDefaultStyles, this.defaultStyles]);
|
||||
}
|
||||
public draw(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): NodeShapeMap {
|
||||
const { data = {} } = model;
|
||||
let shapes: NodeShapeMap = { keyShape: undefined };
|
||||
|
||||
// keyShape
|
||||
shapes.keyShape = this.drawKeyShape(model, shapeMap, diffData);
|
||||
|
||||
// haloShape
|
||||
if (data.haloShape && this.drawHaloShape) {
|
||||
shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// labelShape
|
||||
if (data.labelShape) {
|
||||
shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// labelBackgroundShape
|
||||
if (data.labelBackgroundShape) {
|
||||
shapes.labelBackgroundShape = this.drawLabelBackgroundShape(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
);
|
||||
}
|
||||
|
||||
// anchor shapes
|
||||
if (data.anchorShapes) {
|
||||
const anchorShapes = this.drawAnchorShapes(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
diffState,
|
||||
);
|
||||
shapes = {
|
||||
...shapes,
|
||||
...anchorShapes,
|
||||
};
|
||||
}
|
||||
|
||||
// iconShape
|
||||
if (data.iconShape) {
|
||||
shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// badgeShape
|
||||
if (data.badgeShapes) {
|
||||
const badgeShapes = this.drawBadgeShapes(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
diffState,
|
||||
);
|
||||
shapes = {
|
||||
...shapes,
|
||||
...badgeShapes,
|
||||
};
|
||||
}
|
||||
|
||||
// otherShapes
|
||||
if (data.otherShapes && this.drawOtherShapes) {
|
||||
shapes = {
|
||||
...shapes,
|
||||
...this.drawOtherShapes(model, shapeMap, diffData),
|
||||
};
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
public drawKeyShape(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
return this.upsertShape(
|
||||
'ellipse',
|
||||
'keyShape',
|
||||
this.mergedStyles.keyShape,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
export * from './circle';
|
||||
export * from './sphere';
|
||||
export * from './rect';
|
||||
export * from './triangle';
|
||||
export * from './ellipse';
|
||||
|
308
packages/g6/src/stdlib/item/node/triangle.ts
Normal file
@ -0,0 +1,308 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { NodeDisplayModel } from '../../../types';
|
||||
import {
|
||||
ComboDisplayModel,
|
||||
ComboModelData,
|
||||
ComboShapeMap,
|
||||
} from '../../../types/combo';
|
||||
import { GShapeStyle, State } from '../../../types/item';
|
||||
import {
|
||||
NodeModelData,
|
||||
NodeShapeMap,
|
||||
NodeShapeStyles,
|
||||
} from '../../../types/node';
|
||||
import { BaseNode } from './base';
|
||||
|
||||
type PathArray = any;
|
||||
|
||||
export class TriangleNode extends BaseNode {
|
||||
override defaultStyles = {
|
||||
keyShape: {
|
||||
r: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
direction: 'up', //'up'|'left'|'right'|'down'
|
||||
},
|
||||
};
|
||||
vPoint = {}; // vertex coordinates
|
||||
mergedStyles: NodeShapeStyles;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// suggest to merge default styles like this to avoid style value missing
|
||||
// this.defaultStyles = mergeStyles([this.baseDefaultStyles, this.defaultStyles]);
|
||||
}
|
||||
public draw(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): NodeShapeMap {
|
||||
const { data = {} } = model;
|
||||
let shapes: NodeShapeMap = { keyShape: undefined };
|
||||
|
||||
// keyShape
|
||||
shapes.keyShape = this.drawKeyShape(model, shapeMap, diffData);
|
||||
|
||||
// haloShape
|
||||
if (data.haloShape && this.drawHaloShape) {
|
||||
shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// labelShape
|
||||
if (data.labelShape) {
|
||||
shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// labelBackgroundShape
|
||||
if (data.labelBackgroundShape) {
|
||||
shapes.labelBackgroundShape = this.drawLabelBackgroundShape(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
);
|
||||
}
|
||||
|
||||
// anchor shapes
|
||||
if (data.anchorShapes) {
|
||||
const anchorShapes = this.drawAnchorShapes(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
diffState,
|
||||
);
|
||||
shapes = {
|
||||
...shapes,
|
||||
...anchorShapes,
|
||||
};
|
||||
}
|
||||
|
||||
// iconShape
|
||||
if (data.iconShape) {
|
||||
shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// badgeShape
|
||||
if (data.badgeShapes) {
|
||||
const badgeShapes = this.drawBadgeShapes(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
diffState,
|
||||
);
|
||||
shapes = {
|
||||
...shapes,
|
||||
...badgeShapes,
|
||||
};
|
||||
}
|
||||
|
||||
// otherShapes
|
||||
if (data.otherShapes && this.drawOtherShapes) {
|
||||
shapes = {
|
||||
...shapes,
|
||||
...this.drawOtherShapes(model, shapeMap, diffData),
|
||||
};
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
public drawKeyShape(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
const { keyShape: keyShapeStyle } = this.mergedStyles as any;
|
||||
return this.upsertShape(
|
||||
'path',
|
||||
'keyShape',
|
||||
{
|
||||
...this.mergedStyles.keyShape,
|
||||
path: this.getTrianglePath(keyShapeStyle.r, keyShapeStyle.direction),
|
||||
},
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
private getTrianglePath(
|
||||
r: number,
|
||||
direction: 'up' | 'down' | 'left' | 'right',
|
||||
): PathArray {
|
||||
const height = 3 * r;
|
||||
const length = (3 * r) / Math.sin((1 / 3) * Math.PI);
|
||||
|
||||
let path: PathArray;
|
||||
if (direction === 'down') {
|
||||
path = [
|
||||
['M', 0, height / 2],
|
||||
['L', length / 2, -height / 2],
|
||||
['L', -length / 2, -height / 2],
|
||||
['Z'],
|
||||
];
|
||||
this.vPoint['bottom'] = [0, height / 2];
|
||||
this.vPoint['right'] = this.vPoint['default'] = [length / 2, -height / 2];
|
||||
this.vPoint['left'] = [-length / 2, -height / 2];
|
||||
} else if (direction === 'left') {
|
||||
path = [
|
||||
['M', -height / 2, 0],
|
||||
['L', height / 2, length / 2],
|
||||
['L', height / 2, -length / 2],
|
||||
['Z'],
|
||||
];
|
||||
this.vPoint['top'] = [height / 2, -length / 2];
|
||||
this.vPoint['bottom'] = [height / 2, length / 2];
|
||||
this.vPoint['left'] = this.vPoint['default'] = [-height / 2, 0];
|
||||
} else if (direction === 'right') {
|
||||
path = [
|
||||
['M', height / 2, 0],
|
||||
['L', -height / 2, length / 2],
|
||||
['L', -height / 2, -length / 2],
|
||||
['Z'],
|
||||
];
|
||||
this.vPoint['top'] = [-height / 2, -length / 2];
|
||||
this.vPoint['bottom'] = [-height / 2, length / 2];
|
||||
this.vPoint['right'] = this.vPoint['default'] = [height / 2, 0];
|
||||
} else {
|
||||
//top
|
||||
path = [
|
||||
['M', 0, -height / 2],
|
||||
['L', length / 2, height / 2],
|
||||
['L', -length / 2, height / 2],
|
||||
['Z'],
|
||||
];
|
||||
this.vPoint['left'] = [-length / 2, height / 2];
|
||||
this.vPoint['top'] = this.vPoint['default'] = [0, -height / 2];
|
||||
this.vPoint['right'] = [length / 2, height / 2];
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: add 'defaultOffsetX' and 'defaultOffsetY' making the icon align to the triangle center
|
||||
*/
|
||||
public override drawIconShape(
|
||||
model: NodeDisplayModel | ComboDisplayModel,
|
||||
shapeMap: NodeShapeMap | ComboShapeMap,
|
||||
diffData?: {
|
||||
previous: NodeModelData | ComboModelData;
|
||||
current: NodeModelData | ComboModelData;
|
||||
},
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
const { keyShape: keyShapeStyle } = this.mergedStyles as any;
|
||||
const { iconShape: shapeStyle } = this.mergedStyles;
|
||||
let defaultOffsetX = 0;
|
||||
let defaultOffsetY = keyShapeStyle.r / 4;
|
||||
|
||||
if (keyShapeStyle.direction === 'right') {
|
||||
defaultOffsetX = -keyShapeStyle.r / 4;
|
||||
defaultOffsetY = 0;
|
||||
} else if (keyShapeStyle.direction === 'left') {
|
||||
defaultOffsetX = keyShapeStyle.r / 4;
|
||||
defaultOffsetY = 0;
|
||||
} else if (keyShapeStyle.direction === 'down') {
|
||||
defaultOffsetX = 0;
|
||||
defaultOffsetY = -keyShapeStyle.r / 4;
|
||||
}
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
fontSize,
|
||||
text,
|
||||
offsetX = 0,
|
||||
offsetY = 0,
|
||||
} = shapeStyle;
|
||||
const w = (width || fontSize) as number;
|
||||
const h = (height || fontSize) as number;
|
||||
const iconShapeType = text ? 'text' : 'image';
|
||||
if (iconShapeType === 'image') {
|
||||
shapeStyle.x = -w / 2 + offsetX + defaultOffsetX;
|
||||
shapeStyle.y = -h / 2 + offsetY + defaultOffsetY;
|
||||
shapeStyle.width = w;
|
||||
shapeStyle.height = h;
|
||||
} else {
|
||||
shapeStyle.textAlign = 'center';
|
||||
shapeStyle.textBaseline = 'middle';
|
||||
shapeStyle.x = offsetX + defaultOffsetX;
|
||||
shapeStyle.y = offsetY + defaultOffsetY;
|
||||
shapeStyle.fontSize = w;
|
||||
}
|
||||
|
||||
return this.upsertShape(
|
||||
iconShapeType,
|
||||
'iconShape',
|
||||
shapeStyle as GShapeStyle,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
public override drawAnchorShapes(
|
||||
model: NodeDisplayModel | ComboDisplayModel,
|
||||
shapeMap: NodeShapeMap | ComboShapeMap,
|
||||
diffData?: {
|
||||
previous: NodeModelData | ComboModelData;
|
||||
current: NodeModelData | ComboModelData;
|
||||
},
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
) {
|
||||
const { anchorShapes: commonStyle, keyShape: keyShapeStyle } =
|
||||
this.mergedStyles;
|
||||
|
||||
const individualConfigs = Object.values(this.mergedStyles).filter(
|
||||
(style) => style.tag === 'anchorShape',
|
||||
);
|
||||
if (!individualConfigs.length) return;
|
||||
this.boundsCache.keyShapeLocal =
|
||||
this.boundsCache.keyShapeLocal || shapeMap.keyShape.getLocalBounds();
|
||||
const keyShapeBBox = this.boundsCache.keyShapeLocal;
|
||||
const keyShapeWidth = keyShapeBBox.max[0] - keyShapeBBox.min[0];
|
||||
const keyShapeHeight = keyShapeBBox.max[1] - keyShapeBBox.min[1];
|
||||
|
||||
const shapes = {};
|
||||
individualConfigs.forEach((config, i) => {
|
||||
const { position, fill = keyShapeStyle.fill, ...style } = config;
|
||||
const id = `anchorShape${i}`;
|
||||
if (!position) {
|
||||
console.error(`please set the anchorShape 'position'`);
|
||||
return;
|
||||
}
|
||||
const [cx, cy] = this.getAnchorPosition(position);
|
||||
shapes[id] = this.upsertShape(
|
||||
'circle',
|
||||
id,
|
||||
{
|
||||
cx,
|
||||
cy,
|
||||
fill,
|
||||
...commonStyle,
|
||||
...style,
|
||||
} as GShapeStyle,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
});
|
||||
return shapes;
|
||||
}
|
||||
|
||||
private getAnchorPosition(
|
||||
position: string | [number, number],
|
||||
): [number, number] {
|
||||
const keyShapeBBox = this.boundsCache.keyShapeLocal;
|
||||
const keyShapeWidth = keyShapeBBox.max[0] - keyShapeBBox.min[0];
|
||||
const keyShapeHeight = keyShapeBBox.max[1] - keyShapeBBox.min[1];
|
||||
if (position instanceof Array) {
|
||||
return [
|
||||
keyShapeWidth * (position[0] - 0.5),
|
||||
keyShapeHeight * (position[1] - 0.5),
|
||||
];
|
||||
} else if (typeof position === 'string') {
|
||||
position = position.toLowerCase();
|
||||
return this.vPoint[position] || this.vPoint['default'];
|
||||
}
|
||||
console.error(
|
||||
`there is a unknown position: ${position}, please check the anchorShape 'position' field`,
|
||||
);
|
||||
return [keyShapeWidth, keyShapeHeight];
|
||||
}
|
||||
}
|
@ -35,8 +35,6 @@ export default class Grid extends Base {
|
||||
public init(graph: IGraph) {
|
||||
super.init(graph);
|
||||
const minZoom = graph.getZoom();
|
||||
// console.log('minZoom', minZoom);
|
||||
// console.log('graph', graph.canvas);
|
||||
const graphContainer = graph.container;
|
||||
const canvas = this.canvas || graphContainer.firstChild.nextSibling;
|
||||
const [width, height] = graph.getSize();
|
||||
|
@ -4,7 +4,7 @@ import { IGraph } from '../../../types';
|
||||
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
|
||||
|
||||
interface ToolbarConfig extends IPluginBaseConfig {
|
||||
handleClick: (code: string, graph: IGraph) => void;
|
||||
handleClick?: (code: string, graph: IGraph) => void;
|
||||
getContent: (graph?: IGraph) => HTMLDivElement | string;
|
||||
zoomSensitivity: number;
|
||||
minZoom: number;
|
||||
@ -30,17 +30,19 @@ const getEventPath = (evt: MouseEvent) => {
|
||||
return path;
|
||||
}
|
||||
|
||||
el = el.parentElement;
|
||||
el = el.parentElement as HTMLElement;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
export default class Toolbar extends Base {
|
||||
public options: ToolbarConfig;
|
||||
// public options: ToolbarConfig;
|
||||
public ToolbarDOM: HTMLDivElement;
|
||||
public ContainerDOM: HTMLElement;
|
||||
constructor(config: Partial<ToolbarConfig>) {
|
||||
super(config);
|
||||
this.ToolbarDOM = createDom('<div></div>');
|
||||
this.ContainerDOM = createDom('<div></div>');
|
||||
}
|
||||
public getDefaultCfgs(): ToolbarConfig {
|
||||
return {
|
||||
@ -79,7 +81,7 @@ export default class Toolbar extends Base {
|
||||
public getContainer() {
|
||||
const { container } = this.options;
|
||||
if (typeof container === 'string') {
|
||||
this.ContainerDOM = document.getElementById(container);
|
||||
this.ContainerDOM = document.getElementById(container) as HTMLDivElement;
|
||||
} else {
|
||||
this.ContainerDOM = this.graph.container;
|
||||
}
|
||||
@ -103,7 +105,9 @@ export default class Toolbar extends Base {
|
||||
this.ContainerDOM.appendChild(this.ToolbarDOM);
|
||||
|
||||
this.ToolbarDOM.addEventListener('click', (evt) => {
|
||||
const current = getEventPath(evt).filter((p) => p.nodeName === 'LI');
|
||||
const current = (getEventPath(evt) as HTMLElement[]).filter(
|
||||
(p) => p.nodeName === 'LI',
|
||||
);
|
||||
if (current.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -225,7 +229,6 @@ export default class Toolbar extends Base {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
const { ToolbarDOM, ContainerDOM } = this;
|
||||
const { handleClick } = this.options;
|
||||
if (ToolbarDOM) {
|
||||
@ -235,5 +238,6 @@ export default class Toolbar extends Base {
|
||||
//@ts-ignore
|
||||
ToolbarDOM.removeEventListener('click', handleClick);
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Graph as GraphLib } from '@antv/graphlib';
|
||||
import { Graph as GraphLib, TreeData } from '@antv/graphlib';
|
||||
import { ComboUserModel } from './combo';
|
||||
import { NodeDisplayModelData, NodeModelData, NodeUserModel } from './node';
|
||||
import {
|
||||
NodeDisplayModelData,
|
||||
NodeModelData,
|
||||
NodeUserModel,
|
||||
NodeUserModelData,
|
||||
} from './node';
|
||||
import { EdgeDisplayModelData, EdgeModelData, EdgeUserModel } from './edge';
|
||||
|
||||
export interface GraphData {
|
||||
@ -9,16 +14,26 @@ export interface GraphData {
|
||||
combos?: ComboUserModel[];
|
||||
}
|
||||
|
||||
export interface InlineDataConfig {
|
||||
type: 'inline';
|
||||
export interface InlineGraphDataConfig {
|
||||
type: 'graphData';
|
||||
value: GraphData;
|
||||
}
|
||||
export interface InlineTreeDataConfig {
|
||||
type: 'treeData';
|
||||
value: TreeData<NodeUserModelData> | TreeData<NodeUserModelData>[];
|
||||
}
|
||||
|
||||
export interface FetchDataConfig {
|
||||
type: 'fetch';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type DataConfig =
|
||||
| GraphData
|
||||
| InlineGraphDataConfig
|
||||
| InlineTreeDataConfig
|
||||
| FetchDataConfig;
|
||||
|
||||
export type GraphCore = GraphLib<NodeModelData, EdgeModelData>;
|
||||
export type DisplayGraphCore = GraphLib<
|
||||
NodeDisplayModelData,
|
||||
|
@ -232,6 +232,11 @@ export interface IGraph<
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
upsertAncestors?: boolean,
|
||||
disableAnimate?: boolean,
|
||||
callback?: (
|
||||
model: NodeModel | EdgeModel | ComboModel,
|
||||
canceled?: boolean,
|
||||
) => void,
|
||||
stack?: boolean,
|
||||
) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
|
||||
|
||||
@ -249,6 +254,8 @@ export interface IGraph<
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
upsertAncestors?: boolean,
|
||||
disableAnimate?: boolean,
|
||||
callback?: (model: NodeModel | EdgeModel | ComboModel) => void,
|
||||
stack?: boolean,
|
||||
) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
|
||||
|
||||
@ -444,14 +451,14 @@ export interface IGraph<
|
||||
* @returns
|
||||
* @group Data
|
||||
*/
|
||||
showItem: (ids: ID | ID[], disableAniamte?: boolean) => void;
|
||||
showItem: (ids: ID | ID[], disableAnimate?: boolean) => void;
|
||||
/**
|
||||
* Hide the item(s).
|
||||
* @param ids the item id(s) to be hidden
|
||||
* @returns
|
||||
* @group Item
|
||||
*/
|
||||
hideItem: (ids: ID | ID[], disableAniamte?: boolean) => void;
|
||||
hideItem: (ids: ID | ID[], disableAnimate?: boolean) => void;
|
||||
/**
|
||||
* Make the item(s) to the 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: (options?: LayoutOptions) => Promise<void>;
|
||||
layout: (options?: LayoutOptions, disableAnimate?: boolean) => Promise<void>;
|
||||
stopLayout: () => void;
|
||||
|
||||
// ===== interaction =====
|
||||
@ -627,4 +634,24 @@ export interface IGraph<
|
||||
type: string;
|
||||
[cfgName: string]: unknown;
|
||||
}) => 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;
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ import { Canvas } from '@antv/g';
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { CameraAnimationOptions } from './animate';
|
||||
import { BehaviorOptionsOf } from './behavior';
|
||||
import { DataChangeType, GraphCore, GraphData } from './data';
|
||||
import { EdgeModelData } from './edge';
|
||||
import { DataChangeType, DataConfig, GraphCore } from './data';
|
||||
import { EdgeModel, EdgeModelData } from './edge';
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from './item';
|
||||
import { LayoutOptions } from './layout';
|
||||
import { NodeModelData } from './node';
|
||||
import { NodeModel, NodeModelData } from './node';
|
||||
import { ThemeSpecification } from './theme';
|
||||
import { GraphTransformOptions } from './view';
|
||||
import { ComboModel } from './combo';
|
||||
|
||||
export interface IHook<T> {
|
||||
name: string;
|
||||
@ -35,7 +36,7 @@ export interface Hooks {
|
||||
// data
|
||||
datachange: IHook<{
|
||||
type: DataChangeType;
|
||||
data: GraphData;
|
||||
data: DataConfig;
|
||||
}>;
|
||||
itemchange: IHook<{
|
||||
type: ITEM_TYPE;
|
||||
@ -43,14 +44,20 @@ export interface Hooks {
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
upsertAncestors?: boolean;
|
||||
animate?: boolean;
|
||||
action?: 'updatePosition';
|
||||
callback?: (model: NodeModel | EdgeModel | ComboModel) => void;
|
||||
}>;
|
||||
render: IHook<{
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
transientCanvas: Canvas;
|
||||
}>; // 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
|
||||
modechange: IHook<{ mode: string }>;
|
||||
behaviorchange: IHook<{
|
||||
@ -101,6 +108,11 @@ export interface Hooks {
|
||||
transient: Canvas;
|
||||
};
|
||||
}>;
|
||||
treecollapseexpand: IHook<{
|
||||
ids: ID[];
|
||||
action: 'collapse' | 'expand';
|
||||
graphCore: GraphCore;
|
||||
animate?: boolean;
|
||||
}>;
|
||||
destroy: IHook<{}>;
|
||||
// TODO: more timecycles here
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
NodeShapeMap,
|
||||
NodeUserModel,
|
||||
} from './node';
|
||||
import { ComboStyleSet, EdgeStyleSet, NodeStyleSet } from './theme';
|
||||
|
||||
export type GShapeStyle = CircleStyleProps &
|
||||
RectStyleProps &
|
||||
@ -248,6 +249,8 @@ export interface IItem {
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
animate?: boolean,
|
||||
onfinish?: Function,
|
||||
) => void;
|
||||
/**
|
||||
* Updates the shapes.
|
||||
@ -256,7 +259,14 @@ export interface IItem {
|
||||
update: (
|
||||
model: ItemModel,
|
||||
diffData: { previous: ItemModelData; current: ItemModelData },
|
||||
isUpdate?: boolean,
|
||||
isReplace?: boolean,
|
||||
itemTheme?: {
|
||||
styles: NodeStyleSet | EdgeStyleSet | ComboStyleSet;
|
||||
lodStrategy: LodStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
animate?: boolean,
|
||||
onfinish?: Function,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@ -269,6 +279,7 @@ export interface IItem {
|
||||
updatePosition: (
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
animate?: boolean,
|
||||
onfinish?: Function,
|
||||
) => void;
|
||||
|
||||
|
@ -51,22 +51,21 @@ type CustomLayout = {
|
||||
[option: string]: any;
|
||||
};
|
||||
|
||||
export type StandardLayoutOptions =
|
||||
| (
|
||||
| CircularLayout
|
||||
| RandomLayout
|
||||
| ConcentricLayout
|
||||
| GridLayout
|
||||
| MDSLayout
|
||||
| RadialLayout
|
||||
| FruchtermanLayout
|
||||
| D3ForceLayout
|
||||
| ForceLayout
|
||||
| ForceAtlas2
|
||||
| CustomLayout
|
||||
) &
|
||||
Animatable &
|
||||
Workerized;
|
||||
export type StandardLayoutOptions = (
|
||||
| CircularLayout
|
||||
| RandomLayout
|
||||
| ConcentricLayout
|
||||
| GridLayout
|
||||
| MDSLayout
|
||||
| RadialLayout
|
||||
| FruchtermanLayout
|
||||
| D3ForceLayout
|
||||
| ForceLayout
|
||||
| ForceAtlas2
|
||||
| CustomLayout
|
||||
) &
|
||||
Animatable &
|
||||
Workerized;
|
||||
|
||||
export type LayoutOptions =
|
||||
| StandardLayoutOptions
|
||||
|
@ -45,6 +45,10 @@ export interface NodeUserModelData extends PlainObject {
|
||||
* Reserved for combo.
|
||||
*/
|
||||
parentId?: ID;
|
||||
/**
|
||||
* Whether to be a root at when used as a tree.
|
||||
*/
|
||||
isRoot?: boolean;
|
||||
/**
|
||||
* The icon to show on the node.
|
||||
* More styles should be configured in node mapper.
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { Canvas } from '@antv/g';
|
||||
import { AnimateCfg } from './animate';
|
||||
import { Point } from './common';
|
||||
import {
|
||||
FetchDataConfig,
|
||||
GraphData,
|
||||
InlineDataConfig,
|
||||
TransformerFn,
|
||||
} from './data';
|
||||
import { DataConfig, TransformerFn } from './data';
|
||||
import {
|
||||
EdgeDisplayModel,
|
||||
EdgeEncode,
|
||||
@ -36,7 +31,6 @@ export interface Specification<
|
||||
B extends BehaviorRegistry,
|
||||
T extends ThemeRegistry,
|
||||
> {
|
||||
type: 'graph' | 'tree';
|
||||
container?: string | HTMLElement;
|
||||
backgroundCanvas?: Canvas;
|
||||
canvas?: Canvas;
|
||||
@ -61,7 +55,7 @@ export interface Specification<
|
||||
optimizeThreshold?: number;
|
||||
|
||||
/** data */
|
||||
data: GraphData | InlineDataConfig | FetchDataConfig; // TODO: more
|
||||
data?: DataConfig;
|
||||
transform?:
|
||||
| string[]
|
||||
| {
|
||||
|
@ -47,16 +47,15 @@ export type ThemeObjectOptionsOf<T extends ThemeRegistry = {}> = {
|
||||
|
||||
/** Default and stateStyle for an item */
|
||||
export type NodeStyleSet = {
|
||||
default?: NodeShapeStyles;
|
||||
seledted?: NodeShapeStyles;
|
||||
default: NodeShapeStyles;
|
||||
[stateName: string]: NodeShapeStyles;
|
||||
};
|
||||
export type EdgeStyleSet = {
|
||||
default?: EdgeShapeStyles;
|
||||
default: EdgeShapeStyles;
|
||||
[stateName: string]: EdgeShapeStyles;
|
||||
};
|
||||
export type ComboStyleSet = {
|
||||
default?: ComboShapeStyles;
|
||||
default: ComboShapeStyles;
|
||||
[stateName: string]: ComboShapeStyles;
|
||||
};
|
||||
|
||||
|
@ -248,7 +248,7 @@ const runAnimateGroupOnShapes = (
|
||||
maxDurationIdx = i;
|
||||
}
|
||||
if (animation) {
|
||||
animation.oncancel = () => {
|
||||
animation.onManualCancel = () => {
|
||||
hasCanceled = true;
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Should be called after canvas ready and shape appended.
|
||||
@ -318,7 +345,7 @@ export const animateShapes = (
|
||||
onAnimatesEnd: Function = () => {},
|
||||
): IAnimation[] => {
|
||||
if (!animates?.[timing]) {
|
||||
onAnimatesEnd();
|
||||
onAnimatesEnd(false);
|
||||
return;
|
||||
}
|
||||
const segmentedTiming =
|
||||
@ -335,7 +362,7 @@ export const animateShapes = (
|
||||
let canceled = false;
|
||||
const onfinish = () => {
|
||||
if (i >= groupKeys.length) {
|
||||
onAnimatesEnd();
|
||||
!canceled && onAnimatesEnd(canceled);
|
||||
return;
|
||||
}
|
||||
const groupAnimations = runAnimateGroupOnShapes(
|
||||
@ -345,7 +372,10 @@ export const animateShapes = (
|
||||
mergedStyles,
|
||||
timing,
|
||||
onfinish, // execute next order group
|
||||
() => (canceled = true),
|
||||
() => {
|
||||
canceled = true;
|
||||
onAnimatesEnd(canceled);
|
||||
},
|
||||
canceled,
|
||||
).filter(Boolean);
|
||||
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.
|
||||
* @param animation
|
||||
*/
|
||||
export const stopAnimate = (animation) => {
|
||||
export const stopAnimate = (animation: IAnimation): Promise<any> => {
|
||||
const timing = animation.effect.getTiming();
|
||||
animation.currentTime = Number(timing.duration) + Number(timing.delay || 0);
|
||||
animation.cancel();
|
||||
animation.finish();
|
||||
return animation.finished;
|
||||
};
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { NodeUserModel } from 'types';
|
||||
import { TreeData } from '@antv/graphlib';
|
||||
import { NodeUserModelData } from 'types/node';
|
||||
import { isArray } from '@antv/util';
|
||||
import { depthFirstSearch, connectedComponent } from '@antv/algorithm';
|
||||
import { GraphCore, GraphData } from '../types/data';
|
||||
import { IGraph } from '../types/graph';
|
||||
import { GraphCore } from '../types/data';
|
||||
|
||||
/**
|
||||
* Deconstruct data and distinguish nodes and combos from graphcore data.
|
||||
@ -35,19 +39,30 @@ export const graphCoreTreeDfs = (
|
||||
nodes: NodeUserModel[],
|
||||
fn,
|
||||
mode: 'TB' | 'BT' = 'TB',
|
||||
treeKey = 'combo',
|
||||
stopFns: {
|
||||
stopBranchFn?: (node: NodeUserModel) => boolean;
|
||||
stopAllFn?: (node: NodeUserModel) => boolean;
|
||||
} = {},
|
||||
) => {
|
||||
if (!nodes?.length) return;
|
||||
nodes.forEach((node) => {
|
||||
if (!graphCore.hasNode(node.id)) return;
|
||||
const { stopBranchFn, stopAllFn } = stopFns;
|
||||
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
|
||||
graphCoreTreeDfs(
|
||||
graphCore,
|
||||
graphCore.getChildren(node.id, 'combo'),
|
||||
graphCore.getChildren(node.id, treeKey),
|
||||
fn,
|
||||
mode,
|
||||
treeKey,
|
||||
stopFns,
|
||||
);
|
||||
if (mode !== 'TB') fn(node); // Traverse from bottom to top
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -176,3 +191,105 @@ export const validateComboStrucutre = (
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { IElement } from '@antv/g';
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { IG6GraphEvent, IGraph } from 'types';
|
||||
import {
|
||||
EdgeAdded,
|
||||
EdgeDataUpdated,
|
||||
EdgeRemoved,
|
||||
EdgeUpdated,
|
||||
ID,
|
||||
NodeAdded,
|
||||
NodeDataUpdated,
|
||||
NodeRemoved,
|
||||
TreeStructureChanged,
|
||||
} from '@antv/graphlib';
|
||||
import { IG6GraphEvent, IGraph, NodeModelData } from '../types';
|
||||
import { GraphCore } from '../types/data';
|
||||
import { EdgeModelData } from '../types/edge';
|
||||
|
||||
export type ItemInfo = {
|
||||
itemType: 'canvas' | 'node' | 'edge' | 'combo';
|
||||
@ -61,3 +73,75 @@ export const getContextMenuEventProps = (
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type GroupedChanges = {
|
||||
NodeRemoved: NodeRemoved<NodeModelData>[];
|
||||
EdgeRemoved: EdgeRemoved<EdgeModelData>[];
|
||||
NodeAdded: NodeAdded<NodeModelData>[];
|
||||
EdgeAdded: EdgeAdded<EdgeModelData>[];
|
||||
NodeDataUpdated: NodeDataUpdated<NodeModelData>[];
|
||||
EdgeUpdated: EdgeUpdated<EdgeModelData>[];
|
||||
EdgeDataUpdated: EdgeDataUpdated<EdgeModelData>[];
|
||||
TreeStructureChanged: TreeStructureChanged[];
|
||||
ComboStructureChanged: TreeStructureChanged[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Group the changes with change types from graphCore's changes.
|
||||
* @param graphCore
|
||||
* @param changes
|
||||
* @returns
|
||||
*/
|
||||
export const getGroupedChanges = (
|
||||
graphCore: GraphCore,
|
||||
changes,
|
||||
): GroupedChanges => {
|
||||
const groupedChanges: GroupedChanges = {
|
||||
NodeRemoved: [],
|
||||
EdgeRemoved: [],
|
||||
NodeAdded: [],
|
||||
EdgeAdded: [],
|
||||
NodeDataUpdated: [],
|
||||
EdgeUpdated: [],
|
||||
EdgeDataUpdated: [],
|
||||
TreeStructureChanged: [],
|
||||
ComboStructureChanged: [],
|
||||
};
|
||||
changes.forEach((change) => {
|
||||
const { type: changeType } = change;
|
||||
if (
|
||||
['NodeDataUpdated', 'EdgeUpdated', 'EdgeDataUpdated'].includes(changeType)
|
||||
) {
|
||||
const { id: oid } = change;
|
||||
if (!graphCore.hasNode(oid) && !graphCore.hasEdge(oid)) {
|
||||
const nid = Number(oid);
|
||||
if ((!isNaN(nid) && graphCore.hasNode(nid)) || graphCore.hasEdge(nid)) {
|
||||
groupedChanges[changeType].push({ ...change, id: nid });
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (changeType === 'TreeStructureChanged') {
|
||||
if (change.treeKey === 'combo')
|
||||
groupedChanges.ComboStructureChanged.push(change);
|
||||
else if (change.treeKey === 'tree')
|
||||
groupedChanges.TreeStructureChanged.push(change);
|
||||
return;
|
||||
} else if (['NodeRemoved', 'EdgeRemoved'].includes(changeType)) {
|
||||
groupedChanges[changeType].push(change);
|
||||
} else {
|
||||
const { id: oid } = change.value;
|
||||
if (!graphCore.hasNode(oid) && !graphCore.hasEdge(oid)) {
|
||||
const nid = Number(oid);
|
||||
if ((!isNaN(nid) && graphCore.hasNode(nid)) || graphCore.hasEdge(nid)) {
|
||||
groupedChanges[changeType].push({
|
||||
...change,
|
||||
value: { ...change.value, id: nid },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
groupedChanges[changeType].push(change);
|
||||
});
|
||||
return groupedChanges;
|
||||
};
|
||||
|
@ -31,8 +31,8 @@ export const upsertTransientItem = (
|
||||
nodeGroup: Group,
|
||||
edgeGroup: Group,
|
||||
comboGroup: Group,
|
||||
transientItemMap: Record<string, Node | Edge | Combo | Group>,
|
||||
itemMap: Record<string, Node | Edge | Combo>,
|
||||
transientItemMap: Map<ID, Node | Edge | Combo | Group>,
|
||||
itemMap: Map<ID, Node | Edge | Combo>,
|
||||
graphCore?: GraphCore,
|
||||
onlyDrawKeyShape?: boolean,
|
||||
upsertAncestors = true,
|
||||
@ -78,7 +78,7 @@ export const upsertTransientItem = (
|
||||
const childItems = [];
|
||||
const children = graphCore.getChildren(item.model.id, 'combo');
|
||||
children.forEach((childModel) => {
|
||||
const childItem = itemMap[childModel.id];
|
||||
const childItem = itemMap.get(childModel.id);
|
||||
if (childItem) {
|
||||
childItems.push(
|
||||
upsertTransientItem(
|
||||
@ -111,7 +111,7 @@ export const upsertTransientItem = (
|
||||
// find the ancestors to upsert transients
|
||||
let currentAncestor = graphCore.getParent(item.model.id, 'combo');
|
||||
while (currentAncestor) {
|
||||
const ancestorItem = itemMap[currentAncestor.id];
|
||||
const ancestorItem = itemMap.get(currentAncestor.id);
|
||||
if (ancestorItem) {
|
||||
const transientAncestor = upsertTransientItem(
|
||||
ancestorItem,
|
||||
|
65
packages/g6/src/util/layout.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import Hierarchy from '@antv/hierarchy';
|
||||
|
||||
import { traverse } from './data';
|
||||
type TreeGraphData = any;
|
||||
/**
|
||||
* 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;
|
||||
};
|
@ -5,10 +5,14 @@ import { LodStrategy, LodStrategyObj } from '../types/item';
|
||||
* @param lodStrategy
|
||||
* @returns
|
||||
*/
|
||||
export const formatLodStrategy = (lodStrategy: LodStrategy): LodStrategyObj => {
|
||||
const { levels, animateCfg } = lodStrategy || {};
|
||||
if (!levels) return undefined;
|
||||
export const formatLodStrategy = (
|
||||
lodStrategy?: LodStrategy,
|
||||
): LodStrategyObj | undefined => {
|
||||
if (!lodStrategy) return;
|
||||
const { levels, animateCfg } = lodStrategy;
|
||||
if (!levels) return;
|
||||
const primaryLevel = levels.find((level) => level.primary);
|
||||
if (!primaryLevel) return;
|
||||
const primaryIndex = levels.indexOf(primaryLevel);
|
||||
const formattedLevels = {};
|
||||
levels.forEach((level, i) => {
|
||||
|
69
packages/g6/tests/datasets/dataCfg.ts
Normal file
@ -0,0 +1,69 @@
|
||||
export const graphDataCfg = {
|
||||
type: 'graphData',
|
||||
value: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: { isRoot: true } }, // , collapsed: true
|
||||
{ id: 'cnode2', data: {} },
|
||||
{ id: 'cnode1', data: {} },
|
||||
{ id: 'dynamicNode', data: {} },
|
||||
{ id: 'node5', data: {} },
|
||||
{ id: 'node2', data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'node1', target: 'cnode2', data: {} },
|
||||
{ id: 'edge2', source: 'node1', target: 'cnode1', data: {} },
|
||||
{ id: 'edge4', source: 'cnode2', target: 'cnode1', data: {} },
|
||||
{ id: 'edge5', source: 'cnode1', target: 'dynamicNode', data: {} },
|
||||
{ id: 'edge6', source: 'dynamicNode', target: 'node5', data: {} },
|
||||
{ id: 'edge7', source: 'cnode2', target: 'node2', data: {} },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const treeDataCfg = {
|
||||
type: 'treeData',
|
||||
value: [
|
||||
{
|
||||
id: 'node1',
|
||||
data: {
|
||||
// collapsed: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'cnode1',
|
||||
data: {},
|
||||
children: [
|
||||
{
|
||||
id: 'dynamicNode',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cnode2',
|
||||
data: {},
|
||||
children: [
|
||||
{
|
||||
id: 'node2',
|
||||
data: {},
|
||||
children: [
|
||||
{
|
||||
id: 't2c1-c1',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 't2c2',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
@ -4,7 +4,6 @@ import { TestCaseContext } from '../interface';
|
||||
export default (context: TestCaseContext) => {
|
||||
return new G6.Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
|
@ -1,11 +1,8 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { container, height, width } from '../../datasets/const';
|
||||
export default () => {
|
||||
return new G6.Graph({
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
import { Graph } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
export default (context: TestCaseContext) => {
|
||||
return new Graph({
|
||||
...context,
|
||||
plugins: ['grid'],
|
||||
layout: {
|
||||
type: 'grid',
|
||||
|
@ -1,11 +1,8 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { container, height, width } from '../../datasets/const';
|
||||
export default () => {
|
||||
return new G6.Graph({
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
import { Graph } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
export default (context: TestCaseContext) => {
|
||||
return new Graph({
|
||||
...context,
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
|
30
packages/g6/tests/demo/behaviors/collapse-expand-tree.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { treeDataCfg } from '../../datasets/dataCfg';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (
|
||||
context: TestCaseContext,
|
||||
options: { trigger?: string } = {},
|
||||
) => {
|
||||
const { trigger = 'click' } = options;
|
||||
const graph = new G6.Graph({
|
||||
...context,
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: treeDataCfg,
|
||||
modes: {
|
||||
default: [{ type: 'collapse-expand-tree', trigger }],
|
||||
},
|
||||
});
|
||||
graph.translateTo({ x: 150, y: 200 });
|
||||
return graph;
|
||||
};
|
@ -1,11 +1,8 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { container, height, width } from '../../datasets/const';
|
||||
export default () => {
|
||||
const graph = new G6.Graph({
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
import { Graph } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
export default (context: TestCaseContext) => {
|
||||
const graph = new Graph({
|
||||
...context,
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
|
@ -58,7 +58,6 @@ export default () => {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
type: 'graph',
|
||||
modes: {
|
||||
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
|
||||
},
|
||||
|
@ -124,7 +124,6 @@ const create2DGraph = (
|
||||
container: container as HTMLElement,
|
||||
width,
|
||||
height: 1400,
|
||||
type: 'graph',
|
||||
renderer: rendererType,
|
||||
data: dataFor2D,
|
||||
modes: {
|
||||
@ -223,7 +222,6 @@ const create3DGraph = async () => {
|
||||
container: container as HTMLDivElement,
|
||||
width,
|
||||
height: 1400,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
data: dataFor3D,
|
||||
// layout: {
|
||||
|
128
packages/g6/tests/demo/demo/ellipse.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { TestCaseContext } from '../interface';
|
||||
import { Graph, IGraph } from '../../../src/index';
|
||||
|
||||
let graph: any;
|
||||
|
||||
const createCtrlContainer = (container: HTMLElement) => {
|
||||
const ctrlContainer = document.createElement('div');
|
||||
ctrlContainer.id = 'ctrl-container';
|
||||
ctrlContainer.style.width = '100%';
|
||||
ctrlContainer.style.height = '200px';
|
||||
ctrlContainer.style.backgroundColor = '#eee';
|
||||
container.appendChild(ctrlContainer);
|
||||
};
|
||||
|
||||
const createCtrl = () => {
|
||||
const conEle = document.querySelector('div#ctrl-container')!;
|
||||
const selectedStyleLabel = document.createElement('span');
|
||||
selectedStyleLabel.textContent = 'custom selected style';
|
||||
// selectedStyleLabel.style.position = 'absolute';
|
||||
selectedStyleLabel.style.top = '124px';
|
||||
selectedStyleLabel.style.left = '16px';
|
||||
selectedStyleLabel.style.zIndex = '100';
|
||||
|
||||
const selectedStyleCb = document.createElement('input');
|
||||
selectedStyleCb.setAttribute('id', 'selected');
|
||||
selectedStyleCb.type = 'checkbox';
|
||||
selectedStyleCb.value = 'selected';
|
||||
// selectedStyleCb.style.position = 'absolute';
|
||||
selectedStyleCb.style.width = '20px';
|
||||
selectedStyleCb.style.height = '20px';
|
||||
selectedStyleCb.style.top = '124px';
|
||||
selectedStyleCb.style.left = '166px';
|
||||
selectedStyleCb.style.zIndex = '100';
|
||||
|
||||
selectedStyleCb.addEventListener('click', (e) => {
|
||||
if (selectedStyleCb.checked) {
|
||||
graph.setItemState('node1', 'selected', true);
|
||||
} else {
|
||||
graph.setItemState('node1', 'selected', false);
|
||||
}
|
||||
});
|
||||
conEle.appendChild(selectedStyleLabel);
|
||||
conEle.appendChild(selectedStyleCb);
|
||||
};
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
data: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
type: 'ellipse-node',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
data: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
type: 'rect-node',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 3,
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const { width, height, container } = context;
|
||||
createCtrlContainer(container!);
|
||||
createCtrl();
|
||||
|
||||
graph = new Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
data,
|
||||
modes: {
|
||||
default: ['click-select', 'drag-node'],
|
||||
},
|
||||
node: (nodeInnerModel: any) => {
|
||||
const { id, data } = nodeInnerModel;
|
||||
return {
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
keyShape: {
|
||||
height: 50,
|
||||
width: 50,
|
||||
},
|
||||
labelShape: {
|
||||
text: 'label',
|
||||
position: 'bottom',
|
||||
},
|
||||
iconShape: {
|
||||
// img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
text: 'label',
|
||||
},
|
||||
badgeShapes: [
|
||||
{
|
||||
text: '1',
|
||||
position: 'rightTop',
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
labelBackgroundShape: {
|
||||
fill: 'red',
|
||||
},
|
||||
anchorShapes: [
|
||||
{
|
||||
position: [0, 0.5],
|
||||
r: 2,
|
||||
fill: 'red',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return graph;
|
||||
};
|
@ -44,7 +44,6 @@ export default () => {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
type: 'graph',
|
||||
modes: {
|
||||
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
|
||||
},
|
||||
|
@ -66,7 +66,6 @@ export default () => {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
type: 'graph',
|
||||
modes: {
|
||||
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ export default () => {
|
||||
data: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
type: 'rect-node',
|
||||
type: 'circle-node',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -37,6 +37,13 @@ export default () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 1,
|
||||
target: 2,
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
@ -44,7 +51,6 @@ export default () => {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
type: 'graph',
|
||||
modes: {
|
||||
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
|
||||
},
|
||||
|
@ -44,7 +44,6 @@ export default () => {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
type: 'graph',
|
||||
modes: {
|
||||
default: ['click-select', 'drag-canvas', 'zoom-canvas', 'drag-node'],
|
||||
},
|
||||
|
128
packages/g6/tests/demo/demo/triangle.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { TestCaseContext } from '../interface';
|
||||
import { Graph } from '../../../src/index';
|
||||
|
||||
let graph: any;
|
||||
|
||||
const createCtrlContainer = (container: HTMLElement) => {
|
||||
const ctrlContainer = document.createElement('div');
|
||||
ctrlContainer.id = 'ctrl-container';
|
||||
ctrlContainer.style.width = '100%';
|
||||
ctrlContainer.style.height = '200px';
|
||||
ctrlContainer.style.backgroundColor = '#eee';
|
||||
container.appendChild(ctrlContainer);
|
||||
};
|
||||
|
||||
const createCtrl = () => {
|
||||
const conEle = document.querySelector('div#ctrl-container')!;
|
||||
const selectedStyleLabel = document.createElement('span');
|
||||
selectedStyleLabel.textContent = 'custom selected style';
|
||||
// selectedStyleLabel.style.position = 'absolute';
|
||||
selectedStyleLabel.style.top = '124px';
|
||||
selectedStyleLabel.style.left = '16px';
|
||||
selectedStyleLabel.style.zIndex = '100';
|
||||
|
||||
const selectedStyleCb = document.createElement('input');
|
||||
selectedStyleCb.setAttribute('id', 'selected');
|
||||
selectedStyleCb.type = 'checkbox';
|
||||
selectedStyleCb.value = 'selected';
|
||||
// selectedStyleCb.style.position = 'absolute';
|
||||
selectedStyleCb.style.width = '20px';
|
||||
selectedStyleCb.style.height = '20px';
|
||||
selectedStyleCb.style.top = '124px';
|
||||
selectedStyleCb.style.left = '166px';
|
||||
selectedStyleCb.style.zIndex = '100';
|
||||
|
||||
selectedStyleCb.addEventListener('click', (e) => {
|
||||
if (selectedStyleCb.checked) {
|
||||
graph.setItemState('node1', 'selected', true);
|
||||
} else {
|
||||
graph.setItemState('node1', 'selected', false);
|
||||
}
|
||||
});
|
||||
conEle.appendChild(selectedStyleLabel);
|
||||
conEle.appendChild(selectedStyleCb);
|
||||
};
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
data: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
type: 'triangle-node',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
data: {
|
||||
x: 200,
|
||||
y: 100,
|
||||
type: 'rect-node',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 3,
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const { width, height, container } = context;
|
||||
createCtrlContainer(container!);
|
||||
createCtrl();
|
||||
|
||||
graph = new Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
data,
|
||||
modes: {
|
||||
default: ['click-select', 'drag-node', 'zoom-canvas', 'drag-canvas'],
|
||||
},
|
||||
node: (nodeInnerModel: any) => {
|
||||
const { id, data } = nodeInnerModel;
|
||||
return {
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
keyShape: {
|
||||
direction: 'top',
|
||||
},
|
||||
labelShape: {
|
||||
text: 'label',
|
||||
position: 'bottom',
|
||||
},
|
||||
iconShape: {
|
||||
// img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
text: 'label',
|
||||
fontSize: 8,
|
||||
},
|
||||
badgeShapes: [
|
||||
{
|
||||
text: '1',
|
||||
position: 'rightTop',
|
||||
color: 'blue',
|
||||
},
|
||||
],
|
||||
labelBackgroundShape: {
|
||||
fill: 'red',
|
||||
},
|
||||
anchorShapes: [
|
||||
{
|
||||
position: 'lkj',
|
||||
r: 2,
|
||||
fill: 'red',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return graph;
|
||||
};
|
@ -1,71 +1,81 @@
|
||||
import animations_node_build_in from './animations/node-build-in';
|
||||
import behaviors_activateRelations from './behaviors/activate-relations';
|
||||
import behaviors_brush_select from './behaviors/brush-select';
|
||||
import behaviors_click_select from './behaviors/click-select';
|
||||
import layouts_circular from './layouts/circular';
|
||||
import layouts_grid from './layouts/grid';
|
||||
import layouts_dagre from './layouts/dagre';
|
||||
import layouts_force from './layouts/force';
|
||||
import layouts_d3force from './layouts/d3force';
|
||||
import layouts_custom from './layouts/custom';
|
||||
import user_defined_canvas from './user-defined-canvas/circular';
|
||||
import layouts_fruchterman_wasm from './layouts/fruchterman-wasm';
|
||||
import layouts_forceatlas2_wasm from './layouts/forceatlas2-wasm';
|
||||
import layouts_force_wasm from './layouts/force-wasm';
|
||||
import layouts_fruchterman_gpu from './layouts/fruchterman-gpu';
|
||||
import layouts_force_3d from './layouts/force-3d';
|
||||
import layouts_force_wasm_3d from './layouts/force-wasm-3d';
|
||||
import performance from './performance/performance';
|
||||
import performance_layout from './performance/layout';
|
||||
import performance_layout_3d from './performance/layout-3d';
|
||||
import behaviors_collapse_expand_tree from './behaviors/collapse-expand-tree';
|
||||
import comboBasic from './combo/combo-basic';
|
||||
import bugReproduce from './demo/bugReproduce';
|
||||
import demo from './demo/demo';
|
||||
import demoFor4 from './demo/demoFor4';
|
||||
import bugReproduce from './demo/bugReproduce';
|
||||
import rect from './demo/rect';
|
||||
import visual from './visual/visual';
|
||||
import quadratic from './demo/quadratic';
|
||||
import menu from './demo/menu';
|
||||
import line_edge from './item/edge/line-edge';
|
||||
import quadratic from './demo/quadratic';
|
||||
import rect from './demo/rect';
|
||||
import tooltip from './demo/tooltip';
|
||||
import cubic_edge from './item/edge/cubic-edge';
|
||||
import cubic_horizon_edge from './item/edge/cubic-horizon-edge';
|
||||
import cubic_vertical_edge from './item/edge/cubic-vertical-edge';
|
||||
import line_edge from './item/edge/line-edge';
|
||||
import layouts_circular from './layouts/circular';
|
||||
import layouts_custom from './layouts/custom';
|
||||
import layouts_d3force from './layouts/d3force';
|
||||
import layouts_dagre from './layouts/dagre';
|
||||
import layouts_force from './layouts/force';
|
||||
import layouts_force_3d from './layouts/force-3d';
|
||||
import layouts_force_wasm from './layouts/force-wasm';
|
||||
import layouts_force_wasm_3d from './layouts/force-wasm-3d';
|
||||
import layouts_forceatlas2_wasm from './layouts/forceatlas2-wasm';
|
||||
import layouts_fruchterman_gpu from './layouts/fruchterman-gpu';
|
||||
import layouts_fruchterman_wasm from './layouts/fruchterman-wasm';
|
||||
import layouts_grid from './layouts/grid';
|
||||
import performance_layout from './performance/layout';
|
||||
import performance_layout_3d from './performance/layout-3d';
|
||||
import performance from './performance/performance';
|
||||
import fisheye from './plugins/fisheye';
|
||||
import tooltip from './demo/tooltip';
|
||||
import comboBasic from './combo/combo-basic';
|
||||
import animations_node_build_in from './animations/node-build-in';
|
||||
import toolbar from './plugins/toolbar';
|
||||
|
||||
import ellipse from './demo/ellipse';
|
||||
import triangle from './demo/triangle';
|
||||
import treeGraph from './tree/treeGraph';
|
||||
import user_defined_canvas from './user-defined-canvas/circular';
|
||||
import visual from './visual/visual';
|
||||
export {
|
||||
animations_node_build_in,
|
||||
behaviors_activateRelations,
|
||||
layouts_circular,
|
||||
layouts_grid,
|
||||
layouts_dagre,
|
||||
layouts_force,
|
||||
layouts_d3force,
|
||||
layouts_custom,
|
||||
user_defined_canvas,
|
||||
layouts_fruchterman_wasm,
|
||||
layouts_forceatlas2_wasm,
|
||||
layouts_force_wasm,
|
||||
layouts_fruchterman_gpu,
|
||||
layouts_force_3d,
|
||||
layouts_force_wasm_3d,
|
||||
behaviors_brush_select,
|
||||
behaviors_click_select,
|
||||
performance,
|
||||
performance_layout,
|
||||
performance_layout_3d,
|
||||
demo,
|
||||
demoFor4,
|
||||
behaviors_collapse_expand_tree,
|
||||
bugReproduce,
|
||||
rect,
|
||||
visual,
|
||||
quadratic,
|
||||
menu,
|
||||
line_edge,
|
||||
comboBasic,
|
||||
cubic_edge,
|
||||
cubic_horizon_edge,
|
||||
cubic_vertical_edge,
|
||||
demo,
|
||||
demoFor4,
|
||||
ellipse,
|
||||
fisheye,
|
||||
layouts_circular,
|
||||
layouts_custom,
|
||||
layouts_d3force,
|
||||
layouts_dagre,
|
||||
layouts_force,
|
||||
layouts_force_3d,
|
||||
layouts_force_wasm,
|
||||
layouts_force_wasm_3d,
|
||||
layouts_forceatlas2_wasm,
|
||||
layouts_fruchterman_gpu,
|
||||
layouts_fruchterman_wasm,
|
||||
layouts_grid,
|
||||
line_edge,
|
||||
menu,
|
||||
performance,
|
||||
performance_layout,
|
||||
performance_layout_3d,
|
||||
quadratic,
|
||||
rect,
|
||||
toolbar,
|
||||
tooltip,
|
||||
comboBasic,
|
||||
animations_node_build_in,
|
||||
treeGraph,
|
||||
triangle,
|
||||
user_defined_canvas,
|
||||
visual,
|
||||
};
|
||||
|
@ -231,7 +231,6 @@ export default () => {
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data: defaultData,
|
||||
modes: {
|
||||
// supported behavior
|
||||
|
@ -234,7 +234,6 @@ export default () => {
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data: defaultData,
|
||||
modes: {
|
||||
// 支持的 behavior
|
||||
|
@ -231,7 +231,6 @@ export default () => {
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data: defaultData,
|
||||
modes: {
|
||||
// supported behavior
|
||||
|
@ -227,7 +227,6 @@ export default (context: TestCaseContext) => {
|
||||
// 2.create graph
|
||||
graph = new Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
data: defaultData,
|
||||
modes: {
|
||||
// supported behavior
|
||||
|
@ -6,7 +6,6 @@ export default (context: TestCaseContext) => {
|
||||
const { width, height } = context;
|
||||
return new G6.Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'd3force',
|
||||
|
@ -17,7 +17,6 @@ export default async () => {
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
renderer: 'webgl-3d',
|
||||
modes: {
|
||||
|
@ -13,7 +13,6 @@ export default async () => {
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'force-wasm',
|
||||
|
@ -17,7 +17,6 @@ export default async () => {
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'forceatlas2-wasm',
|
||||
|
@ -10,7 +10,6 @@ export default async () => {
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'fruchterman-gpu',
|
||||
|
@ -17,7 +17,6 @@ export default async () => {
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'fruchterman-wasm',
|
||||
|
@ -77,7 +77,6 @@ export default async () => {
|
||||
container: $container1,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
modes: {
|
||||
default: [
|
||||
@ -144,7 +143,6 @@ export default async () => {
|
||||
container: $container2,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
modes: {
|
||||
default: [
|
||||
|
@ -135,7 +135,6 @@ export default async () => {
|
||||
container: $container1,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'force-wasm',
|
||||
@ -171,7 +170,6 @@ export default async () => {
|
||||
container: $container2,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'force',
|
||||
|
@ -1944,7 +1944,6 @@ const createGraph = async () => {
|
||||
container: container as HTMLElement,
|
||||
width,
|
||||
height: 1200,
|
||||
type: 'graph',
|
||||
// renderer: 'webgl',
|
||||
data: { nodes, edges },
|
||||
layout: {
|
||||
|
@ -139,7 +139,6 @@ export default async () => {
|
||||
container: 'container',
|
||||
width,
|
||||
height,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
type: 'force',
|
||||
rankdir: 'LR',
|
||||
|
57
packages/g6/tests/demo/plugins/toolbar.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Graph } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
export default (context: TestCaseContext) => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{ id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } },
|
||||
{ id: 'node2', data: { x: 200, y: 250, nodeType: 'b' } },
|
||||
{ id: 'node3', data: { x: 200, y: 350, nodeType: 'b' } },
|
||||
{ id: 'node4', data: { x: 300, y: 250, nodeType: 'c' } },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: { edgeType: 'e1' },
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
data: { edgeType: 'e2' },
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'node3',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
{
|
||||
id: 'edge4',
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const graph = new Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: ['toolbar'],
|
||||
data,
|
||||
});
|
||||
return graph;
|
||||
};
|
193
packages/g6/tests/demo/tree/treeGraph.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
import { treeDataCfg, graphDataCfg } from '../../datasets/dataCfg';
|
||||
|
||||
export default (
|
||||
context: TestCaseContext,
|
||||
options: {
|
||||
dataType?: 'graph' | 'tree';
|
||||
layoutType?: string;
|
||||
defaultCollapse?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const {
|
||||
dataType = 'graph',
|
||||
layoutType = 'compactBox',
|
||||
defaultCollapse = false,
|
||||
} = options;
|
||||
|
||||
const datasets = {
|
||||
tree: JSON.parse(JSON.stringify(treeDataCfg)),
|
||||
graph: JSON.parse(JSON.stringify(graphDataCfg)),
|
||||
};
|
||||
|
||||
const data = datasets[dataType];
|
||||
if (defaultCollapse) {
|
||||
if (dataType === 'tree') data.value[0].children[0].data.collapsed = true;
|
||||
else data.value.nodes[2].data.collapsed = true;
|
||||
}
|
||||
|
||||
const graph = new G6.Graph({
|
||||
...context,
|
||||
layout: {
|
||||
type: layoutType,
|
||||
},
|
||||
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',
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
let currentDataType = dataType;
|
||||
|
||||
const changeDataBtn = document.createElement('button');
|
||||
changeDataBtn.textContent = '切换树/图数据';
|
||||
changeDataBtn.id = 'treegraph-changedata';
|
||||
changeDataBtn.addEventListener('click', (e) => {
|
||||
currentDataType = currentDataType === 'tree' ? 'graph' : 'tree';
|
||||
graph.changeData(datasets[currentDataType]);
|
||||
});
|
||||
document.body.appendChild(changeDataBtn);
|
||||
|
||||
const collapseBtn = document.createElement('button');
|
||||
collapseBtn.textContent = '展开/收起';
|
||||
collapseBtn.id = 'treegraph-collapse';
|
||||
collapseBtn.addEventListener('click', (e) => {
|
||||
const collapseNode = graph.getNodeData('cnode1');
|
||||
console.log('collapseNode?.data.collapsed', collapseNode?.data.collapsed);
|
||||
if (collapseNode?.data.collapsed) graph.expand(['cnode1', 'cnode2']);
|
||||
else graph.collapse(['cnode1', 'cnode2']);
|
||||
});
|
||||
document.body.appendChild(collapseBtn);
|
||||
|
||||
let currentLayout = layoutType;
|
||||
const layoutBtn = document.createElement('button');
|
||||
layoutBtn.id = 'treegraph-changelayout';
|
||||
layoutBtn.textContent = '切换树/图布局';
|
||||
layoutBtn.addEventListener('click', (e) => {
|
||||
if (
|
||||
['compactBox', 'indented', 'mindmap', 'dendrogram'].includes(
|
||||
currentLayout,
|
||||
)
|
||||
)
|
||||
currentLayout = 'grid';
|
||||
else currentLayout = 'compactBox';
|
||||
graph.layout({ type: currentLayout });
|
||||
});
|
||||
document.body.appendChild(layoutBtn);
|
||||
|
||||
let currentAction = 'add';
|
||||
const removeNodeBtn = document.createElement('button');
|
||||
removeNodeBtn.id = 'treegraph-removenode';
|
||||
removeNodeBtn.textContent = '移除/增加节点';
|
||||
removeNodeBtn.addEventListener('click', (e) => {
|
||||
currentAction = currentAction === 'remove' ? 'add' : 'remove';
|
||||
if (currentAction === 'remove') {
|
||||
graph.removeData('node', ['dynamicNode']);
|
||||
} else {
|
||||
graph.addData('node', [{ id: 'dynamicNode', data: { x: 10, y: 10 } }]);
|
||||
graph.addData('edge', [
|
||||
{ id: 'newedge', source: 'node1', target: 'dynamicNode', data: {} },
|
||||
]);
|
||||
}
|
||||
graph.layout();
|
||||
});
|
||||
document.body.appendChild(removeNodeBtn);
|
||||
|
||||
let updateTimes = 0;
|
||||
const updateNodeBtn = document.createElement('button');
|
||||
updateNodeBtn.id = 'treegraph-updatenode';
|
||||
updateNodeBtn.textContent = '更新节点';
|
||||
updateNodeBtn.addEventListener('click', (e) => {
|
||||
updateTimes++;
|
||||
graph.updateData('node', {
|
||||
id: 'node2',
|
||||
data: { labelShape: { text: `updated-${updateTimes}` } },
|
||||
});
|
||||
});
|
||||
document.body.appendChild(updateNodeBtn);
|
||||
|
||||
graph.translateTo({ x: 100, y: 100 });
|
||||
|
||||
return graph;
|
||||
};
|
@ -10,7 +10,6 @@ const createGraph = () => {
|
||||
container: container as HTMLElement,
|
||||
width,
|
||||
height: 1200,
|
||||
type: 'graph',
|
||||
// renderer: 'webgl',
|
||||
data: {
|
||||
nodes: [
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import nodeBuildIn from '../demo/animations/node-build-in';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Animation node buildIn', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import activateRelations from '../demo/behaviors/activate-relations';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext, triggerEvent } from './utils';
|
||||
|
||||
describe('Activate relations behavior', () => {
|
||||
beforeEach(() => {
|
||||
@ -34,8 +34,7 @@ describe('Activate relations behavior', () => {
|
||||
|
||||
// @ts-ignore
|
||||
// mouseEvent.target = canvas.getContextService().getDomElement();
|
||||
triggerEvent(graph, 'mousedown', 81, 50);
|
||||
triggerEvent(graph, 'mouseup', 81, 50);
|
||||
graph.emit('node:click', { itemId: 'node1', itemType: 'node' });
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-activate-relations-activate-node1',
|
||||
@ -44,8 +43,7 @@ describe('Activate relations behavior', () => {
|
||||
/**
|
||||
* Click document to clear active state.
|
||||
*/
|
||||
triggerEvent(graph, 'mousedown', 0, 0);
|
||||
triggerEvent(graph, 'mouseup', 0, 0);
|
||||
graph.emit('canvas:click', {});
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-activate-relations-deactivate-node1',
|
||||
|
@ -0,0 +1,73 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import collapseExpandTree from '../demo/behaviors/collapse-expand-tree';
|
||||
import { createContext } from './utils';
|
||||
|
||||
describe('Collapse or expand a branch', () => {
|
||||
beforeEach(() => {
|
||||
/**
|
||||
* SVG Snapshot testing will generate a unique id for each element.
|
||||
* Reset to 0 to keep snapshot consistent.
|
||||
*/
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = collapseExpandTree({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-collapse-expand',
|
||||
);
|
||||
|
||||
// collapse child branch
|
||||
graph.emit('node:click', { itemId: 'cnode1', itemType: 'node' });
|
||||
setTimeout(async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-collapse-expand-collapse-cnode1',
|
||||
);
|
||||
|
||||
// collapse parent branch
|
||||
graph.emit('node:click', { itemId: 'node1', itemType: 'node' });
|
||||
setTimeout(async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-collapse-expand-collapse-node1',
|
||||
);
|
||||
|
||||
// expand parent branch
|
||||
graph.emit('node:click', { itemId: 'node1', itemType: 'node' });
|
||||
setTimeout(async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-collapse-expand-expand-node1',
|
||||
);
|
||||
// expand child branch
|
||||
graph.emit('node:click', { itemId: 'cnode1', itemType: 'node' });
|
||||
setTimeout(async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-collapse-expand-expand-cnode1',
|
||||
);
|
||||
graph.destroy();
|
||||
done();
|
||||
}, 500);
|
||||
}, 500);
|
||||
}, 500);
|
||||
}, 500);
|
||||
}, 500);
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import lineEdge from '../demo/item/edge/line-edge';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Items edge line', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import circular from '../demo/layouts/circular';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Circular layout', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import d3force from '../demo/layouts/d3force';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('D3Force layout', () => {
|
||||
beforeEach(() => {
|
||||
@ -12,7 +12,10 @@ describe('D3Force layout', () => {
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('should be rendered correctly with Canvas2D', (done) => {
|
||||
/**
|
||||
* D3 force has some random result, which is hard to test with screenshots.
|
||||
*/
|
||||
it.skip('should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
@ -39,7 +42,7 @@ describe('D3Force layout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rendered correctly with SVG', (done) => {
|
||||
it.skip('should be rendered correctly with SVG', (done) => {
|
||||
const dir = `${__dirname}/snapshots/svg`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('svg', 500, 500);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import dagre from '../demo/layouts/dagre';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Dagre layout', () => {
|
||||
beforeEach(() => {
|
||||
@ -33,7 +33,8 @@ describe('Dagre layout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rendered correctly with SVG', (done) => {
|
||||
// TODO: timeout on github ci
|
||||
it.skip('should be rendered correctly with SVG', (done) => {
|
||||
const dir = `${__dirname}/snapshots/svg`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('svg', 500, 500);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import force from '../demo/layouts/force';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe.skip('Force layout', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import grid from '../demo/layouts/grid';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Grid layout', () => {
|
||||
beforeEach(() => {
|
||||
|
61
packages/g6/tests/integration/node-ellipse.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import ellipse from '../demo/demo/ellipse';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import { triggerEvent } from './utils/event';
|
||||
|
||||
describe('node ellipse', () => {
|
||||
it('should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = ellipse({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'node-ellipse');
|
||||
//seleted state
|
||||
triggerEvent(graph, 'mousedown', 100, 100);
|
||||
triggerEvent(graph, 'mouseup', 100, 100);
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'node-ellipse-selected');
|
||||
//normal state
|
||||
triggerEvent(graph, 'mousedown', 100, 100);
|
||||
triggerEvent(graph, 'mouseup', 100, 100);
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'node-ellipse');
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rendered correctly with SVG', (done) => {
|
||||
const dir = `${__dirname}/snapshots/svg`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('svg', 500, 500);
|
||||
|
||||
const graph = ellipse({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'node-ellipse');
|
||||
const $selected = document.querySelector(
|
||||
'input#selected',
|
||||
) as HTMLInputElement;
|
||||
$selected.click();
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'node-ellipse-seleted');
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
61
packages/g6/tests/integration/node-triangle.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import triangle from '../demo/demo/triangle';
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext } from './utils';
|
||||
import { triggerEvent } from './utils/event';
|
||||
|
||||
describe('node triangle', () => {
|
||||
it('should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = triangle({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'node-triangle');
|
||||
//seleted state
|
||||
triggerEvent(graph, 'mousedown', 100, 100);
|
||||
triggerEvent(graph, 'mouseup', 100, 100);
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'node-triangle-selected');
|
||||
//normal state
|
||||
triggerEvent(graph, 'mousedown', 100, 100);
|
||||
triggerEvent(graph, 'mouseup', 100, 100);
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'node-triangle');
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rendered correctly with SVG', (done) => {
|
||||
const dir = `${__dirname}/snapshots/svg`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('svg', 500, 500);
|
||||
|
||||
const graph = triangle({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'node-triangle');
|
||||
const $selected = document.querySelector(
|
||||
'input#selected',
|
||||
) as HTMLInputElement;
|
||||
$selected.click();
|
||||
await expect(canvas).toMatchSVGSnapshot(dir, 'node-triangle-seleted');
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,62 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import toolbar from '../../demo/plugins/toolbar';
|
||||
import { createContext } from '../utils';
|
||||
import '../utils/useSnapshotMatchers';
|
||||
|
||||
describe('Circular layout', () => {
|
||||
beforeEach(() => {
|
||||
/**
|
||||
* SVG Snapshot testing will generate a unique id for each element.
|
||||
* Reset to 0 to keep snapshot consistent.
|
||||
*/
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/../snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
const graph = toolbar({
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
container,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'plugins-toolbar-default',
|
||||
);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// it.skip('should be rendered correctly with WebGL', (done) => {
|
||||
// const dir = `${__dirname}/snapshots/webgl`;
|
||||
// const {
|
||||
// backgroundCanvas,
|
||||
// canvas,
|
||||
// transientCanvas,
|
||||
// container,
|
||||
// } = createContext('webgl', 500, 500);
|
||||
|
||||
// const graph = toolbar({
|
||||
// container,
|
||||
// backgroundCanvas,
|
||||
// canvas,
|
||||
// transientCanvas,
|
||||
// width: 500,
|
||||
// height: 500,
|
||||
// });
|
||||
|
||||
// graph.on('afterlayout', async () => {
|
||||
// await expect(canvas).toMatchWebGLSnapshot(dir, 'toolbar-default');
|
||||
// graph.destroy();
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
});
|
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 7.7 KiB |
BIN
packages/g6/tests/integration/snapshots/canvas/node-ellipse.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 6.4 KiB |
BIN
packages/g6/tests/integration/snapshots/canvas/node-triangle.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 12 KiB |