mirror of
https://gitee.com/antv/g6.git
synced 2024-12-02 03:38:20 +08:00
feat: a demo for v5 and fix several bugs, details are in commits (#4553)
* feat: demo * chore: refine demo * chore: refine demo * perf: demo with themes * perf: demo with webgl renderer * feat: add 3d layout demo * fix: state style affect size of node incorrectly * feat: support text in 3D scene * fix: conflict between behaviors; fix: animate problems; fix: zoomStrategy for individual nodes * chore: rename zoomStrategy to lodStrategy * chore: refine demo * chore: demo refine * chore: type templates problems * chore: lint * chore: refine * feat: constrain the range of zoom-canvas-3d * chore: refine * chore: remove unecessary notes --------- Co-authored-by: yuqi.pyq <yuqi.pyq@antgroup.com>
This commit is contained in:
parent
2b44df189d
commit
1cd6c9d2c3
@ -14,6 +14,7 @@
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"husky": "^8.0.3",
|
||||
"react-scripts": "3.1.2",
|
||||
"vite": "^4.2.1"
|
||||
"vite": "^4.2.1",
|
||||
"stats.js": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
@ -248,10 +248,10 @@ Update a behavior on a mode.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
| :--------- | :------------------------------ | :---------------------------------------------------------------- |
|
||||
| `behavior` | `BehaviorObjectOptionsOf`<`B`\> | behavior configs, whose name indicates the behavior to be updated |
|
||||
| `mode?` | `string` | mode name |
|
||||
| Name | Type | Description |
|
||||
| :--------- | :------------------------ | :---------------------------------------------------------------- |
|
||||
| `behavior` | `BehaviorOptionsOf`<`B`\> | behavior configs, whose name indicates the behavior to be updated |
|
||||
| `mode?` | `string` | mode name |
|
||||
|
||||
#### Returns
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@antv/g6",
|
||||
"version": "5.0.0-beta.1",
|
||||
"version": "5.0.0-alpha.4",
|
||||
"description": "A Graph Visualization Framework in JavaScript",
|
||||
"keywords": [
|
||||
"antv",
|
||||
@ -43,7 +43,7 @@
|
||||
"clean": "rimraf es lib",
|
||||
"coverage": "jest --coverage",
|
||||
"doc": "rimraf apis && typedoc",
|
||||
"lint": "eslint ./src ./tests --quiet && prettier ./src ./tests --check",
|
||||
"lint": "eslint ./src --quiet && prettier ./src --check",
|
||||
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
|
||||
"fix": "eslint ./src ./tests --fix && prettier ./src ./tests --write ",
|
||||
"test": "jest",
|
||||
@ -60,6 +60,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.0.0",
|
||||
"@antv/algorithm": "^0.1.25",
|
||||
"@antv/dom-util": "^2.0.4",
|
||||
"@antv/g": "^5.15.7",
|
||||
"@antv/g-canvas": "^1.9.28",
|
||||
@ -67,11 +68,12 @@
|
||||
"@antv/g-plugin-control": "^1.7.43",
|
||||
"@antv/g-svg": "^1.8.36",
|
||||
"@antv/g-webgl": "^1.7.44",
|
||||
"@antv/g6": "^4.8.13",
|
||||
"@antv/graphlib": "^2.0.0",
|
||||
"@antv/gui": "^0.5.0-alpha.16",
|
||||
"@antv/layout": "^1.2.0",
|
||||
"@antv/layout-gpu": "^1.0.0",
|
||||
"@antv/layout-wasm": "^1.3.0",
|
||||
"@antv/layout-wasm": "1.3.1",
|
||||
"@antv/matrix-util": "^3.0.4",
|
||||
"@antv/util": "~2.0.5",
|
||||
"color": "^4.2.3",
|
||||
@ -103,6 +105,7 @@
|
||||
"rollup-plugin-typescript": "^1.0.1",
|
||||
"rollup-plugin-uglify": "^6.0.4",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"stats.js": "^0.17.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"typedoc": "^0.23.24",
|
||||
"typescript": "^4.6.3",
|
||||
|
@ -12,7 +12,7 @@ module.exports = [
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/g6.min.js',
|
||||
name: 'G6',
|
||||
name: 'G6V5',
|
||||
format: 'umd',
|
||||
sourcemap: false,
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ import { Group } from '@antv/g';
|
||||
import { clone, throttle } from '@antv/util';
|
||||
import { EdgeDisplayModel, EdgeModel, NodeModelData } from '../types';
|
||||
import { EdgeModelData } from '../types/edge';
|
||||
import { DisplayMapper, State, ZoomStrategyObj } from '../types/item';
|
||||
import { DisplayMapper, State, lodStrategyObj } from '../types/item';
|
||||
import { updateShapes } from '../util/shape';
|
||||
import Item from './item';
|
||||
import Node from './node';
|
||||
@ -22,7 +22,7 @@ interface IProps {
|
||||
zoom?: number;
|
||||
theme: {
|
||||
styles: EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
};
|
||||
onframe?: Function;
|
||||
}
|
||||
@ -88,10 +88,11 @@ export default class Edge extends Item {
|
||||
if (firstRendering) {
|
||||
// update the transform
|
||||
this.renderExt.onZoom(this.shapeMap, this.zoom);
|
||||
} else {
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
}
|
||||
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
const timing = firstRendering ? 'buildIn' : 'update';
|
||||
// handle shape's animate
|
||||
if (!disableAnimate && usingAnimates[timing]?.length) {
|
||||
@ -154,7 +155,7 @@ export default class Edge extends Item {
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
return new Edge({
|
||||
const clonedEdge = new Edge({
|
||||
model: clonedModel,
|
||||
renderExtensions: this.renderExtensions,
|
||||
sourceItem,
|
||||
@ -164,9 +165,14 @@ export default class Edge extends Item {
|
||||
stateMapper: this.stateMapper,
|
||||
zoom: this.zoom,
|
||||
theme: {
|
||||
styles: clone(this.themeStyles),
|
||||
zoomStrategy: this.zoomStrategy,
|
||||
styles: this.themeStyles,
|
||||
lodStrategy: this.lodStrategy,
|
||||
},
|
||||
});
|
||||
Object.keys(this.shapeMap).forEach((shapeId) => {
|
||||
if (!this.shapeMap[shapeId].isVisible())
|
||||
clonedEdge.shapeMap[shapeId].hide();
|
||||
});
|
||||
return clonedEdge;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
ItemShapeStyles,
|
||||
ITEM_TYPE,
|
||||
State,
|
||||
ZoomStrategyObj,
|
||||
lodStrategyObj,
|
||||
} from '../types/item';
|
||||
import { NodeShapeMap } from '../types/node';
|
||||
import { EdgeStyleSet, NodeStyleSet } from '../types/theme';
|
||||
@ -23,8 +23,10 @@ import {
|
||||
getShapeAnimateBeginStyles,
|
||||
animateShapes,
|
||||
GROUP_ANIMATE_STYLES,
|
||||
stopAnimate,
|
||||
} from '../util/animate';
|
||||
import { AnimateTiming, IAnimates } from '../types/animate';
|
||||
import { formatLodStrategy } from '../util/zoom';
|
||||
|
||||
export default abstract class Item implements IItem {
|
||||
public destroyed = false;
|
||||
@ -58,6 +60,8 @@ export default abstract class Item implements IItem {
|
||||
public afterDrawShapeMap = {};
|
||||
/** Set to different value in implements. */
|
||||
public type: ITEM_TYPE;
|
||||
/** The flag of transient item. */
|
||||
public transient: Boolean = false;
|
||||
public renderExtensions: any; // TODO
|
||||
/** Cache the animation instances to stop at next lifecycle. */
|
||||
public animations: IAnimation[];
|
||||
@ -66,8 +70,8 @@ export default abstract class Item implements IItem {
|
||||
default?: ItemShapeStyles;
|
||||
[stateName: string]: ItemShapeStyles;
|
||||
};
|
||||
/** The zoom strategy to show and hide shapes according to their showLevel. */
|
||||
public zoomStrategy: ZoomStrategyObj;
|
||||
/** The zoom strategy to show and hide shapes according to their lod. */
|
||||
public lodStrategy: lodStrategyObj;
|
||||
/** Last zoom ratio. */
|
||||
public zoom: number;
|
||||
/** Cache the chaging states which are not consomed by draw */
|
||||
@ -108,14 +112,20 @@ export default abstract class Item implements IItem {
|
||||
this.stateMapper = stateMapper;
|
||||
this.displayModel = this.getDisplayModelAndChanges(model).model;
|
||||
this.renderExtensions = renderExtensions;
|
||||
const { type = this.type === 'node' ? 'circle-node' : 'line-edge' } =
|
||||
this.displayModel.data;
|
||||
const {
|
||||
type = this.type === 'node' ? 'circle-node' : 'line-edge',
|
||||
lodStrategy: modelLodStrategy,
|
||||
} = this.displayModel.data;
|
||||
const RenderExtension = renderExtensions.find((ext) => ext.type === type);
|
||||
this.themeStyles = theme.styles;
|
||||
const lodStrategy = modelLodStrategy
|
||||
? formatLodStrategy(modelLodStrategy)
|
||||
: theme.lodStrategy;
|
||||
this.renderExt = new RenderExtension({
|
||||
themeStyles: this.themeStyles.default,
|
||||
zoomStrategy: theme.zoomStrategy,
|
||||
lodStrategy,
|
||||
device: this.device,
|
||||
zoom: this.zoom,
|
||||
});
|
||||
}
|
||||
|
||||
@ -163,7 +173,7 @@ export default abstract class Item implements IItem {
|
||||
isReplace?: boolean,
|
||||
itemTheme?: {
|
||||
styles: NodeStyleSet | EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
onfinish?: Function,
|
||||
@ -172,7 +182,6 @@ export default abstract class Item implements IItem {
|
||||
this.model = model;
|
||||
if (itemTheme) {
|
||||
this.themeStyles = itemTheme.styles;
|
||||
this.zoomStrategy = itemTheme.zoomStrategy;
|
||||
}
|
||||
// 2. map new merged model to displayModel, keep prevModel and newModel for 3.
|
||||
const { model: displayModel, typeChange } = this.getDisplayModelAndChanges(
|
||||
@ -182,6 +191,10 @@ export default abstract class Item implements IItem {
|
||||
);
|
||||
this.displayModel = displayModel;
|
||||
|
||||
this.lodStrategy = displayModel.data.lodStrategy
|
||||
? formatLodStrategy(displayModel.data.lodStrategy)
|
||||
: itemTheme?.lodStrategy || this.lodStrategy;
|
||||
|
||||
if (onlyMove) {
|
||||
this.updatePosition(displayModel, diffData, onfinish);
|
||||
return;
|
||||
@ -197,12 +210,13 @@ export default abstract class Item implements IItem {
|
||||
);
|
||||
this.renderExt = new RenderExtension({
|
||||
themeStyles: this.themeStyles.default,
|
||||
zoomStrategy: this.zoomStrategy,
|
||||
lodStrategy: this.lodStrategy,
|
||||
device: this.device,
|
||||
zoom: this.zoom,
|
||||
});
|
||||
} else {
|
||||
this.renderExt.themeStyles = this.themeStyles.default;
|
||||
this.renderExt.zoomStrategy = this.zoomStrategy;
|
||||
this.renderExt.lodStrategy = this.lodStrategy;
|
||||
}
|
||||
// 3. call element update fn from useLib
|
||||
if (this.states?.length) {
|
||||
@ -630,10 +644,7 @@ export default abstract class Item implements IItem {
|
||||
// displayModel
|
||||
{
|
||||
...this.displayModel,
|
||||
data: {
|
||||
...displayModelData,
|
||||
...styles,
|
||||
},
|
||||
data: mergeStyles([displayModelData, styles]),
|
||||
} as ItemDisplayModel,
|
||||
// diffData
|
||||
undefined,
|
||||
@ -675,13 +686,7 @@ export default abstract class Item implements IItem {
|
||||
* Stop all the animations on the item.
|
||||
*/
|
||||
public stopAnimations() {
|
||||
this.animations?.forEach((animation) => {
|
||||
const timing = animation.effect.getTiming();
|
||||
if (animation.playState !== 'running') return;
|
||||
animation.currentTime =
|
||||
Number(timing.duration) + Number(timing.delay || 0);
|
||||
animation.cancel();
|
||||
});
|
||||
this.animations?.forEach(stopAnimate);
|
||||
this.animations = [];
|
||||
}
|
||||
|
||||
@ -719,7 +724,7 @@ export default abstract class Item implements IItem {
|
||||
// 1. stop animations, run buildOut animations
|
||||
this.stopAnimations();
|
||||
const { animates } = this.displayModel.data;
|
||||
if (animates.buildOut?.length) {
|
||||
if (animates?.buildOut?.length && !this.transient) {
|
||||
this.animations = this.runWithAnimates(
|
||||
animates,
|
||||
'buildOut',
|
||||
|
@ -2,7 +2,7 @@ import { Group } from '@antv/g';
|
||||
import { clone } from '@antv/util';
|
||||
import { Point } from '../types/common';
|
||||
import { NodeModel } from '../types';
|
||||
import { DisplayMapper, State, ZoomStrategyObj } from '../types/item';
|
||||
import { DisplayMapper, State, lodStrategyObj } from '../types/item';
|
||||
import { NodeDisplayModel, NodeModelData } from '../types/node';
|
||||
import { NodeStyleSet } from '../types/theme';
|
||||
import { updateShapes } from '../util/shape';
|
||||
@ -26,7 +26,7 @@ interface IProps {
|
||||
zoom?: number;
|
||||
theme: {
|
||||
styles: NodeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
};
|
||||
device?: any; // for 3d shapes
|
||||
onframe?: Function;
|
||||
@ -74,6 +74,8 @@ export default class Node extends Item {
|
||||
// first rendering, move the group
|
||||
group.setLocalPosition(x, y, z);
|
||||
} else {
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
this.updatePosition(displayModel, diffData, onfinish);
|
||||
}
|
||||
|
||||
@ -90,8 +92,6 @@ export default class Node extends Item {
|
||||
renderExt.onZoom(this.shapeMap, this.zoom);
|
||||
}
|
||||
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
// handle shape's and group's animate
|
||||
if (!disableAnimate && animates) {
|
||||
const animatesExcludePosition = getAnimatesExcludePosition(animates);
|
||||
@ -114,7 +114,7 @@ export default class Node extends Item {
|
||||
isReplace?: boolean,
|
||||
theme?: {
|
||||
styles: NodeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
onfinish?: Function,
|
||||
@ -150,8 +150,8 @@ export default class Node extends Item {
|
||||
this.animateFrameListener,
|
||||
() => onfinish(displayModel.id),
|
||||
);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
group.setLocalPosition(x, y, z);
|
||||
}
|
||||
@ -172,7 +172,7 @@ export default class Node extends Item {
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
return new Node({
|
||||
const clonedNode = new Node({
|
||||
model: clonedModel,
|
||||
renderExtensions: this.renderExtensions,
|
||||
containerGroup,
|
||||
@ -180,10 +180,15 @@ export default class Node extends Item {
|
||||
stateMapper: this.stateMapper,
|
||||
zoom: this.zoom,
|
||||
theme: {
|
||||
styles: clone(this.themeStyles),
|
||||
zoomStrategy: this.zoomStrategy,
|
||||
styles: this.themeStyles,
|
||||
lodStrategy: this.lodStrategy,
|
||||
},
|
||||
});
|
||||
Object.keys(this.shapeMap).forEach((shapeId) => {
|
||||
if (!this.shapeMap[shapeId].isVisible())
|
||||
clonedNode.shapeMap[shapeId].hide();
|
||||
});
|
||||
return clonedNode;
|
||||
}
|
||||
|
||||
public getAnchorPoint(point: Point) {
|
||||
|
@ -247,7 +247,6 @@ export class InteractionController {
|
||||
|
||||
private initEvents = () => {
|
||||
Object.values(CANVAS_EVENT_TYPE).forEach((eventName) => {
|
||||
// console.debug('Listen on canvas: ', eventName);
|
||||
this.graph.canvas.document.addEventListener(
|
||||
eventName,
|
||||
this.handleCanvasEvent,
|
||||
@ -262,13 +261,6 @@ export class InteractionController {
|
||||
};
|
||||
|
||||
private handleCanvasEvent = (gEvent: FederatedPointerEvent) => {
|
||||
// const debug = gEvent.type.includes('over') || gEvent.type.includes('move') ? () => {} : console.debug;
|
||||
|
||||
// Find the Node/Edge/Combo group element.
|
||||
// const itemGroup = findItemGroup(gEvent.target as IElement);
|
||||
// const itemType = itemGroup?.getAttribute('data-item-type') || 'canvas';
|
||||
// const itemId = itemGroup?.getAttribute('data-item-id') || 'CANVAS';
|
||||
|
||||
const itemInfo = getItemInfoFromElement(gEvent.target as IElement);
|
||||
if (!itemInfo) {
|
||||
// This event was triggered from an element which belongs to none of the nodes/edges/canvas.
|
||||
@ -313,7 +305,6 @@ export class InteractionController {
|
||||
}
|
||||
this.graph.emit(`${itemType}:${gEvent.type}`, event);
|
||||
this.graph.emit(`${gEvent.type}`, event);
|
||||
// debug(`Item ${event.type} :`, event);
|
||||
}
|
||||
};
|
||||
|
||||
@ -328,14 +319,11 @@ export class InteractionController {
|
||||
const preType = prevItemInfo.itemType;
|
||||
this.graph.emit(`${preType}:pointerleave`, {
|
||||
...event,
|
||||
itemId: prevItemInfo.itemId,
|
||||
itemType: prevItemInfo.itemType,
|
||||
type: 'pointerleave',
|
||||
target: prevItemInfo.groupElement,
|
||||
});
|
||||
// console.debug(`${preType}:pointerleave`, {
|
||||
// ...event,
|
||||
// type: 'pointerleave',
|
||||
// target: prevItemInfo.groupElement,
|
||||
// });
|
||||
}
|
||||
if (curItemInfo) {
|
||||
const curType = curItemInfo.itemType;
|
||||
@ -344,11 +332,6 @@ export class InteractionController {
|
||||
type: 'pointerenter',
|
||||
target: curItemInfo.groupElement,
|
||||
});
|
||||
// console.debug(`${curType}:pointerenter`, {
|
||||
// ...event,
|
||||
// type: 'pointerenter',
|
||||
// target: curItemInfo.groupElement,
|
||||
// });
|
||||
}
|
||||
}
|
||||
this.prevItemInfo = curItemInfo;
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
ITEM_TYPE,
|
||||
ShapeStyle,
|
||||
SHAPE_TYPE,
|
||||
ZoomStrategyObj,
|
||||
lodStrategyObj,
|
||||
} from '../../types/item';
|
||||
import {
|
||||
ThemeSpecification,
|
||||
@ -39,7 +39,7 @@ import {
|
||||
} from '../../types/theme';
|
||||
import { DirectionalLight, AmbientLight } from '@antv/g-plugin-3d';
|
||||
import { ViewportChangeHookParams } from '../../types/hook';
|
||||
import { formatZoomStrategy } from '../../util/zoom';
|
||||
import { formatLodStrategy } from '../../util/zoom';
|
||||
|
||||
/**
|
||||
* Manages and stores the node / edge / combo items.
|
||||
@ -125,6 +125,8 @@ export class ItemController {
|
||||
);
|
||||
this.graph.hooks.transientupdate.tap(this.onTransientUpdate.bind(this));
|
||||
this.graph.hooks.viewportchange.tap(this.onViewportChange.bind(this));
|
||||
this.graph.hooks.themechange.tap(this.onThemeChange.bind(this));
|
||||
this.graph.hooks.destroy.tap(this.onDestroy.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -196,7 +198,7 @@ export class ItemController {
|
||||
graph.canvas.appendChild(ambientLight);
|
||||
graph.canvas.appendChild(light);
|
||||
const { width, height } = graph.canvas.getConfig();
|
||||
graph.canvas.getCamera().setPerspective(0.1, 5000, 45, width / height);
|
||||
graph.canvas.getCamera().setPerspective(0.1, 50000, 45, width / height);
|
||||
}
|
||||
|
||||
// 2. create node / edge / combo items, classes from ../../item, and element drawing and updating fns from node/edge/comboExtensions
|
||||
@ -287,8 +289,8 @@ export class ItemController {
|
||||
const { dataTypeField: nodeDataTypeField } = nodeTheme;
|
||||
const edgeToUpdate = {};
|
||||
const updateEdges = throttle(
|
||||
() => {
|
||||
Object.keys(edgeToUpdate).forEach((id) => {
|
||||
(updateMap) => {
|
||||
Object.keys(updateMap || edgeToUpdate).forEach((id) => {
|
||||
const item = itemMap[id] as Edge;
|
||||
if (item && !item.destroyed) item.forceUpdate();
|
||||
});
|
||||
@ -316,7 +318,15 @@ export class ItemController {
|
||||
}
|
||||
const node = itemMap[id] as Node;
|
||||
const innerModel = graphCore.getNode(id);
|
||||
node.onframe = updateEdges;
|
||||
|
||||
const relatedEdgeInnerModels = graphCore.getRelatedEdges(id);
|
||||
const nodeRelatedToUpdate = {};
|
||||
relatedEdgeInnerModels.forEach((edge) => {
|
||||
edgeToUpdate[edge.id] = edge;
|
||||
nodeRelatedToUpdate[edge.id] = edge;
|
||||
});
|
||||
|
||||
node.onframe = () => updateEdges(nodeRelatedToUpdate);
|
||||
node.update(
|
||||
innerModel,
|
||||
{ previous, current },
|
||||
@ -328,10 +338,6 @@ export class ItemController {
|
||||
node.onframe = undefined;
|
||||
},
|
||||
);
|
||||
const relatedEdgeInnerModels = graphCore.getRelatedEdges(id);
|
||||
relatedEdgeInnerModels.forEach((edge) => {
|
||||
edgeToUpdate[edge.id] = edge;
|
||||
});
|
||||
});
|
||||
updateEdges();
|
||||
}
|
||||
@ -461,6 +467,40 @@ export class ItemController {
|
||||
false,
|
||||
);
|
||||
|
||||
private onThemeChange = ({ theme }) => {
|
||||
if (!theme) return;
|
||||
const { nodeDataTypeSet, edgeDataTypeSet } = this;
|
||||
const { node: nodeTheme, edge: edgeTheme } = theme;
|
||||
Object.values(this.itemMap).forEach((item) => {
|
||||
const itemTye = item.getType();
|
||||
const usingTheme = itemTye === 'node' ? nodeTheme : edgeTheme;
|
||||
const usingTypeSet =
|
||||
itemTye === 'node' ? nodeDataTypeSet : edgeDataTypeSet;
|
||||
const { dataTypeField } = usingTheme;
|
||||
let dataType;
|
||||
if (dataTypeField) dataType = item.model.data[dataTypeField] as string;
|
||||
const itemTheme = getItemTheme(
|
||||
usingTypeSet,
|
||||
dataTypeField,
|
||||
dataType,
|
||||
usingTheme,
|
||||
);
|
||||
item.update(
|
||||
item.model,
|
||||
undefined,
|
||||
false,
|
||||
itemTheme as {
|
||||
styles: NodeStyleSet;
|
||||
lodStrategy: lodStrategyObj;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
private onDestroy = () => {
|
||||
Object.values(this.itemMap).forEach((item) => item.destroy());
|
||||
};
|
||||
|
||||
private onTransientUpdate(param: {
|
||||
type: ITEM_TYPE | SHAPE_TYPE;
|
||||
id: ID;
|
||||
@ -583,7 +623,7 @@ export class ItemController {
|
||||
zoom,
|
||||
theme: itemTheme as {
|
||||
styles: NodeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
},
|
||||
device:
|
||||
graph.rendererType === 'webgl-3d'
|
||||
@ -640,7 +680,7 @@ export class ItemController {
|
||||
zoom,
|
||||
theme: itemTheme as {
|
||||
styles: EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -717,16 +757,16 @@ const getItemTheme = (
|
||||
itemTheme: NodeThemeSpecifications | EdgeThemeSpecifications,
|
||||
): {
|
||||
styles: NodeStyleSet | EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
lodStrategy: lodStrategyObj;
|
||||
} => {
|
||||
const { styles: themeStyles, zoomStrategy } = itemTheme;
|
||||
const formattedZoomStrategy = formatZoomStrategy(zoomStrategy);
|
||||
const { styles: themeStyles, lodStrategy } = itemTheme;
|
||||
const formattedLodStrategy = formatLodStrategy(lodStrategy);
|
||||
if (!dataTypeField) {
|
||||
// dataType field is not assigned
|
||||
const styles = isArray(themeStyles)
|
||||
? themeStyles[0]
|
||||
: Object.values(themeStyles)[0];
|
||||
return { styles, zoomStrategy: formattedZoomStrategy };
|
||||
return { styles, lodStrategy: formattedLodStrategy };
|
||||
}
|
||||
dataTypeSet.add(dataType as string);
|
||||
let themeStyle;
|
||||
@ -739,6 +779,6 @@ const getItemTheme = (
|
||||
}
|
||||
return {
|
||||
styles: themeStyle,
|
||||
zoomStrategy: formattedZoomStrategy,
|
||||
lodStrategy: formattedLodStrategy,
|
||||
};
|
||||
};
|
||||
|
@ -55,6 +55,8 @@ export class LayoutController {
|
||||
|
||||
const { graphCore, options } = params;
|
||||
|
||||
this.graph.emit('startlayout');
|
||||
|
||||
if (isImmediatelyInvokedLayoutOptions(options)) {
|
||||
const {
|
||||
animated = false,
|
||||
@ -145,6 +147,8 @@ export class LayoutController {
|
||||
}
|
||||
}
|
||||
|
||||
this.graph.emit('endlayout');
|
||||
|
||||
// Update nodes' positions.
|
||||
this.updateNodesPosition(positions);
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export class ThemeController {
|
||||
this.extension = this.getExtension();
|
||||
this.themes = this.getThemes();
|
||||
this.graph.hooks.init.tap(this.onInit.bind(this));
|
||||
this.graph.hooks.themechange.tap(this.onThemeChange.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,4 +60,10 @@ export class ThemeController {
|
||||
Object.keys(canvas).forEach((key) => (dom.style[key] = canvas[key]));
|
||||
}
|
||||
}
|
||||
|
||||
private onThemeChange({ canvases }) {
|
||||
if (!canvases) return;
|
||||
this.extension = this.getExtension();
|
||||
this.onInit({ canvases });
|
||||
}
|
||||
}
|
||||
|
@ -48,11 +48,11 @@ export class ViewportController {
|
||||
}> = {};
|
||||
|
||||
if (translate) {
|
||||
const { dx = 0, dy = 0 } = translate;
|
||||
const [px, py] = camera.getPosition();
|
||||
const [fx, fy] = camera.getFocalPoint();
|
||||
landmarkOptions.position = [px - dx, py - dy];
|
||||
landmarkOptions.focalPoint = [fx - dx, fy - dy];
|
||||
const { dx = 0, dy = 0, dz = 0 } = translate;
|
||||
const [px, py, pz] = camera.getPosition();
|
||||
const [fx, fy, fz] = camera.getFocalPoint();
|
||||
landmarkOptions.position = [px - dx, py - dy, pz - dz];
|
||||
landmarkOptions.focalPoint = [fx - dx, fy - dy, fz - dz];
|
||||
}
|
||||
|
||||
if (zoom) {
|
||||
@ -73,6 +73,7 @@ export class ViewportController {
|
||||
`mark${landmarkCounter}`,
|
||||
landmarkOptions,
|
||||
);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
transientCamera.gotoLandmark(transientLandmark, {
|
||||
duration: Number(duration),
|
||||
|
@ -11,11 +11,7 @@ import {
|
||||
Specification,
|
||||
} from '../types';
|
||||
import { CameraAnimationOptions } from '../types/animate';
|
||||
import {
|
||||
BehaviorObjectOptionsOf,
|
||||
BehaviorOptionsOf,
|
||||
BehaviorRegistry,
|
||||
} from '../types/behavior';
|
||||
import { BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
|
||||
import { ComboModel } from '../types/combo';
|
||||
import { Padding, Point } from '../types/common';
|
||||
import { DataChangeType, GraphCore } from '../types/data';
|
||||
@ -29,9 +25,13 @@ import {
|
||||
} from '../types/layout';
|
||||
import { NodeModel, NodeModelData } from '../types/node';
|
||||
import { RendererName } from '../types/render';
|
||||
import { ThemeRegistry, ThemeSpecification } from '../types/theme';
|
||||
import {
|
||||
ThemeOptionsOf,
|
||||
ThemeRegistry,
|
||||
ThemeSpecification,
|
||||
} from '../types/theme';
|
||||
import { FitViewRules, GraphTransformOptions } from '../types/view';
|
||||
import { createCanvas } from '../util/canvas';
|
||||
import { changeRenderer, createCanvas } from '../util/canvas';
|
||||
import { formatPadding } from '../util/shape';
|
||||
import {
|
||||
DataController,
|
||||
@ -187,6 +187,16 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
).then(() => (this.canvasReady = true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the renderer at runtime.
|
||||
* @param type renderer name
|
||||
* @returns
|
||||
*/
|
||||
public changeRenderer(type) {
|
||||
this.rendererType = type || 'canvas';
|
||||
changeRenderer(this.rendererType, this.canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the hooks for graph's lifecycles.
|
||||
*/
|
||||
@ -249,6 +259,14 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
| { key: string; type: string; [cfgName: string]: unknown }
|
||||
)[];
|
||||
}>({ name: 'pluginchange' }),
|
||||
themechange: new Hook<{
|
||||
theme: ThemeSpecification;
|
||||
canvases: {
|
||||
background: Canvas;
|
||||
main: Canvas;
|
||||
transient: Canvas;
|
||||
};
|
||||
}>({ name: 'init' }),
|
||||
destroy: new Hook<{}>({ name: 'destroy' }),
|
||||
};
|
||||
}
|
||||
@ -256,10 +274,30 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
/**
|
||||
* Update the specs(configurations).
|
||||
*/
|
||||
public updateSpecification(spec: Specification<B, T>) {
|
||||
public updateSpecification(spec: Specification<B, T>): Specification<B, T> {
|
||||
return Object.assign(this.specification, spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the theme specs (configurations).
|
||||
*/
|
||||
public updateTheme(theme: ThemeOptionsOf<T>) {
|
||||
this.specification.theme = theme;
|
||||
// const { specification } = this.themeController;
|
||||
// notifiying the themeController
|
||||
this.hooks.themechange.emit({
|
||||
canvases: {
|
||||
background: this.backgroundCanvas,
|
||||
main: this.canvas,
|
||||
transient: this.transientCanvas,
|
||||
},
|
||||
});
|
||||
// theme is formatted by themeController, notify the item controller to update the items
|
||||
this.hooks.themechange.emit({
|
||||
theme: this.themeController.specification,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copy of specs(configurations).
|
||||
* @returns graph specs
|
||||
@ -358,16 +396,16 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public async translate(
|
||||
dx: number,
|
||||
dy: number,
|
||||
distance: Partial<{
|
||||
dx: number;
|
||||
dy: number;
|
||||
dz: number;
|
||||
}>,
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) {
|
||||
await this.transform(
|
||||
{
|
||||
translate: {
|
||||
dx,
|
||||
dy,
|
||||
},
|
||||
translate: distance,
|
||||
},
|
||||
effectTiming,
|
||||
);
|
||||
@ -383,7 +421,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) {
|
||||
const { x: cx, y: cy } = this.getViewportCenter();
|
||||
await this.translate(cx - x, cy - y, effectTiming);
|
||||
await this.translate({ dx: cx - x, dy: cy - y }, effectTiming);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1210,7 +1248,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns
|
||||
* @group Interaction
|
||||
*/
|
||||
public updateBehavior(behavior: BehaviorObjectOptionsOf<B>, mode?: string) {
|
||||
public updateBehavior(behavior: BehaviorOptionsOf<B>, mode?: string) {
|
||||
this.hooks.behaviorchange.emit({
|
||||
action: 'update',
|
||||
modes: [mode],
|
||||
@ -1377,10 +1415,14 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns
|
||||
* @group Graph Instance
|
||||
*/
|
||||
public destroy() {
|
||||
this.canvas.destroy();
|
||||
this.backgroundCanvas.destroy();
|
||||
this.transientCanvas.destroy();
|
||||
public destroy(callback?: Function) {
|
||||
// TODO: call the destroy functions after items' buildOut animations finished
|
||||
setTimeout(() => {
|
||||
this.canvas.destroy();
|
||||
this.backgroundCanvas.destroy();
|
||||
this.transientCanvas.destroy();
|
||||
callback?.();
|
||||
}, 500);
|
||||
|
||||
this.hooks.destroy.emit({});
|
||||
|
||||
|
@ -21,31 +21,31 @@ interface ActivateRelationsOptions {
|
||||
* Defaults to true.
|
||||
* If set to false, `trigger` options will be ignored.
|
||||
*/
|
||||
multiple: boolean;
|
||||
multiple?: boolean;
|
||||
/**
|
||||
* The key to pressed with mouse click to apply multiple selection.
|
||||
* Defaults to `"click"`.
|
||||
* Could be "click", "mouseenter".
|
||||
*/
|
||||
trigger: Trigger;
|
||||
trigger?: Trigger;
|
||||
|
||||
/**
|
||||
*
|
||||
* Defaults to `"selected"`.
|
||||
*
|
||||
*/
|
||||
activeState: 'selected';
|
||||
activeState?: 'selected';
|
||||
|
||||
/**
|
||||
* Whether allow the behavior happen on the current item.
|
||||
*/
|
||||
shouldBegin: (event: IG6GraphEvent) => boolean;
|
||||
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;
|
||||
shouldUpdate?: (event: IG6GraphEvent) => boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ActivateRelationsOptions = {
|
||||
|
@ -123,13 +123,9 @@ export default class BrushSelect extends Behavior {
|
||||
|
||||
getEvents = () => {
|
||||
return {
|
||||
// 'dragstart': this.onMouseDown,
|
||||
// 'drag': this.onMouseMove,
|
||||
// 'dragend': this.onMouseUp,
|
||||
|
||||
'canvas:pointerdown': this.onMouseDown,
|
||||
'canvas:pointermove': this.onMouseMove,
|
||||
'canvas:pointerup': this.onMouseUp,
|
||||
pointerdown: this.onMouseDown,
|
||||
pointermove: this.onMouseMove,
|
||||
pointerup: this.onMouseUp,
|
||||
};
|
||||
};
|
||||
|
||||
@ -147,16 +143,19 @@ export default class BrushSelect extends Behavior {
|
||||
|
||||
public onMouseDown(event: IG6GraphEvent) {
|
||||
if (!this.options.shouldBegin(event)) return;
|
||||
const { itemId, canvas } = event;
|
||||
// should not begin at an item
|
||||
if (itemId) return;
|
||||
const { itemId, itemType, canvas } = event;
|
||||
// should not begin at node
|
||||
if (itemId && itemType === 'node') return;
|
||||
|
||||
this.beginPoint = {
|
||||
x: canvas.x,
|
||||
y: canvas.y,
|
||||
};
|
||||
|
||||
if (!this.isKeydown(event as any)) return;
|
||||
if (!this.isKeydown(event as any)) {
|
||||
this.clearStates();
|
||||
return;
|
||||
}
|
||||
|
||||
const { brush } = this;
|
||||
if (!brush) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { IG6GraphEvent } from '../../types/event';
|
||||
import { Point } from '../../types/common';
|
||||
|
||||
const ALLOWED_TRIGGERS = ['shift', 'ctrl', 'alt', 'meta'] as const;
|
||||
type Trigger = (typeof ALLOWED_TRIGGERS)[number];
|
||||
@ -19,7 +21,7 @@ interface ClickSelectOptions {
|
||||
trigger: Trigger;
|
||||
/**
|
||||
* Item types to be able to select.
|
||||
* Defaults to `["nodes"]`.
|
||||
* Defaults to `["node"]`.
|
||||
* Should be an array of "node", "edge", or "combo".
|
||||
*/
|
||||
itemTypes: Array<'node' | 'edge' | 'combo'>;
|
||||
@ -49,7 +51,7 @@ const DEFAULT_OPTIONS: ClickSelectOptions = {
|
||||
multiple: true,
|
||||
trigger: 'shift',
|
||||
selectedState: 'selected',
|
||||
itemTypes: ['node'],
|
||||
itemTypes: ['node', 'edge'],
|
||||
eventName: '',
|
||||
shouldBegin: () => true,
|
||||
shouldUpdate: () => true,
|
||||
@ -57,6 +59,15 @@ const DEFAULT_OPTIONS: ClickSelectOptions = {
|
||||
|
||||
export default class ClickSelect extends Behavior {
|
||||
options: ClickSelectOptions;
|
||||
/**
|
||||
* Cache the ids of items selected by this behavior
|
||||
*/
|
||||
private selectedIds: ID[] = [];
|
||||
/**
|
||||
* Two flag to avoid onCanvasClick triggered while dragging canvas
|
||||
*/
|
||||
private canvasPointerDown: Point | undefined = undefined;
|
||||
private canvasPointerMove: Boolean = false;
|
||||
|
||||
constructor(options: Partial<ClickSelectOptions>) {
|
||||
super(Object.assign({}, DEFAULT_OPTIONS, options));
|
||||
@ -73,7 +84,9 @@ export default class ClickSelect extends Behavior {
|
||||
return {
|
||||
'node:click': this.onClick,
|
||||
'edge:click': this.onClick,
|
||||
'canvas:click': this.onCanvasClick,
|
||||
'canvas:pointerdown': this.onCanvasPointerDown,
|
||||
'canvas:pointermove': this.onCanvasPointerMove,
|
||||
'canvas:pointerup': this.onCanvasPointerUp,
|
||||
};
|
||||
};
|
||||
|
||||
@ -111,17 +124,15 @@ export default class ClickSelect extends Behavior {
|
||||
|
||||
// Select/Unselect item.
|
||||
if (this.options.shouldUpdate(event)) {
|
||||
if (multiple) {
|
||||
this.graph.setItemState(itemId, state, isSelectAction);
|
||||
} else {
|
||||
if (!multiple) {
|
||||
// Not multiple, clear all currently selected items
|
||||
const selectedItemIds = [
|
||||
...this.graph.findIdByState('node', state),
|
||||
...this.graph.findIdByState('edge', state),
|
||||
...this.graph.findIdByState('combo', state),
|
||||
];
|
||||
this.graph.setItemState(selectedItemIds, state, false);
|
||||
this.graph.setItemState(itemId, state, isSelectAction);
|
||||
this.graph.setItemState(this.selectedIds, state, false);
|
||||
}
|
||||
this.graph.setItemState(itemId, state, isSelectAction);
|
||||
if (isSelectAction) {
|
||||
this.selectedIds.push(itemId);
|
||||
} else {
|
||||
this.selectedIds.filter((id) => id !== itemId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,21 +152,38 @@ export default class ClickSelect extends Behavior {
|
||||
}
|
||||
}
|
||||
|
||||
public onCanvasPointerDown(event: IG6GraphEvent) {
|
||||
this.canvasPointerDown = {
|
||||
x: event.canvas.x,
|
||||
y: event.canvas.y,
|
||||
};
|
||||
this.canvasPointerMove = false;
|
||||
}
|
||||
public onCanvasPointerMove(event: IG6GraphEvent) {
|
||||
if (this.canvasPointerDown) {
|
||||
const deltaX = Math.abs(this.canvasPointerDown.x - event.canvas.x);
|
||||
const deltaY = Math.abs(this.canvasPointerDown.y - event.canvas.y);
|
||||
if (deltaX > 1 || deltaY > 1) this.canvasPointerMove = true;
|
||||
}
|
||||
}
|
||||
|
||||
public onCanvasPointerUp(event: IG6GraphEvent) {
|
||||
if (this.canvasPointerDown && !this.canvasPointerMove)
|
||||
this.onCanvasClick(event);
|
||||
this.canvasPointerDown = undefined;
|
||||
this.canvasPointerMove = false;
|
||||
}
|
||||
|
||||
public onCanvasClick(event: IG6GraphEvent) {
|
||||
if (!this.options.shouldBegin(event)) return;
|
||||
|
||||
// Find current selected items.
|
||||
const state = this.options.selectedState;
|
||||
const selectedItemIds = [
|
||||
...this.graph.findIdByState('node', state),
|
||||
...this.graph.findIdByState('edge', state),
|
||||
...this.graph.findIdByState('combo', state),
|
||||
];
|
||||
if (!selectedItemIds.length) return;
|
||||
if (!this.selectedIds.length) return;
|
||||
|
||||
// Unselect all items.
|
||||
if (this.options.shouldUpdate(event)) {
|
||||
this.graph.setItemState(selectedItemIds, state, false);
|
||||
this.graph.setItemState(this.selectedIds, state, false);
|
||||
}
|
||||
|
||||
// Emit an event.
|
||||
|
@ -24,19 +24,24 @@ export interface DragCanvasOptions {
|
||||
* The assistant secondary key on keyboard. If it is not assigned, the behavior will be triggered when trigger happens.
|
||||
*/
|
||||
secondaryKey?: string;
|
||||
/**
|
||||
* The assistant secondary key on keyboard to prevent the behavior to be tiggered. 'shift' by default.
|
||||
*/
|
||||
secondaryKeyToDisable?: string;
|
||||
/**
|
||||
* The key on keyboard to speed up translating while pressing and drag-canvas by direction keys. The trigger should be 'directionKeys' for this option.
|
||||
*/
|
||||
speedUpKey?: string;
|
||||
/**
|
||||
* The range of canvas to limit dragging, 0 by default, which means the graph cannot be dragged totally out of the view port range.
|
||||
* If scalableRange > 0, the graph can be dragged out of the view port range.
|
||||
* If scalableRange is number or a string without 'px', means it is a ratio of the graph content.
|
||||
* If scalableRange is a string with 'px', it is regarded as pixels.
|
||||
* If scalableRange = 0, no constrains;
|
||||
* If scalableRange > 0, the graph can be dragged out of the view port range
|
||||
* If scalableRange < 0, the range is smaller than the view port.
|
||||
* If 0 < abs(scalableRange) < 1, it is regarded as a ratio of view port size.
|
||||
* If abs(scalableRange) > 1, it is regarded as pixels.
|
||||
* Refer to https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*IFfoS67_HssAAAAAAAAAAAAAARQnAQ
|
||||
*/
|
||||
scalableRange?: number;
|
||||
scalableRange?: string | number;
|
||||
/**
|
||||
* The event name to trigger when drag end.
|
||||
*/
|
||||
@ -48,11 +53,12 @@ export interface DragCanvasOptions {
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<DragCanvasOptions> = {
|
||||
enableOptimize: true,
|
||||
enableOptimize: false,
|
||||
dragOnItems: false,
|
||||
trigger: 'drag',
|
||||
direction: 'both',
|
||||
secondaryKey: '',
|
||||
secondaryKeyToDisable: 'shift',
|
||||
speedUpKey: '',
|
||||
scalableRange: 0,
|
||||
eventName: '',
|
||||
@ -66,6 +72,7 @@ export default class DragCanvas extends Behavior {
|
||||
private dragging: boolean; // pointerdown + pointermove a distance
|
||||
private keydown: boolean;
|
||||
private speedupKeydown: boolean;
|
||||
private disableKeydown: boolean;
|
||||
private hiddenEdgeIds: ID[];
|
||||
private hiddenNodeIds: ID[];
|
||||
|
||||
@ -81,6 +88,10 @@ export default class DragCanvas extends Behavior {
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('keydown', this.onKeydown.bind(this));
|
||||
window.addEventListener('keyup', this.onKeyup.bind(this));
|
||||
}
|
||||
if (this.options.trigger === 'directionKeys') {
|
||||
return {
|
||||
keydown: this.onKeydown,
|
||||
@ -97,7 +108,10 @@ export default class DragCanvas extends Behavior {
|
||||
}
|
||||
|
||||
public onPointerDown(event) {
|
||||
const { secondaryKey, dragOnItems, shouldBegin } = this.options;
|
||||
const { secondaryKey, secondaryKeyToDisable, dragOnItems, shouldBegin } =
|
||||
this.options;
|
||||
// disabled key is pressing
|
||||
if (secondaryKeyToDisable && this.disableKeydown) return;
|
||||
// assistant key is not pressing
|
||||
if (secondaryKey && !this.keydown) return;
|
||||
// should not begin
|
||||
@ -145,12 +159,25 @@ export default class DragCanvas extends Behavior {
|
||||
const { scalableRange, direction } = this.options;
|
||||
const [width, height] = graph.getSize();
|
||||
const graphBBox = graph.canvas.getRoot().getRenderBounds();
|
||||
let expandWidth = scalableRange;
|
||||
let expandHeight = scalableRange;
|
||||
// 若 scalableRange 是 0~1 的小数,则作为比例考虑
|
||||
if (expandWidth < 1 && expandWidth > -1) {
|
||||
expandWidth = width * expandWidth;
|
||||
expandHeight = height * expandHeight;
|
||||
let rangeNum = Number(scalableRange);
|
||||
let isPixel;
|
||||
if (typeof scalableRange === 'string') {
|
||||
if (scalableRange.includes('px')) {
|
||||
isPixel = scalableRange.includes('px');
|
||||
rangeNum = Number(scalableRange.replace('px', ''));
|
||||
}
|
||||
if (scalableRange.includes('%')) {
|
||||
rangeNum = Number(scalableRange.replace('%', '')) / 100;
|
||||
}
|
||||
}
|
||||
if (rangeNum === 0) return { dx: diffX, dy: diffY };
|
||||
|
||||
let expandWidth = rangeNum;
|
||||
let expandHeight = rangeNum;
|
||||
// If it is not a string with 'px', regard as ratio
|
||||
if (!isPixel) {
|
||||
expandWidth = width * rangeNum;
|
||||
expandHeight = height * rangeNum;
|
||||
}
|
||||
const leftTopClient = graph.getViewportByCanvas({
|
||||
x: graphBBox.min[0],
|
||||
@ -182,9 +209,11 @@ export default class DragCanvas extends Behavior {
|
||||
|
||||
public onPointerMove(event) {
|
||||
if (!this.pointerDownAt) return;
|
||||
const { eventName, direction, secondaryKeyToDisable } = this.options;
|
||||
// disabled key is pressing
|
||||
if (secondaryKeyToDisable && this.disableKeydown) return;
|
||||
const { graph } = this;
|
||||
const { client } = event;
|
||||
const { eventName, direction } = this.options;
|
||||
const diffX = client.x - this.pointerDownAt.x;
|
||||
const diffY = client.y - this.pointerDownAt.y;
|
||||
if (direction === 'x' && !diffX) return;
|
||||
@ -198,7 +227,7 @@ export default class DragCanvas extends Behavior {
|
||||
}
|
||||
|
||||
const { dx, dy } = this.formatDisplacement(diffX, diffY);
|
||||
graph.translate(dx, dy);
|
||||
graph.translate({ dx, dy });
|
||||
|
||||
this.pointerDownAt = { x: client.x, y: client.y };
|
||||
|
||||
@ -228,16 +257,26 @@ export default class DragCanvas extends Behavior {
|
||||
|
||||
public onKeydown(event) {
|
||||
const { key } = event;
|
||||
const { secondaryKey, trigger, speedUpKey, eventName, shouldBegin } =
|
||||
this.options;
|
||||
const {
|
||||
secondaryKey,
|
||||
secondaryKeyToDisable,
|
||||
trigger,
|
||||
speedUpKey,
|
||||
eventName,
|
||||
shouldBegin,
|
||||
} = this.options;
|
||||
if (secondaryKey && secondaryKey === key.toLowerCase()) {
|
||||
this.keydown = true;
|
||||
}
|
||||
if (speedUpKey && speedUpKey === key.toLowerCase()) {
|
||||
this.speedupKeydown = true;
|
||||
}
|
||||
if (secondaryKeyToDisable && secondaryKeyToDisable === key.toLowerCase()) {
|
||||
this.disableKeydown = true;
|
||||
}
|
||||
if (trigger === 'directionKeys') {
|
||||
if (secondaryKey && !this.keydown) return;
|
||||
if (secondaryKeyToDisable && this.disableKeydown) return;
|
||||
if (!shouldBegin(event)) return;
|
||||
const { graph, speedupKeydown } = this;
|
||||
const speed = speedupKeydown ? 20 : 1;
|
||||
@ -262,7 +301,7 @@ export default class DragCanvas extends Behavior {
|
||||
dx,
|
||||
dy,
|
||||
);
|
||||
graph.translate(formattedDx, formattedDy);
|
||||
graph.translate({ dx: formattedDx, dy: formattedDy });
|
||||
if (eventName) {
|
||||
this.graph.emit(eventName, {
|
||||
translate: { dx: formattedDx, dy: formattedDy },
|
||||
@ -274,12 +313,21 @@ export default class DragCanvas extends Behavior {
|
||||
|
||||
public onKeyup(event) {
|
||||
const { key } = event;
|
||||
const { secondaryKey, speedUpKey } = this.options;
|
||||
const { secondaryKey, secondaryKeyToDisable, speedUpKey } = this.options;
|
||||
if (secondaryKey && secondaryKey === key.toLowerCase()) {
|
||||
this.keydown = false;
|
||||
}
|
||||
if (speedUpKey && speedUpKey === key.toLowerCase()) {
|
||||
this.speedupKeydown = false;
|
||||
}
|
||||
if (secondaryKeyToDisable && secondaryKeyToDisable === key.toLowerCase()) {
|
||||
this.disableKeydown = false;
|
||||
}
|
||||
}
|
||||
public destroy() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('keydown', this.onKeydown.bind(this));
|
||||
window.removeEventListener('keyup', this.onKeyup.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { throttle, uniq } from '@antv/util';
|
||||
import { EdgeModel } from '../../types';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { IG6GraphEvent } from '../../types/event';
|
||||
import { Point } from '../../types/common';
|
||||
|
||||
const DELEGATE_SHAPE_ID = 'g6-drag-node-delegate-shape';
|
||||
|
||||
@ -17,12 +18,12 @@ export interface DragNodeOptions {
|
||||
* Ignored when enableDelegate is true.
|
||||
* Defaults to true.
|
||||
*/
|
||||
enableTransient?: boolean;
|
||||
enableTransient?: Boolean;
|
||||
/**
|
||||
* Whether to use a virtual rect moved with the dragging mouse instead of the node.
|
||||
* Defaults to false.
|
||||
*/
|
||||
enableDelegate?: boolean;
|
||||
enableDelegate?: Boolean;
|
||||
/**
|
||||
* The drawing properties when the nodes are dragged.
|
||||
* Only used when enableDelegate is true.
|
||||
@ -46,7 +47,7 @@ export interface DragNodeOptions {
|
||||
* Ignored when enableTransient or enableDelegate is true.
|
||||
* Defaults to false.
|
||||
*/
|
||||
hideRelatedEdges?: boolean;
|
||||
hideRelatedEdges?: Boolean;
|
||||
/**
|
||||
* The state name to be considered as "selected".
|
||||
* Defaults to "selected".
|
||||
@ -79,7 +80,7 @@ const DEFAULT_OPTIONS: Required<DragNodeOptions> = {
|
||||
shouldBegin: () => true,
|
||||
};
|
||||
|
||||
export class DragNode extends Behavior {
|
||||
export default class DragNode extends Behavior {
|
||||
options: DragNodeOptions;
|
||||
|
||||
// Private states
|
||||
@ -96,6 +97,8 @@ export class DragNode extends Behavior {
|
||||
minY?: number;
|
||||
maxY?: number;
|
||||
}> = [];
|
||||
private pointerDown: Point | undefined = undefined;
|
||||
private dragging: Boolean = false;
|
||||
|
||||
constructor(options: Partial<DragNodeOptions>) {
|
||||
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options);
|
||||
@ -131,85 +134,102 @@ export class DragNode extends Behavior {
|
||||
|
||||
public onPointerDown(event: IG6GraphEvent) {
|
||||
if (!this.options.shouldBegin(event)) return;
|
||||
const currentNodeId = event.itemId;
|
||||
let selectedNodeIds = this.graph.findIdByState(
|
||||
'node',
|
||||
this.options.selectedState,
|
||||
true,
|
||||
);
|
||||
|
||||
// If current node is selected, drag all the selected nodes together.
|
||||
// Otherwise drag current node.
|
||||
if (!selectedNodeIds.includes(currentNodeId)) {
|
||||
selectedNodeIds = [currentNodeId];
|
||||
}
|
||||
|
||||
this.originPositions = selectedNodeIds.map((id) => {
|
||||
const { x, y } = this.graph.getNodeData(id).data as {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
// If delegate is enabled, record bbox together.
|
||||
if (this.options.enableDelegate) {
|
||||
const bbox = this.graph.getRenderBBox(id);
|
||||
if (bbox) {
|
||||
const [minX, minY] = bbox.min;
|
||||
const [maxX, maxY] = bbox.max;
|
||||
return { id, x, y, minX, minY, maxX, maxY };
|
||||
}
|
||||
}
|
||||
return { id, x, y };
|
||||
});
|
||||
|
||||
const enableTransient =
|
||||
this.options.enableTransient && this.graph.rendererType !== 'webgl-3d';
|
||||
|
||||
// Hide related edge.
|
||||
if (this.options.hideRelatedEdges && !enableTransient) {
|
||||
this.hiddenEdges = this.getRelatedEdges(selectedNodeIds);
|
||||
this.graph.hideItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw transient nodes and edges.
|
||||
if (enableTransient) {
|
||||
// Draw transient edges and nodes.
|
||||
this.hiddenEdges = this.getRelatedEdges(selectedNodeIds);
|
||||
this.hiddenEdges.forEach((edge) => {
|
||||
this.graph.drawTransient('edge', edge.id, {});
|
||||
});
|
||||
selectedNodeIds.forEach((nodeId) => {
|
||||
this.graph.drawTransient('node', nodeId, {});
|
||||
});
|
||||
|
||||
// Hide original edges and nodes. They will be restored when pointerup.
|
||||
this.graph.hideItem(selectedNodeIds, true);
|
||||
this.graph.hideItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Throttle moving.
|
||||
if (this.options.throttle > 0) {
|
||||
this.throttledMoveNodes = throttle(
|
||||
this.moveNodes,
|
||||
this.options.throttle,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
} else {
|
||||
this.throttledMoveNodes = this.moveNodes;
|
||||
}
|
||||
|
||||
// @ts-ignore FIXME: Type
|
||||
this.originX = event.client.x;
|
||||
// @ts-ignore FIXME: Type
|
||||
this.originY = event.client.y;
|
||||
this.pointerDown = { x: event.canvas.x, y: event.canvas.y };
|
||||
this.dragging = false;
|
||||
}
|
||||
|
||||
public onPointerMove(event: IG6GraphEvent) {
|
||||
if (!this.pointerDown) return;
|
||||
|
||||
const beginDeltaX = Math.abs(this.pointerDown.x - event.canvas.x);
|
||||
const beginDeltaY = Math.abs(this.pointerDown.y - event.canvas.y);
|
||||
if (beginDeltaX < 1 && beginDeltaY < 1) return;
|
||||
|
||||
// pointerDown + first move = dragging
|
||||
if (!this.dragging) {
|
||||
this.dragging = true;
|
||||
const currentNodeId = event.itemId;
|
||||
let selectedNodeIds = this.graph.findIdByState(
|
||||
'node',
|
||||
this.options.selectedState,
|
||||
true,
|
||||
);
|
||||
|
||||
// If current node is selected, drag all the selected nodes together.
|
||||
// Otherwise drag current node.
|
||||
if (currentNodeId && !selectedNodeIds.includes(currentNodeId)) {
|
||||
selectedNodeIds = [currentNodeId];
|
||||
}
|
||||
|
||||
this.originPositions = selectedNodeIds.map((id) => {
|
||||
if (!this.graph.getNodeData(id)) {
|
||||
console.log('node does not exist', id);
|
||||
return;
|
||||
}
|
||||
const { x, y } = this.graph.getNodeData(id).data as {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
// If delegate is enabled, record bbox together.
|
||||
if (this.options.enableDelegate) {
|
||||
const bbox = this.graph.getRenderBBox(id);
|
||||
if (bbox) {
|
||||
const [minX, minY] = bbox.min;
|
||||
const [maxX, maxY] = bbox.max;
|
||||
return { id, x, y, minX, minY, maxX, maxY };
|
||||
}
|
||||
}
|
||||
return { id, x, y };
|
||||
});
|
||||
|
||||
const enableTransient =
|
||||
this.options.enableTransient && this.graph.rendererType !== 'webgl-3d';
|
||||
|
||||
// Hide related edge.
|
||||
if (this.options.hideRelatedEdges && !enableTransient) {
|
||||
this.hiddenEdges = this.getRelatedEdges(selectedNodeIds);
|
||||
this.graph.hideItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw transient nodes and edges.
|
||||
if (enableTransient) {
|
||||
// Draw transient edges and nodes.
|
||||
this.hiddenEdges = this.getRelatedEdges(selectedNodeIds);
|
||||
this.hiddenEdges.forEach((edge) => {
|
||||
this.graph.drawTransient('edge', edge.id, {});
|
||||
});
|
||||
selectedNodeIds.forEach((nodeId) => {
|
||||
this.graph.drawTransient('node', nodeId, {});
|
||||
});
|
||||
|
||||
// Hide original edges and nodes. They will be restored when pointerup.
|
||||
this.graph.hideItem(selectedNodeIds, true);
|
||||
this.graph.hideItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Throttle moving.
|
||||
if (this.options.throttle > 0) {
|
||||
this.throttledMoveNodes = throttle(
|
||||
this.moveNodes,
|
||||
this.options.throttle,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
} else {
|
||||
this.throttledMoveNodes = this.moveNodes;
|
||||
}
|
||||
|
||||
// @ts-ignore FIXME: Type
|
||||
this.originX = event.canvas.x;
|
||||
// @ts-ignore FIXME: Type
|
||||
this.originY = event.canvas.y;
|
||||
}
|
||||
|
||||
if (!this.originPositions.length) {
|
||||
return;
|
||||
}
|
||||
@ -217,9 +237,9 @@ export class DragNode extends Behavior {
|
||||
// @ts-ignore FIXME: type
|
||||
const pointerEvent = event as PointerEvent;
|
||||
// @ts-ignore FIXME: Type
|
||||
const deltaX = pointerEvent.client.x - this.originX;
|
||||
const deltaX = pointerEvent.canvas.x - this.originX;
|
||||
// @ts-ignore FIXME: Type
|
||||
const deltaY = pointerEvent.client.y - this.originY;
|
||||
const deltaY = pointerEvent.canvas.y - this.originY;
|
||||
|
||||
if (this.options.enableDelegate) {
|
||||
this.moveDelegate(deltaX, deltaY);
|
||||
@ -230,7 +250,7 @@ export class DragNode extends Behavior {
|
||||
}
|
||||
}
|
||||
|
||||
public moveNodes(deltaX: number, deltaY: number, transient: boolean) {
|
||||
public moveNodes(deltaX: number, deltaY: number, transient: Boolean) {
|
||||
const positionChanges = this.originPositions.map(({ id, x, y }) => {
|
||||
return {
|
||||
id,
|
||||
@ -262,7 +282,7 @@ export class DragNode extends Behavior {
|
||||
public throttledMoveNodes: Function = (
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
transient: boolean,
|
||||
transient: Boolean,
|
||||
) => {
|
||||
// Should be overrided when drag start.
|
||||
};
|
||||
@ -325,6 +345,8 @@ export class DragNode extends Behavior {
|
||||
}
|
||||
|
||||
public onPointerUp(event: IG6GraphEvent) {
|
||||
this.pointerDown = undefined;
|
||||
this.dragging = false;
|
||||
const enableTransient =
|
||||
this.options.enableTransient && this.graph.rendererType !== 'webgl-3d';
|
||||
// If transient or delegate was enabled, move the real nodes.
|
||||
@ -332,9 +354,9 @@ export class DragNode extends Behavior {
|
||||
// @ts-ignore FIXME: type
|
||||
const pointerEvent = event as PointerEvent;
|
||||
// @ts-ignore FIXME: Type
|
||||
const deltaX = pointerEvent.client.x - this.originX;
|
||||
const deltaX = pointerEvent.canvas.x - this.originX;
|
||||
// @ts-ignore FIXME: Type
|
||||
const deltaY = pointerEvent.client.y - this.originY;
|
||||
const deltaY = pointerEvent.canvas.y - this.originY;
|
||||
this.moveNodes(deltaX, deltaY, false);
|
||||
}
|
||||
|
||||
|
104
packages/g6/src/stdlib/behavior/hover-activate.ts
Normal file
104
packages/g6/src/stdlib/behavior/hover-activate.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { IG6GraphEvent } from '../../types/event';
|
||||
import { ITEM_TYPE } from 'types/item';
|
||||
|
||||
// TODO: Combo related features:
|
||||
// hover combo
|
||||
|
||||
export interface HoverActivateOptions {
|
||||
/**
|
||||
* The time in milliseconds to throttle moving. Useful to avoid the frequent calculation.
|
||||
* Defaults to 0.
|
||||
*/
|
||||
throttle?: number;
|
||||
/**
|
||||
* The state name to be considered as "selected".
|
||||
* Defaults to "selected".
|
||||
*/
|
||||
activateState?: string;
|
||||
/**
|
||||
* Item types to be able to acitvate.
|
||||
* Defaults to `["node", "edge"]`.
|
||||
* Should be an array of "node", "edge", or "combo".
|
||||
*/
|
||||
itemTypes: Array<'node' | 'edge' | 'combo'>;
|
||||
/**
|
||||
* The event name to trigger when drag end.
|
||||
*/
|
||||
eventName?: string;
|
||||
/**
|
||||
* Whether allow the behavior happen on the current item.
|
||||
*/
|
||||
shouldBegin?: (event: IG6GraphEvent) => boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<HoverActivateOptions> = {
|
||||
throttle: 16,
|
||||
activateState: 'active',
|
||||
eventName: '',
|
||||
itemTypes: ['node', 'edge', 'combo'],
|
||||
shouldBegin: () => true,
|
||||
};
|
||||
|
||||
export class HoverActivate extends Behavior {
|
||||
options: HoverActivateOptions;
|
||||
private currentItemInfo: { id: ID; itemType: ITEM_TYPE };
|
||||
|
||||
constructor(options: Partial<HoverActivateOptions>) {
|
||||
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options);
|
||||
super(finalOptions);
|
||||
}
|
||||
|
||||
getEvents = () => {
|
||||
return {
|
||||
'node:pointerenter': this.onPointerEnter,
|
||||
'node:pointerleave': this.onPointerLeave,
|
||||
'edge:pointerenter': this.onPointerEnter,
|
||||
'edge:pointerleave': this.onPointerLeave,
|
||||
};
|
||||
};
|
||||
|
||||
public onPointerEnter(event: IG6GraphEvent) {
|
||||
const { itemId, itemType } = event;
|
||||
const { graph, currentItemInfo } = this;
|
||||
const { activateState, itemTypes, eventName } = this.options;
|
||||
|
||||
if (currentItemInfo && itemTypes.includes(currentItemInfo.itemType)) {
|
||||
graph.setItemState(currentItemInfo.id, activateState, false);
|
||||
}
|
||||
|
||||
if (!itemTypes.includes(itemType as ITEM_TYPE)) return;
|
||||
graph.setItemState(itemId, activateState, true);
|
||||
this.currentItemInfo = { id: itemId, itemType: itemType as ITEM_TYPE };
|
||||
|
||||
// Emit event.
|
||||
if (eventName) {
|
||||
this.graph.emit(eventName, {
|
||||
itemId,
|
||||
itemType,
|
||||
state: activateState,
|
||||
action: 'pointerenter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onPointerLeave(event: IG6GraphEvent) {
|
||||
const { itemId, itemType } = event;
|
||||
const { activateState, itemTypes, eventName } = this.options;
|
||||
if (!itemTypes.includes(itemType as ITEM_TYPE)) return;
|
||||
const { graph } = this;
|
||||
graph.setItemState(itemId, activateState, false);
|
||||
this.currentItemInfo = undefined;
|
||||
|
||||
// Emit event.
|
||||
if (eventName) {
|
||||
this.graph.emit(eventName, {
|
||||
itemId,
|
||||
itemType,
|
||||
state: activateState,
|
||||
action: 'pointerleave',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ export default class LassoSelect extends BrushSelect {
|
||||
return this.points;
|
||||
}
|
||||
|
||||
publiccreateBrush() {
|
||||
public createBrush() {
|
||||
const { graph, options } = this;
|
||||
const { brushStyle } = options;
|
||||
return graph.drawTransient('path', LASSO_SHAPE_ID, {
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { ICamera } from '@antv/g';
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { debounce, uniq } from '@antv/util';
|
||||
import { EdgeModel } from '../../types';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { IG6GraphEvent } from '../../types/event';
|
||||
import { Point } from '../../types/common';
|
||||
|
@ -31,6 +31,14 @@ export interface ZoomCanvas3DOptions {
|
||||
* The event name to trigger when drag end.
|
||||
*/
|
||||
eventName?: string;
|
||||
/**
|
||||
* The min value of camera's dolly to constrain the zoom-canvas-3d behavior
|
||||
*/
|
||||
minZoom?: number;
|
||||
/**
|
||||
* The max value of camera's dolly to constrain the zoom-canvas-3d behavior
|
||||
*/
|
||||
maxZoom?: number;
|
||||
/**
|
||||
* Whether allow the behavior happen on the current item.
|
||||
*/
|
||||
@ -41,8 +49,10 @@ const DEFAULT_OPTIONS: Required<ZoomCanvas3DOptions> = {
|
||||
trigger: 'wheel',
|
||||
secondaryKey: '',
|
||||
eventName: '',
|
||||
sensitivity: 1,
|
||||
triggerOnItems: false,
|
||||
sensitivity: 10,
|
||||
triggerOnItems: true,
|
||||
minZoom: 0.01,
|
||||
maxZoom: 10,
|
||||
shouldBegin: () => true,
|
||||
};
|
||||
|
||||
@ -70,6 +80,17 @@ export default class ZoomCanvas3D extends Behavior {
|
||||
}
|
||||
|
||||
getEvents = () => {
|
||||
this.graph.canvas
|
||||
.getContextService()
|
||||
.getDomElement()
|
||||
.addEventListener(
|
||||
'wheel',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
if (this.options.trigger === 'wheel') {
|
||||
return {
|
||||
wheel: this.onWheel,
|
||||
@ -89,6 +110,8 @@ export default class ZoomCanvas3D extends Behavior {
|
||||
secondaryKey,
|
||||
triggerOnItems,
|
||||
eventName,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
sensitivity = 1,
|
||||
shouldBegin,
|
||||
} = options;
|
||||
@ -96,10 +119,28 @@ export default class ZoomCanvas3D extends Behavior {
|
||||
if (!shouldBegin(event)) return;
|
||||
if (secondaryKey && !this.keydown) return;
|
||||
const camera = graph.canvas.getCamera();
|
||||
const sign = event.deltaY > 0 ? 1 : -1;
|
||||
const currentDistance = camera.getDistance();
|
||||
const dolly =
|
||||
((100 * sign * sensitivity) / currentDistance) *
|
||||
Math.sqrt(currentDistance);
|
||||
const toDistance = currentDistance + dolly;
|
||||
const cameraFrontOfFocalPoint = camera.getDistanceVector()[2] < 0;
|
||||
|
||||
console.log(event.deltaY);
|
||||
// zoom out constraint
|
||||
if (
|
||||
dolly > 0 &&
|
||||
cameraFrontOfFocalPoint &&
|
||||
toDistance > (1 / minZoom) * 200
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// zoom in constraint
|
||||
if (dolly < 0 && !cameraFrontOfFocalPoint && toDistance > maxZoom * 200) {
|
||||
return;
|
||||
}
|
||||
|
||||
camera.dolly(event.deltaY * sensitivity);
|
||||
camera.dolly(dolly);
|
||||
|
||||
// Emit event.
|
||||
if (eventName) {
|
||||
|
@ -11,7 +11,7 @@ export interface ZoomCanvasOptions {
|
||||
/**
|
||||
* Whether allow trigger this behavior when wheeling start on nodes / edges / combos.
|
||||
*/
|
||||
zoomOnItems?: boolean;
|
||||
triggerOnItems?: boolean;
|
||||
/**
|
||||
* The trigger for the behavior, 'wheel' by default. 'upDownKeys' means trigger this behavior by up / down keys on keyboard.
|
||||
*/
|
||||
@ -32,6 +32,14 @@ export interface ZoomCanvasOptions {
|
||||
* The event name to trigger when zoom end.
|
||||
*/
|
||||
eventName?: string;
|
||||
/**
|
||||
* The min value of zoom ratio to constrain the zoom-canvas-3d behavior
|
||||
*/
|
||||
minZoom?: number;
|
||||
/**
|
||||
* The max value of zoom ratio to constrain the zoom-canvas-3d behavior
|
||||
*/
|
||||
maxZoom?: number;
|
||||
/**
|
||||
* Whether allow the behavior happen on the current item.
|
||||
*/
|
||||
@ -49,12 +57,14 @@ export interface ZoomCanvasOptions {
|
||||
|
||||
const DEFAULT_OPTIONS: Required<ZoomCanvasOptions> = {
|
||||
enableOptimize: false,
|
||||
zoomOnItems: false,
|
||||
sensitivity: 1,
|
||||
triggerOnItems: true,
|
||||
sensitivity: 2,
|
||||
trigger: 'wheel',
|
||||
secondaryKey: '',
|
||||
speedUpKey: 'shift',
|
||||
eventName: '',
|
||||
minZoom: 0.00001,
|
||||
maxZoom: 1000,
|
||||
shouldBegin: () => true,
|
||||
};
|
||||
|
||||
@ -80,6 +90,17 @@ export default class ZoomCanvas extends Behavior {
|
||||
}
|
||||
|
||||
getEvents() {
|
||||
this.graph.canvas
|
||||
.getContextService()
|
||||
.getDomElement()
|
||||
.addEventListener(
|
||||
'wheel',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
||||
if (this.options.trigger === 'upDownKeys') {
|
||||
return {
|
||||
keydown: this.onKeydown,
|
||||
@ -137,15 +158,22 @@ export default class ZoomCanvas extends Behavior {
|
||||
|
||||
public onWheel(event) {
|
||||
const { graph, keydown } = this;
|
||||
const { deltaY, canvas, itemId } = event;
|
||||
const { eventName, sensitivity, secondaryKey, zoomOnItems, shouldBegin } =
|
||||
this.options;
|
||||
const { deltaY, client, itemId } = event;
|
||||
const {
|
||||
eventName,
|
||||
sensitivity,
|
||||
secondaryKey,
|
||||
triggerOnItems,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
shouldBegin,
|
||||
} = this.options;
|
||||
|
||||
// TODO: CANVAS
|
||||
const isOnItem = itemId && itemId !== 'CANVAS';
|
||||
if (
|
||||
(secondaryKey && !keydown) ||
|
||||
(isOnItem && !zoomOnItems) ||
|
||||
(isOnItem && !triggerOnItems) ||
|
||||
!shouldBegin(event)
|
||||
) {
|
||||
this.endZoom();
|
||||
@ -160,8 +188,11 @@ export default class ZoomCanvas extends Behavior {
|
||||
let zoomRatio = 1;
|
||||
if (deltaY < 0) zoomRatio = (100 + sensitivity) / 100;
|
||||
if (deltaY > 0) zoomRatio = 100 / (100 + sensitivity);
|
||||
const zoomTo = zoomRatio * graph.getZoom();
|
||||
if (minZoom && zoomTo < minZoom) return;
|
||||
if (maxZoom && zoomTo > maxZoom) return;
|
||||
// TODO: the zoom center is wrong?
|
||||
graph.zoom(zoomRatio, { x: canvas.x, y: canvas.y });
|
||||
graph.zoom(zoomRatio, { x: client.x, y: client.y });
|
||||
|
||||
clearTimeout(this.zoomTimer);
|
||||
this.zoomTimer = setTimeout(() => {
|
||||
|
@ -5,7 +5,7 @@ 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 DragNode from './behavior/drag-node';
|
||||
import { comboFromNode } from './data/comboFromNode';
|
||||
import { LineEdge } from './item/edge';
|
||||
import { CircleNode, SphereNode } from './item/node';
|
||||
@ -23,6 +23,7 @@ import ZoomCanvas3D from './behavior/zoom-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';
|
||||
|
||||
const stdLib = {
|
||||
transforms: {
|
||||
@ -40,6 +41,7 @@ const stdLib = {
|
||||
behaviors: {
|
||||
'activate-relations': ActivateRelations,
|
||||
'drag-canvas': DragCanvas,
|
||||
'hover-activate': HoverActivate,
|
||||
'zoom-canvas': ZoomCanvas,
|
||||
'drag-node': DragNode,
|
||||
'click-select': ClickSelect,
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
SHAPE_TYPE,
|
||||
ShapeStyle,
|
||||
State,
|
||||
ZoomStrategyObj,
|
||||
lodStrategyObj,
|
||||
} from '../../../types/item';
|
||||
import {
|
||||
LOCAL_BOUNDS_DIRTY_FLAG_KEY,
|
||||
@ -37,7 +37,7 @@ export abstract class BaseEdge {
|
||||
mergedStyles: EdgeShapeStyles;
|
||||
sourcePoint: Point;
|
||||
targetPoint: Point;
|
||||
zoomStrategy?: ZoomStrategyObj;
|
||||
lodStrategy?: lodStrategyObj;
|
||||
labelPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -54,8 +54,6 @@ export abstract class BaseEdge {
|
||||
private zoomCache: {
|
||||
// the id of shapes which are hidden by zoom changing.
|
||||
hiddenShape: { [shapeId: string]: boolean };
|
||||
// timeout timer for scaling shapes with balanceRatio, simulates debounce in function.
|
||||
balanceTimer: NodeJS.Timeout;
|
||||
// the ratio to scale the size of shapes whose visual size should be kept, e.g. label and badges.
|
||||
balanceRatio: number;
|
||||
// last responsed zoom ratio.
|
||||
@ -70,24 +68,28 @@ export abstract class BaseEdge {
|
||||
wordWrapWidth: number;
|
||||
// animate configurations for zoom level changing
|
||||
animateConfig: AnimateCfg;
|
||||
// the tag of first rendering
|
||||
firstRender: boolean;
|
||||
} = {
|
||||
hiddenShape: {},
|
||||
balanceRatio: 1,
|
||||
zoom: 1,
|
||||
zoomLevel: 0,
|
||||
balanceTimer: undefined,
|
||||
levelShapes: {},
|
||||
wordWrapWidth: 50,
|
||||
animateConfig: DEFAULT_ANIMATE_CFG.zoom,
|
||||
firstRender: true,
|
||||
};
|
||||
constructor(props) {
|
||||
const { themeStyles, zoomStrategy } = props;
|
||||
const { themeStyles, lodStrategy, zoom } = props;
|
||||
if (themeStyles) this.themeStyles = themeStyles;
|
||||
this.zoomStrategy = zoomStrategy;
|
||||
this.lodStrategy = lodStrategy;
|
||||
this.boundsCache = {};
|
||||
this.zoomCache.zoom = zoom;
|
||||
this.zoomCache.balanceRatio = 1 / zoom;
|
||||
this.zoomCache.animateConfig = {
|
||||
...DEFAULT_ANIMATE_CFG.zoom,
|
||||
...zoomStrategy?.animateCfg,
|
||||
...lodStrategy?.animateCfg,
|
||||
};
|
||||
}
|
||||
public mergeStyles(model: EdgeDisplayModel) {
|
||||
@ -137,10 +139,10 @@ export abstract class BaseEdge {
|
||||
|
||||
const { levelShapes, zoom } = this.zoomCache;
|
||||
Object.keys(shapeMap).forEach((shapeId) => {
|
||||
const { showLevel } = shapeMap[shapeId].attributes;
|
||||
if (showLevel !== undefined) {
|
||||
levelShapes[showLevel] = levelShapes[showLevel] || [];
|
||||
levelShapes[showLevel].push(shapeId);
|
||||
const { lod } = shapeMap[shapeId].attributes;
|
||||
if (lod !== undefined) {
|
||||
levelShapes[lod] = levelShapes[lod] || [];
|
||||
levelShapes[lod].push(shapeId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -436,12 +438,14 @@ export abstract class BaseEdge {
|
||||
): DisplayObject {
|
||||
const { keyShape } = shapeMap;
|
||||
const { haloShape: haloShapeStyle } = this.mergedStyles;
|
||||
if (haloShapeStyle.visible === false) return;
|
||||
const { nodeName, attributes } = keyShape;
|
||||
return this.upsertShape(
|
||||
nodeName as SHAPE_TYPE,
|
||||
'haloShape',
|
||||
{
|
||||
...attributes,
|
||||
stroke: attributes.stroke,
|
||||
...haloShapeStyle,
|
||||
},
|
||||
shapeMap,
|
||||
@ -461,38 +465,53 @@ export abstract class BaseEdge {
|
||||
this.balanceShapeSize(shapeMap, zoom);
|
||||
|
||||
// zoomLevel changed
|
||||
if (!this.zoomStrategy) return;
|
||||
const { levels } = this.zoomStrategy;
|
||||
if (!this.lodStrategy) return;
|
||||
const { levels } = this.lodStrategy;
|
||||
const {
|
||||
levelShapes,
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
firstRender = true,
|
||||
zoomLevel: previousLevel,
|
||||
} = this.zoomCache;
|
||||
|
||||
// last zoom ratio responsed by zoom changing, which might not equal to zoom.previous in props since the function is debounced.
|
||||
const currentLevel = getZoomLevel(levels, zoom);
|
||||
const levelNums = Object.keys(levelShapes).map(Number);
|
||||
const maxLevel = Math.max(...levelNums);
|
||||
const minLevel = Math.min(...levelNums);
|
||||
if (currentLevel < previousLevel) {
|
||||
// zoomLevel changed, from higher to lower, hide something
|
||||
levelShapes[currentLevel + 1]?.forEach((id) =>
|
||||
fadeOut(id, shapeMap[id], hiddenShape, animateConfig),
|
||||
);
|
||||
if (firstRender) {
|
||||
for (let i = currentLevel + 1; i <= maxLevel; i++) {
|
||||
levelShapes[String(i)]?.forEach((id) => shapeMap[id]?.hide());
|
||||
}
|
||||
} else {
|
||||
for (let i = currentLevel + 1; i <= maxLevel; i++) {
|
||||
levelShapes[String(i)]?.forEach((id) =>
|
||||
fadeOut(id, shapeMap[id], hiddenShape, animateConfig),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (currentLevel > previousLevel) {
|
||||
// zoomLevel changed, from lower to higher, show something
|
||||
levelShapes[String(currentLevel)]?.forEach((id) =>
|
||||
fadeIn(
|
||||
id,
|
||||
shapeMap[id],
|
||||
this.mergedStyles[id] ||
|
||||
this.mergedStyles[id.replace('Background', '')],
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
),
|
||||
);
|
||||
for (let i = currentLevel; i >= minLevel; i--) {
|
||||
levelShapes[String(i)]?.forEach((id) =>
|
||||
fadeIn(
|
||||
id,
|
||||
shapeMap[id],
|
||||
this.mergedStyles[id] ||
|
||||
this.mergedStyles[id.replace('Background', '')],
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.zoomCache.zoom = zoom;
|
||||
this.zoomCache.zoomLevel = currentLevel;
|
||||
this.zoomCache.firstRender = false;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -521,7 +540,7 @@ export abstract class BaseEdge {
|
||||
if (!labelBackgroundShape) return;
|
||||
|
||||
const oriBgTransform = this.boundsCache.labelBackgroundShapeTransform;
|
||||
labelBackgroundShape.style.transform = `${oriBgTransform} scale(1, ${balanceRatio})`;
|
||||
labelBackgroundShape.style.transform = `${oriBgTransform} scale(${balanceRatio}, ${balanceRatio})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
SHAPE_TYPE_3D,
|
||||
ShapeStyle,
|
||||
State,
|
||||
ZoomStrategyObj,
|
||||
lodStrategyObj,
|
||||
} from '../../../types/item';
|
||||
import {
|
||||
NodeModelData,
|
||||
@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
LOCAL_BOUNDS_DIRTY_FLAG_KEY,
|
||||
formatPadding,
|
||||
getShapeLocalBoundsByStyle,
|
||||
mergeStyles,
|
||||
upsertShape,
|
||||
} from '../../../util/shape';
|
||||
@ -31,17 +32,15 @@ export abstract class BaseNode {
|
||||
defaultStyles: NodeShapeStyles;
|
||||
themeStyles: NodeShapeStyles;
|
||||
mergedStyles: NodeShapeStyles;
|
||||
zoomStrategy?: ZoomStrategyObj;
|
||||
lodStrategy?: lodStrategyObj;
|
||||
boundsCache: {
|
||||
keyShapeLocal?: AABB;
|
||||
labelShapeLocal?: AABB;
|
||||
};
|
||||
// cache the zoom level infomations
|
||||
private zoomCache: {
|
||||
protected zoomCache: {
|
||||
// the id of shapes which are hidden by zoom changing.
|
||||
hiddenShape: { [shapeId: string]: boolean };
|
||||
// timeout timer for scaling shapes with to keep the visual size, simulates debounce in function.
|
||||
balanceTimer: NodeJS.Timeout;
|
||||
// the ratio to scale the shapes (e.g. labelShape, labelBackgroundShape) to keep to visual size while zooming.
|
||||
balanceRatio: number;
|
||||
// last responsed zoom ratio.
|
||||
@ -56,25 +55,29 @@ export abstract class BaseNode {
|
||||
wordWrapWidth: number;
|
||||
// animate configurations for zoom level changing
|
||||
animateConfig: AnimateCfg;
|
||||
// the tag of first rendering
|
||||
firstRender: boolean;
|
||||
} = {
|
||||
hiddenShape: {},
|
||||
zoom: 1,
|
||||
zoomLevel: 0,
|
||||
balanceTimer: undefined,
|
||||
balanceRatio: 1,
|
||||
levelShapes: {},
|
||||
wordWrapWidth: 32,
|
||||
animateConfig: DEFAULT_ANIMATE_CFG.zoom,
|
||||
firstRender: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
const { themeStyles, zoomStrategy } = props;
|
||||
const { themeStyles, lodStrategy, zoom } = props;
|
||||
if (themeStyles) this.themeStyles = themeStyles;
|
||||
this.zoomStrategy = zoomStrategy;
|
||||
this.lodStrategy = lodStrategy;
|
||||
this.boundsCache = {};
|
||||
this.zoomCache.zoom = zoom;
|
||||
this.zoomCache.balanceRatio = 1 / zoom;
|
||||
this.zoomCache.animateConfig = {
|
||||
...DEFAULT_ANIMATE_CFG.zoom,
|
||||
...zoomStrategy?.animateCfg,
|
||||
...lodStrategy?.animateCfg,
|
||||
};
|
||||
}
|
||||
public mergeStyles(model: NodeDisplayModel) {
|
||||
@ -131,31 +134,37 @@ export abstract class BaseNode {
|
||||
* Call it after calling draw function to update cache about bounds and zoom levels.
|
||||
*/
|
||||
public updateCache(shapeMap) {
|
||||
['keyShape', 'labelShape', 'rightBadgeShape']
|
||||
.concat(Object.keys(BadgePosition).map((pos) => `${pos}BadgeShape`))
|
||||
['keyShape', 'labelShape']
|
||||
.concat(Object.keys(BadgePosition))
|
||||
.map((pos) => `${pos}BadgeShape`)
|
||||
.forEach((id) => {
|
||||
const shape = shapeMap[id];
|
||||
if (shape?.getAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY)) {
|
||||
this.boundsCache[`${id}Local`] = shape.getLocalBounds();
|
||||
this.boundsCache[`${id}Local`] =
|
||||
id === 'labelShape'
|
||||
? shape.getGeometryBounds()
|
||||
: shape.getLocalBounds();
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, false);
|
||||
}
|
||||
});
|
||||
|
||||
const { levelShapes } = this.zoomCache;
|
||||
Object.keys(shapeMap).forEach((shapeId) => {
|
||||
const { showLevel } = shapeMap[shapeId].attributes;
|
||||
if (showLevel !== undefined) {
|
||||
levelShapes[showLevel] = levelShapes[showLevel] || [];
|
||||
levelShapes[showLevel].push(shapeId);
|
||||
const { lod } = shapeMap[shapeId].attributes;
|
||||
if (lod !== undefined) {
|
||||
levelShapes[lod] = levelShapes[lod] || [];
|
||||
levelShapes[lod].push(shapeId);
|
||||
}
|
||||
});
|
||||
|
||||
const { maxWidth = '200%' } = this.mergedStyles.labelShape || {};
|
||||
this.zoomCache.wordWrapWidth = getWordWrapWidthByBox(
|
||||
this.boundsCache.keyShapeLocal,
|
||||
maxWidth,
|
||||
1,
|
||||
);
|
||||
if (shapeMap.labelShape && this.boundsCache.keyShapeLocal) {
|
||||
const { maxWidth = '200%' } = this.mergedStyles.labelShape || {};
|
||||
this.zoomCache.wordWrapWidth = getWordWrapWidthByBox(
|
||||
this.boundsCache.keyShapeLocal,
|
||||
maxWidth,
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
abstract draw(
|
||||
model: NodeDisplayModel,
|
||||
@ -199,18 +208,23 @@ export abstract class BaseNode {
|
||||
const { keyShape } = shapeMap;
|
||||
this.boundsCache.keyShapeLocal =
|
||||
this.boundsCache.keyShapeLocal || keyShape.getLocalBounds();
|
||||
const keyShapeBox = this.boundsCache.keyShapeLocal;
|
||||
const keyShapeBox = getShapeLocalBoundsByStyle(
|
||||
keyShape,
|
||||
this.mergedStyles.keyShape,
|
||||
this.boundsCache.keyShapeLocal,
|
||||
);
|
||||
const { labelShape: shapeStyle } = this.mergedStyles;
|
||||
const {
|
||||
position,
|
||||
offsetX: propsOffsetX,
|
||||
offsetY: propsOffsetY,
|
||||
offsetZ: propsOffsetZ,
|
||||
maxWidth,
|
||||
...otherStyle
|
||||
} = shapeStyle;
|
||||
|
||||
const wordWrapWidth = getWordWrapWidthByBox(
|
||||
keyShapeBox,
|
||||
keyShapeBox as AABB,
|
||||
maxWidth,
|
||||
this.zoomCache.zoom,
|
||||
);
|
||||
@ -218,10 +232,12 @@ export abstract class BaseNode {
|
||||
const positionPreset = {
|
||||
x: keyShapeBox.center[0],
|
||||
y: keyShapeBox.max[1],
|
||||
z: keyShapeBox.center[2],
|
||||
textBaseline: 'top',
|
||||
textAlign: 'center',
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
offsetZ: 0,
|
||||
wordWrapWidth,
|
||||
};
|
||||
switch (position) {
|
||||
@ -257,8 +273,12 @@ export abstract class BaseNode {
|
||||
const offsetY = (
|
||||
propsOffsetY === undefined ? positionPreset.offsetY : propsOffsetY
|
||||
) as number;
|
||||
const offsetZ = (
|
||||
propsOffsetZ === undefined ? positionPreset.offsetZ : propsOffsetZ
|
||||
) as number;
|
||||
positionPreset.x += offsetX;
|
||||
positionPreset.y += offsetY;
|
||||
positionPreset.z += offsetZ;
|
||||
|
||||
const style: any = {
|
||||
...this.defaultStyles.labelShape,
|
||||
@ -280,24 +300,33 @@ export abstract class BaseNode {
|
||||
!this.boundsCache.labelShapeLocal ||
|
||||
labelShape.getAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY)
|
||||
) {
|
||||
this.boundsCache.labelShapeLocal = labelShape.getLocalBounds();
|
||||
this.boundsCache.labelShapeLocal = labelShape.getGeometryBounds();
|
||||
labelShape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, false);
|
||||
}
|
||||
// label's local bounds, will take scale into acount
|
||||
const { labelShapeLocal: textBBox } = this.boundsCache;
|
||||
const labelWidth = Math.min(
|
||||
textBBox.max[0] - textBBox.min[0],
|
||||
labelShape.attributes.wordWrapWidth,
|
||||
);
|
||||
const height = textBBox.max[1] - textBBox.min[1];
|
||||
const labelAspectRatio = labelWidth / (textBBox.max[1] - textBBox.min[1]);
|
||||
const width = labelAspectRatio * height;
|
||||
|
||||
const { padding, ...backgroundStyle } =
|
||||
this.mergedStyles.labelBackgroundShape;
|
||||
const { balanceRatio = 1 } = this.zoomCache;
|
||||
const y =
|
||||
labelShape.attributes.y -
|
||||
(labelShape.attributes.textBaseline === 'top'
|
||||
? padding[0]
|
||||
: height / 2 + padding[0]);
|
||||
const bgStyle: any = {
|
||||
fill: '#fff',
|
||||
...backgroundStyle,
|
||||
x: textBBox.min[0] - padding[3],
|
||||
y: textBBox.min[1] - padding[0] / balanceRatio,
|
||||
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
|
||||
height:
|
||||
textBBox.max[1] -
|
||||
textBBox.min[1] +
|
||||
(padding[0] + padding[2]) / balanceRatio,
|
||||
x: textBBox.center[0] - width / 2 - padding[3],
|
||||
y,
|
||||
width: width + padding[1] + padding[3],
|
||||
height: height + padding[0] + padding[2],
|
||||
};
|
||||
|
||||
return this.upsertShape(
|
||||
@ -357,12 +386,14 @@ export abstract class BaseNode {
|
||||
): DisplayObject {
|
||||
const { keyShape } = shapeMap;
|
||||
const { haloShape: haloShapeStyle } = this.mergedStyles;
|
||||
if (haloShapeStyle.visible === false) return;
|
||||
const { nodeName, attributes } = keyShape;
|
||||
return this.upsertShape(
|
||||
nodeName as SHAPE_TYPE,
|
||||
'haloShape',
|
||||
{
|
||||
...attributes,
|
||||
stroke: attributes.fill,
|
||||
...haloShapeStyle,
|
||||
},
|
||||
shapeMap,
|
||||
@ -420,14 +451,19 @@ export abstract class BaseNode {
|
||||
): {
|
||||
[shapeId: string]: DisplayObject;
|
||||
} {
|
||||
const commonStyle = this.mergedStyles.badgeShapes;
|
||||
const { badgeShapes: commonStyle, keyShape: keyShapeStyle } =
|
||||
this.mergedStyles;
|
||||
const individualConfigs = Object.values(this.mergedStyles).filter(
|
||||
(style) => style.tag === 'badgeShape',
|
||||
);
|
||||
if (!individualConfigs.length) return {};
|
||||
this.boundsCache.keyShapeLocal =
|
||||
this.boundsCache.keyShapeLocal || shapeMap.keyShape.getLocalBounds();
|
||||
const { keyShapeLocal: keyShapeBBox } = this.boundsCache;
|
||||
const keyShapeBBox = getShapeLocalBoundsByStyle(
|
||||
shapeMap.keyShape,
|
||||
keyShapeStyle,
|
||||
this.boundsCache.keyShapeLocal,
|
||||
);
|
||||
const keyShapeWidth = keyShapeBBox.max[0] - keyShapeBBox.min[0];
|
||||
const shapes = {};
|
||||
individualConfigs.forEach((config) => {
|
||||
@ -498,7 +534,7 @@ export abstract class BaseNode {
|
||||
{
|
||||
text,
|
||||
fill: textColor,
|
||||
fontSize: bgHeight - 2,
|
||||
fontSize: bgHeight - 3,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
...otherStyles,
|
||||
@ -509,8 +545,7 @@ export abstract class BaseNode {
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
this.boundsCache[`${id}Local`] =
|
||||
this.boundsCache[`${id}Local`] || shapes[id].getLocalBounds();
|
||||
this.boundsCache[`${id}Local`] = shapes[id].getLocalBounds();
|
||||
const bbox = this.boundsCache[`${id}Local`];
|
||||
|
||||
const bgShapeId = `${position}BadgeBackgroundShape`;
|
||||
@ -526,7 +561,7 @@ export abstract class BaseNode {
|
||||
fill: color,
|
||||
height: bgHeight,
|
||||
width: bgWidth,
|
||||
x: bbox.min[0] - 2, // begin at the border, minus half height
|
||||
x: bbox.min[0] - 3, // begin at the border, minus half height
|
||||
y: bbox.min[1],
|
||||
radius: bgHeight / 2,
|
||||
zIndex,
|
||||
@ -557,37 +592,57 @@ export abstract class BaseNode {
|
||||
* @param zoom
|
||||
*/
|
||||
public onZoom = (shapeMap: NodeShapeMap, zoom: number) => {
|
||||
this.balanceShapeSize(shapeMap, zoom);
|
||||
// zoomLevel changed
|
||||
if (!this.zoomStrategy) return;
|
||||
const { levels } = this.zoomStrategy;
|
||||
// last zoom ratio responsed by zoom changing, which might not equal to zoom.previous in props since the function is debounced.
|
||||
const {
|
||||
levelShapes,
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
zoomLevel: previousLevel,
|
||||
} = this.zoomCache;
|
||||
const currentLevel = getZoomLevel(levels, zoom);
|
||||
if (currentLevel < previousLevel) {
|
||||
// zoomLevel changed, from higher to lower, hide something
|
||||
levelShapes[currentLevel + 1]?.forEach((id) =>
|
||||
fadeOut(id, shapeMap[id], hiddenShape, animateConfig),
|
||||
);
|
||||
} else if (currentLevel > previousLevel) {
|
||||
// zoomLevel changed, from lower to higher, show something
|
||||
levelShapes[String(currentLevel)]?.forEach((id) =>
|
||||
fadeIn(
|
||||
id,
|
||||
shapeMap[id],
|
||||
this.mergedStyles[id] ||
|
||||
this.mergedStyles[id.replace('Background', '')],
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
),
|
||||
);
|
||||
if (this.lodStrategy) {
|
||||
const { levels } = this.lodStrategy;
|
||||
// last zoom ratio responsed by zoom changing, which might not equal to zoom.previous in props since the function is debounced.
|
||||
const {
|
||||
levelShapes,
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
firstRender = true,
|
||||
zoomLevel: previousLevel,
|
||||
} = this.zoomCache;
|
||||
const currentLevel = getZoomLevel(levels, zoom);
|
||||
const levelNums = Object.keys(levelShapes).map(Number);
|
||||
const maxLevel = Math.max(...levelNums);
|
||||
const minLevel = Math.min(...levelNums);
|
||||
if (currentLevel < previousLevel) {
|
||||
if (firstRender) {
|
||||
// zoomLevel changed, from higher to lower, hide something
|
||||
for (let i = currentLevel + 1; i <= maxLevel; i++) {
|
||||
levelShapes[String(i)]?.forEach((id) => {
|
||||
if (!shapeMap[id]) return;
|
||||
shapeMap[id].hide();
|
||||
hiddenShape[id] = true;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// zoomLevel changed, from higher to lower, hide something
|
||||
for (let i = currentLevel + 1; i <= maxLevel; i++) {
|
||||
levelShapes[String(i)]?.forEach((id) =>
|
||||
fadeOut(id, shapeMap[id], hiddenShape, animateConfig),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (currentLevel > previousLevel) {
|
||||
// zoomLevel changed, from lower to higher, show something
|
||||
for (let i = currentLevel; i >= minLevel; i--) {
|
||||
levelShapes[String(i)]?.forEach((id) => {
|
||||
fadeIn(
|
||||
id,
|
||||
shapeMap[id],
|
||||
this.mergedStyles[id] ||
|
||||
this.mergedStyles[id.replace('Background', '')],
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.zoomCache.zoomLevel = currentLevel;
|
||||
}
|
||||
this.zoomCache.zoomLevel = currentLevel;
|
||||
this.balanceShapeSize(shapeMap, zoom);
|
||||
this.zoomCache.zoom = zoom;
|
||||
};
|
||||
|
||||
@ -604,7 +659,7 @@ export abstract class BaseNode {
|
||||
this.zoomCache.balanceRatio = balanceRatio;
|
||||
const { labelShape: labelStyle } = this.mergedStyles;
|
||||
const { position = 'bottom' } = labelStyle;
|
||||
if (!labelShape) return;
|
||||
if (!labelShape || !labelShape.isVisible()) return;
|
||||
|
||||
if (position === 'bottom') labelShape.style.transformOrigin = '0';
|
||||
else labelShape.style.transformOrigin = '';
|
||||
@ -612,7 +667,7 @@ export abstract class BaseNode {
|
||||
const wordWrapWidth = this.zoomCache.wordWrapWidth * zoom;
|
||||
labelShape.style.wordWrapWidth = wordWrapWidth;
|
||||
|
||||
if (!labelBackgroundShape) return;
|
||||
if (!labelBackgroundShape || !labelBackgroundShape.isVisible()) return;
|
||||
|
||||
const { padding } = this.mergedStyles.labelBackgroundShape;
|
||||
const { width, height } = labelBackgroundShape.attributes;
|
||||
@ -641,8 +696,16 @@ export abstract class BaseNode {
|
||||
paddingLeft + (width - paddingLeft - paddingRight) / 2
|
||||
} ${paddingTop + (height - paddingTop - paddingBottom) / 2}`;
|
||||
}
|
||||
// only scale y-asix, to expand the text range while zoom-in
|
||||
labelBackgroundShape.style.transform = `scale(1, ${balanceRatio})`;
|
||||
|
||||
const labelBBox = labelShape.getGeometryBounds();
|
||||
const labelWidth = Math.min(
|
||||
labelBBox.max[0] - labelBBox.min[0],
|
||||
this.zoomCache.wordWrapWidth,
|
||||
);
|
||||
const xAxistRatio =
|
||||
((labelWidth + paddingLeft + paddingRight) * balanceRatio) / width;
|
||||
|
||||
labelBackgroundShape.style.transform = `scale(${xAxistRatio}, ${balanceRatio})`;
|
||||
}
|
||||
|
||||
public upsertShape(
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { DEFAULT_LABEL_BG_PADDING } from '../../../constant';
|
||||
import { NodeDisplayModel } from '../../../types';
|
||||
import {
|
||||
GShapeStyle,
|
||||
ItemShapeStyles,
|
||||
SHAPE_TYPE,
|
||||
SHAPE_TYPE_3D,
|
||||
ShapeStyle,
|
||||
@ -19,7 +17,7 @@ import { BaseNode } from './base';
|
||||
|
||||
export abstract class BaseNode3D extends BaseNode {
|
||||
type: string;
|
||||
defaultStyles: ItemShapeStyles;
|
||||
defaultStyles: NodeShapeStyles;
|
||||
themeStyles: NodeShapeStyles;
|
||||
mergedStyles: NodeShapeStyles;
|
||||
device: any; // for 3d renderer
|
||||
@ -35,7 +33,78 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
return super.drawLabelShape(model, shapeMap, diffData, diffState);
|
||||
const { keyShape } = shapeMap;
|
||||
const { r, width, height, depth, x, y, z } = keyShape.attributes;
|
||||
const keyShapeBox = {
|
||||
center: [x, y, z],
|
||||
min: [x - r || width / 2, y - r || height / 2, z - r || depth / 2],
|
||||
max: [x + r || width / 2, y + r || height / 2, z + r || depth / 2],
|
||||
};
|
||||
const { labelShape: shapeStyle } = this.mergedStyles;
|
||||
const {
|
||||
position,
|
||||
offsetX: propsOffsetX,
|
||||
offsetY: propsOffsetY,
|
||||
maxWidth,
|
||||
...otherStyle
|
||||
} = shapeStyle;
|
||||
|
||||
// TODO
|
||||
// const wordWrapWidth = getWordWrapWidthByBox(
|
||||
// keyShapeBox,
|
||||
// maxWidth,
|
||||
// this.zoomCache.zoom,
|
||||
// );
|
||||
|
||||
const positionPreset = {
|
||||
x: keyShapeBox.center[0],
|
||||
y: keyShapeBox.max[1],
|
||||
z: keyShapeBox.center[2],
|
||||
textBaseline: 'top',
|
||||
textAlign: 'center',
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
// wordWrapWidth,
|
||||
};
|
||||
switch (position) {
|
||||
case 'top':
|
||||
positionPreset.y = keyShapeBox.min[1];
|
||||
positionPreset.textBaseline = 'bottom';
|
||||
positionPreset.offsetY = -4;
|
||||
break;
|
||||
case 'left':
|
||||
positionPreset.x = keyShapeBox.min[0];
|
||||
positionPreset.y = keyShapeBox.center[1];
|
||||
positionPreset.textAlign = 'right';
|
||||
positionPreset.textBaseline = 'middle';
|
||||
positionPreset.offsetX = -8;
|
||||
break;
|
||||
case 'right':
|
||||
positionPreset.x = keyShapeBox.max[0];
|
||||
positionPreset.y = keyShapeBox.center[1];
|
||||
positionPreset.textAlign = 'left';
|
||||
positionPreset.textBaseline = 'middle';
|
||||
positionPreset.offsetX = 8;
|
||||
break;
|
||||
default: // at bottom by default
|
||||
positionPreset.offsetY = 4;
|
||||
break;
|
||||
}
|
||||
const offsetX = (
|
||||
propsOffsetX === undefined ? positionPreset.offsetX : propsOffsetX
|
||||
) as number;
|
||||
const offsetY = (
|
||||
propsOffsetY === undefined ? positionPreset.offsetY : propsOffsetY
|
||||
) as number;
|
||||
positionPreset.x += offsetX;
|
||||
positionPreset.y += offsetY;
|
||||
|
||||
const style: any = {
|
||||
...this.defaultStyles.labelShape,
|
||||
...positionPreset,
|
||||
...otherStyle,
|
||||
};
|
||||
return this.upsertShape('text', 'labelShape', style, shapeMap);
|
||||
}
|
||||
|
||||
// TODO: 3d icon? - billboard image or text for alpha
|
||||
@ -57,12 +126,14 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
): DisplayObject {
|
||||
const { keyShape } = shapeMap;
|
||||
const { haloShape: haloShapeStyle } = this.mergedStyles;
|
||||
if (haloShapeStyle.visible === false) return;
|
||||
const { nodeName, attributes } = keyShape;
|
||||
return this.upsertShape(
|
||||
nodeName as SHAPE_TYPE,
|
||||
'haloShape',
|
||||
{
|
||||
...attributes,
|
||||
stroke: attributes.fill,
|
||||
...haloShapeStyle,
|
||||
},
|
||||
shapeMap,
|
||||
|
@ -173,9 +173,14 @@ export default class Minimap extends Base {
|
||||
}
|
||||
|
||||
// Translate tht graph and update minimap viewport.
|
||||
graph!.translate((dx * zoom) / ratio, (dy * zoom) / ratio).then(() => {
|
||||
this.updateViewport();
|
||||
});
|
||||
graph!
|
||||
.translate({
|
||||
dx: (dx * zoom) / ratio,
|
||||
dy: (dy * zoom) / ratio,
|
||||
})
|
||||
.then(() => {
|
||||
this.updateViewport();
|
||||
});
|
||||
x = e.clientX;
|
||||
y = e.clientY;
|
||||
};
|
||||
|
@ -1,100 +1,188 @@
|
||||
import { DEFAULT_SHAPE_STYLE, DEFAULT_TEXT_STYLE } from '../../constant';
|
||||
import { ThemeSpecification } from '../../types/theme';
|
||||
|
||||
const subjectColor = 'rgb(95, 149, 255)';
|
||||
const textColor = 'rgb(0, 0, 0)';
|
||||
const subjectColor = 'rgb(34,126,255)';
|
||||
const textColor = 'rgba(255,255,255,0.85)';
|
||||
|
||||
const activeFill = 'rgb(247, 250, 255)';
|
||||
// const nodeMainFill = 'rgb(239, 244, 255)';
|
||||
const nodeMainFill = 'rgb(255, 255, 255)';
|
||||
const nodeColor = 'rgb(34,126,255)';
|
||||
const edgeColor = 'rgb(153, 173, 209)';
|
||||
const comboFill = 'rgb(253, 253, 253)';
|
||||
const disabledFill = 'rgb(250, 250, 250)';
|
||||
const disabledFill = '#D0E4FF';
|
||||
|
||||
const edgeMainStroke = 'rgb(224, 224, 224)';
|
||||
const edgeInactiveStroke = 'rgb(234, 234, 234)';
|
||||
const edgeDisablesStroke = 'rgb(245, 245, 245)';
|
||||
const inactiveStroke = 'rgb(191, 213, 255)';
|
||||
const edgeMainStroke = '#637088';
|
||||
|
||||
const highlightStroke = '#4572d9';
|
||||
const highlightFill = 'rgb(223, 234, 255)';
|
||||
const nodeStroke = '#D0E4FF';
|
||||
|
||||
export default {
|
||||
node: {
|
||||
palette: [],
|
||||
palette: [
|
||||
'#227EFF',
|
||||
'#AD5CFF',
|
||||
'#00B8B8',
|
||||
'#FA822D',
|
||||
'#F252AF',
|
||||
'#1EB8F5',
|
||||
'#108A44',
|
||||
'#F4B106',
|
||||
'#5241A8',
|
||||
'#95CF21',
|
||||
],
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ zoomRange: [0, 0.65] },
|
||||
{ zoomRange: [0.65, 0.8] },
|
||||
{ zoomRange: [0.8, 1.6], primary: true },
|
||||
{ zoomRange: [1.6, 2] },
|
||||
{ zoomRange: [2, Infinity] },
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 200,
|
||||
},
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
default: {
|
||||
keyShape: {
|
||||
...DEFAULT_SHAPE_STYLE,
|
||||
r: 10,
|
||||
fill: nodeMainFill,
|
||||
stroke: subjectColor,
|
||||
lineWidth: 1,
|
||||
r: 16,
|
||||
fill: nodeColor,
|
||||
lineWidth: 0,
|
||||
zIndex: 0,
|
||||
},
|
||||
labelShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: '#000',
|
||||
fill: textColor,
|
||||
opacity: 0.85,
|
||||
position: 'bottom',
|
||||
offsetY: 4,
|
||||
zIndex: 2,
|
||||
lod: 0,
|
||||
maxWidth: '200%',
|
||||
textOverflow: 'ellipsis',
|
||||
wordWrap: true,
|
||||
maxLines: 1,
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
padding: [2, 4, 2, 4],
|
||||
lineWidth: 0,
|
||||
fill: '#000',
|
||||
opacity: 0.75,
|
||||
zIndex: -1,
|
||||
lod: 0,
|
||||
},
|
||||
iconShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: '#333',
|
||||
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*wAmHQJbNVdwAAAAAAAAAAABkARQnAQ',
|
||||
width: 15,
|
||||
height: 15,
|
||||
fill: '#fff',
|
||||
fontSize: 16,
|
||||
zIndex: 1,
|
||||
lod: -1,
|
||||
},
|
||||
anchorShapes: {
|
||||
lineWidth: 1,
|
||||
stroke: nodeStroke,
|
||||
fill: '#000',
|
||||
zIndex: 2,
|
||||
r: 3,
|
||||
lod: 0,
|
||||
},
|
||||
badgeShapes: {
|
||||
color: 'rgb(140, 140, 140)',
|
||||
textColor: '#fff',
|
||||
zIndex: 3,
|
||||
lod: -1,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
keyShape: {
|
||||
fill: nodeMainFill,
|
||||
stroke: subjectColor,
|
||||
lineWidth: 4,
|
||||
shadowColor: subjectColor,
|
||||
shadowBlur: 10,
|
||||
stroke: nodeStroke,
|
||||
lineWidth: 3,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
haloShape: {
|
||||
opacity: 0.45,
|
||||
lineWidth: 20,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
keyShape: {
|
||||
fill: activeFill,
|
||||
stroke: subjectColor,
|
||||
shadowColor: subjectColor,
|
||||
lineWidth: 2,
|
||||
shadowBlur: 10,
|
||||
lineWidth: 0,
|
||||
},
|
||||
haloShape: {
|
||||
opacity: 0.25,
|
||||
lineWidth: 20,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
keyShape: {
|
||||
fill: highlightFill,
|
||||
stroke: highlightStroke,
|
||||
lineWidth: 2,
|
||||
stroke: nodeStroke,
|
||||
lineWidth: 3,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
keyShape: {
|
||||
fill: activeFill,
|
||||
stroke: inactiveStroke,
|
||||
lineWidth: 1,
|
||||
opacity: 0.45,
|
||||
},
|
||||
labelShape: {
|
||||
opacity: 0.45,
|
||||
},
|
||||
iconShape: {
|
||||
opacity: 0.45,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
disable: {
|
||||
keyShape: {
|
||||
fill: disabledFill,
|
||||
stroke: edgeMainStroke,
|
||||
lineWidth: 1,
|
||||
lineWidth: 0,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
edge: {
|
||||
palette: [],
|
||||
palette: [
|
||||
'#63A4FF',
|
||||
'#CD9CFF',
|
||||
'#2DEFEF',
|
||||
'#FFBDA1',
|
||||
'#F49FD0',
|
||||
'#80DBFF',
|
||||
'#41CB7C',
|
||||
'#FFD362',
|
||||
'#A192E8',
|
||||
'#CEFB75',
|
||||
],
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ zoomRange: [0, 0.65] },
|
||||
{ zoomRange: [0.65, 0.8] },
|
||||
{ zoomRange: [0.8, 1.6], primary: true },
|
||||
{ zoomRange: [1.6, 2] },
|
||||
{ zoomRange: [2, Infinity] },
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 200,
|
||||
},
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
default: {
|
||||
@ -107,52 +195,77 @@ export default {
|
||||
labelShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: textColor,
|
||||
textAlign: 'center',
|
||||
opacity: 0.85,
|
||||
position: 'middle',
|
||||
textBaseline: 'middle',
|
||||
zIndex: 2,
|
||||
textOverflow: 'ellipsis',
|
||||
wordWrap: true,
|
||||
maxLines: 1,
|
||||
maxWidth: '60%',
|
||||
lod: 0,
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
padding: [4, 4, 4, 4],
|
||||
lineWidth: 0,
|
||||
fill: '#000',
|
||||
opacity: 0.75,
|
||||
zIndex: 1,
|
||||
lod: 0,
|
||||
},
|
||||
iconShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: '#333',
|
||||
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*wAmHQJbNVdwAAAAAAAAAAABkARQnAQ',
|
||||
width: 15,
|
||||
height: 15,
|
||||
fill: 'rgb(140, 140, 140)',
|
||||
fontSize: 16,
|
||||
zIndex: 2,
|
||||
offsetX: -10,
|
||||
lod: -1,
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
keyShape: {
|
||||
stroke: subjectColor,
|
||||
lineWidth: 2,
|
||||
shadowColor: subjectColor,
|
||||
shadowBlur: 10,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
haloShape: {
|
||||
opacity: 0.25,
|
||||
lineWidth: 12,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
keyShape: {
|
||||
stroke: subjectColor,
|
||||
lineWidth: 1,
|
||||
},
|
||||
haloShape: {
|
||||
opacity: 0.25,
|
||||
lineWidth: 12,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
keyShape: {
|
||||
stroke: subjectColor,
|
||||
lineWidth: 2,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
keyShape: {
|
||||
stroke: edgeInactiveStroke,
|
||||
stroke: edgeMainStroke,
|
||||
lineWidth: 1,
|
||||
opacity: 0.45,
|
||||
},
|
||||
},
|
||||
disable: {
|
||||
keyShape: {
|
||||
stroke: edgeDisablesStroke,
|
||||
stroke: edgeMainStroke,
|
||||
opacity: 0.08,
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
@ -177,31 +290,28 @@ export default {
|
||||
},
|
||||
selected: {
|
||||
keyShape: {
|
||||
stroke: subjectColor,
|
||||
stroke: nodeStroke,
|
||||
fill: comboFill,
|
||||
shadowColor: subjectColor,
|
||||
lineWidth: 2,
|
||||
shadowBlur: 10,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
keyShape: {
|
||||
stroke: subjectColor,
|
||||
stroke: nodeStroke,
|
||||
lineWidth: 1,
|
||||
fill: activeFill,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
keyShape: {
|
||||
stroke: highlightStroke,
|
||||
stroke: nodeStroke,
|
||||
fill: comboFill,
|
||||
lineWidth: 2,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
@ -213,9 +323,12 @@ export default {
|
||||
},
|
||||
disable: {
|
||||
keyShape: {
|
||||
stroke: edgeInactiveStroke,
|
||||
fill: disabledFill,
|
||||
lineWidth: 1,
|
||||
opacity: 0.25,
|
||||
},
|
||||
iconShape: {
|
||||
fill: disabledFill,
|
||||
opacity: 0.25,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -14,7 +14,6 @@ const edgeDisableStroke = 'rgb(217, 217, 217)';
|
||||
const edgeInactiveStroke = 'rgb(210, 218, 233)';
|
||||
|
||||
const nodeStroke = 'rgba(0,0,0,0.85)';
|
||||
const haloStroke = 'rgb(0, 0, 0)';
|
||||
|
||||
export default {
|
||||
node: {
|
||||
@ -30,13 +29,13 @@ export default {
|
||||
'#5241A8',
|
||||
'#95CF21',
|
||||
],
|
||||
zoomStrategy: {
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ range: [0, 0.65] },
|
||||
{ range: [0.65, 0.8] },
|
||||
{ range: [0.8, 1.6], primary: true },
|
||||
{ range: [1.6, 2] },
|
||||
{ range: [2, Infinity] },
|
||||
{ zoomRange: [0, 0.65] },
|
||||
{ zoomRange: [0.65, 0.8] },
|
||||
{ zoomRange: [0.8, 1.6], primary: true },
|
||||
{ zoomRange: [1.6, 2] },
|
||||
{ zoomRange: [2, Infinity] },
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 200,
|
||||
@ -57,7 +56,7 @@ export default {
|
||||
fill: '#000',
|
||||
position: 'bottom',
|
||||
zIndex: 2,
|
||||
showLevel: 0,
|
||||
lod: 0,
|
||||
maxWidth: '200%',
|
||||
textOverflow: 'ellipsis',
|
||||
wordWrap: true,
|
||||
@ -69,53 +68,56 @@ export default {
|
||||
fill: '#fff',
|
||||
opacity: 0.75,
|
||||
zIndex: -1,
|
||||
showLevel: 0,
|
||||
lod: 0,
|
||||
},
|
||||
iconShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: '#fff',
|
||||
fontSize: 16,
|
||||
zIndex: 1,
|
||||
showLevel: -1,
|
||||
lod: -1,
|
||||
},
|
||||
anchorShapes: {
|
||||
lineWidth: 1,
|
||||
stroke: 'rgba(0, 0, 0, 0.65)',
|
||||
zIndex: 2,
|
||||
r: 3,
|
||||
showLevel: 0,
|
||||
lod: 0,
|
||||
},
|
||||
badgeShapes: {
|
||||
color: 'rgb(140, 140, 140)',
|
||||
textColor: '#fff',
|
||||
zIndex: 3,
|
||||
showLevel: -1,
|
||||
lod: -1,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
keyShape: {
|
||||
stroke: nodeStroke,
|
||||
stroke: '#000',
|
||||
lineWidth: 3,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
haloShape: {
|
||||
stroke: haloStroke,
|
||||
opacity: 0.06,
|
||||
opacity: 0.25,
|
||||
lineWidth: 20,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
keyShape: {
|
||||
stroke: nodeStroke,
|
||||
lineWidth: 2,
|
||||
lineWidth: 0,
|
||||
},
|
||||
haloShape: {
|
||||
stroke: haloStroke,
|
||||
opacity: 0.06,
|
||||
lineWidth: 4,
|
||||
opacity: 0.25,
|
||||
lineWidth: 20,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
@ -124,7 +126,10 @@ export default {
|
||||
lineWidth: 3,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
@ -137,12 +142,18 @@ export default {
|
||||
iconShape: {
|
||||
opacity: 0.25,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
disable: {
|
||||
keyShape: {
|
||||
fill: disabledFill,
|
||||
lineWidth: 0,
|
||||
},
|
||||
haloShape: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -160,13 +171,13 @@ export default {
|
||||
'#A192E8',
|
||||
'#CEFB75',
|
||||
],
|
||||
zoomStrategy: {
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ range: [0, 0.65] },
|
||||
{ range: [0.65, 0.8] },
|
||||
{ range: [0.8, 1.6], primary: true },
|
||||
{ range: [1.6, 2] },
|
||||
{ range: [2, Infinity] },
|
||||
{ zoomRange: [0, 0.65] },
|
||||
{ zoomRange: [0.65, 0.8] },
|
||||
{ zoomRange: [0.8, 1.6], primary: true },
|
||||
{ zoomRange: [1.6, 2] },
|
||||
{ zoomRange: [2, Infinity] },
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 200,
|
||||
@ -191,7 +202,7 @@ export default {
|
||||
wordWrap: true,
|
||||
maxLines: 1,
|
||||
maxWidth: '60%',
|
||||
showLevel: 0,
|
||||
lod: 0,
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
padding: [4, 4, 4, 4],
|
||||
@ -199,7 +210,7 @@ export default {
|
||||
fill: '#fff',
|
||||
opacity: 0.75,
|
||||
zIndex: 1,
|
||||
showLevel: 0,
|
||||
lod: 0,
|
||||
},
|
||||
iconShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
@ -207,7 +218,7 @@ export default {
|
||||
fontSize: 16,
|
||||
zIndex: 2,
|
||||
offsetX: -10,
|
||||
showLevel: -1,
|
||||
lod: -1,
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
@ -215,13 +226,13 @@ export default {
|
||||
lineWidth: 2,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
haloShape: {
|
||||
stroke: haloStroke,
|
||||
opacity: 0.06,
|
||||
opacity: 0.25,
|
||||
lineWidth: 12,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
@ -229,10 +240,10 @@ export default {
|
||||
lineWidth: 1,
|
||||
},
|
||||
haloShape: {
|
||||
stroke: haloStroke,
|
||||
opacity: 0.06,
|
||||
opacity: 0.25,
|
||||
lineWidth: 12,
|
||||
zIndex: -1,
|
||||
visible: true,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
@ -240,7 +251,7 @@ export default {
|
||||
lineWidth: 2,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
@ -281,7 +292,7 @@ export default {
|
||||
lineWidth: 2,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
@ -297,7 +308,7 @@ export default {
|
||||
lineWidth: 2,
|
||||
},
|
||||
labelShape: {
|
||||
fontWeight: 500,
|
||||
fontWeight: 700,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
@ -317,6 +328,6 @@ export default {
|
||||
],
|
||||
},
|
||||
canvas: {
|
||||
backgroundColor: '#000',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
} as ThemeSpecification;
|
||||
|
@ -9,6 +9,7 @@ export default abstract class BaseThemeSolver {
|
||||
protected options: ThemeSolverOptions;
|
||||
constructor(options: ThemeSolverOptions, themes: ThemeSpecificationMap) {
|
||||
this.specification = this.solver(options, themes);
|
||||
this.options = options;
|
||||
}
|
||||
abstract solver(
|
||||
options: ThemeSolverOptions,
|
||||
|
@ -46,7 +46,7 @@ export default class SpecThemeSolver extends BaseThemeSolver {
|
||||
if (!specification[itemType]) return;
|
||||
const {
|
||||
palette = mergedSpec[itemType].palette,
|
||||
zoomStrategy = mergedSpec[itemType].zoomStrategy,
|
||||
lodStrategy = mergedSpec[itemType].lodStrategy,
|
||||
dataTypeField,
|
||||
} = specification[itemType];
|
||||
let { getStyleSets } = specification[itemType];
|
||||
@ -96,7 +96,7 @@ export default class SpecThemeSolver extends BaseThemeSolver {
|
||||
mergedSpec[itemType] = {
|
||||
dataTypeField,
|
||||
palette,
|
||||
zoomStrategy,
|
||||
lodStrategy,
|
||||
styles: mergedStyles,
|
||||
};
|
||||
});
|
||||
|
@ -66,7 +66,6 @@ export interface IAnimates {
|
||||
update?: (IAnimate | IStateAnimate)[];
|
||||
}
|
||||
|
||||
export type CameraAnimationOptions = Pick<
|
||||
IAnimationEffectTiming,
|
||||
'duration' | 'easing' | 'easingFunction'
|
||||
export type CameraAnimationOptions = Partial<
|
||||
Pick<IAnimationEffectTiming, 'duration' | 'easing' | 'easingFunction'>
|
||||
>;
|
||||
|
@ -39,16 +39,10 @@ export type BehaviorOptionsOf<B extends BehaviorRegistry = {}> =
|
||||
| Extract<keyof B, string>
|
||||
| {
|
||||
[K in keyof B]: B[K] extends { new (options: infer O): any }
|
||||
? O & { type: K; key: string }
|
||||
: never;
|
||||
? { type: K; key: string } & O
|
||||
: { type: K; key: string };
|
||||
}[Extract<keyof B, string>];
|
||||
|
||||
export type BehaviorObjectOptionsOf<B extends BehaviorRegistry = {}> = {
|
||||
[K in keyof B]: B[K] extends { new (options: infer O): any }
|
||||
? O & { type: K; key: string }
|
||||
: never;
|
||||
}[Extract<keyof B, string>];
|
||||
|
||||
/**
|
||||
* TODO: interaction specification
|
||||
*/
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
ShapeAttrEncode,
|
||||
ShapesEncode,
|
||||
ShapeStyle,
|
||||
LodStrategy,
|
||||
} from './item';
|
||||
|
||||
export type ComboLabelPosition =
|
||||
@ -46,6 +47,7 @@ export interface ComboShapeStyles extends ItemShapeStyles {
|
||||
position?: ComboLabelPosition;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
};
|
||||
labelBackgroundShape?: ShapeStyle & {
|
||||
padding?: number | number[];
|
||||
@ -68,6 +70,7 @@ export interface ComboShapeStyles extends ItemShapeStyles {
|
||||
size?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
// individual styles and their position
|
||||
[key: number]: ShapeStyle & {
|
||||
position?: BadgePosition;
|
||||
@ -76,12 +79,14 @@ export interface ComboShapeStyles extends ItemShapeStyles {
|
||||
size?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** Displayed data, only for drawing and not received by users. */
|
||||
export type ComboDisplayModelData = ComboModelData & ComboShapeStyles;
|
||||
export type ComboDisplayModelData = ComboModelData &
|
||||
ComboShapeStyles & { lodStrategy?: LodStrategy };
|
||||
|
||||
/** User input model. */
|
||||
export type ComboUserModel = GNode<ComboUserModelData>;
|
||||
@ -97,6 +102,7 @@ interface ComboLabelShapeAttrEncode extends ShapeAttrEncode {
|
||||
position?: ComboLabelPosition | Encode<ComboLabelPosition>;
|
||||
offsetX?: number | Encode<number>;
|
||||
offsetY?: number | Encode<number>;
|
||||
offsetZ?: number | Encode<number>;
|
||||
background?: LabelBackground | Encode<LabelBackground>;
|
||||
}
|
||||
export interface ComboShapesEncode extends ShapesEncode {
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
ShapeAttrEncode,
|
||||
ShapesEncode,
|
||||
ShapeStyle,
|
||||
LodStrategy,
|
||||
} from './item';
|
||||
|
||||
export interface EdgeUserModelData extends PlainObject {
|
||||
@ -61,6 +62,7 @@ export interface EdgeShapeStyles extends ItemShapeStyles {
|
||||
position?: 'start' | 'middle' | 'end';
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
autoRotate?: boolean;
|
||||
// if it is a string, means the percentage of the keyShape, number means pixel
|
||||
maxWidth?: string | number;
|
||||
@ -74,7 +76,8 @@ export interface EdgeShapeStyles extends ItemShapeStyles {
|
||||
};
|
||||
}
|
||||
|
||||
export type EdgeDisplayModelData = EdgeModelData & EdgeShapeStyles;
|
||||
export type EdgeDisplayModelData = EdgeModelData &
|
||||
EdgeShapeStyles & { lodStrategy?: LodStrategy };
|
||||
|
||||
/** User input data. */
|
||||
export type EdgeUserModel = GEdge<EdgeUserModelData>;
|
||||
@ -88,8 +91,9 @@ export type EdgeDisplayModel = GEdge<EdgeDisplayModelData>;
|
||||
export type EdgeLabelPosition = 'start' | 'middle' | 'end';
|
||||
interface EdgeLabelShapeAttrEncode extends ShapeAttrEncode {
|
||||
position?: EdgeLabelPosition | Encode<EdgeLabelPosition>;
|
||||
offsetX: number | Encode<number>;
|
||||
offsetX?: number | Encode<number>;
|
||||
offsetY?: number | Encode<number>;
|
||||
offsetZ?: number | Encode<number>;
|
||||
background?: LabelBackground | Encode<LabelBackground>;
|
||||
autoRotate?: boolean | Encode<boolean>;
|
||||
}
|
||||
|
@ -3,11 +3,7 @@ import { AABB, Canvas, DisplayObject, PointLike } from '@antv/g';
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { Hooks } from '../types/hook';
|
||||
import { CameraAnimationOptions } from './animate';
|
||||
import {
|
||||
BehaviorObjectOptionsOf,
|
||||
BehaviorOptionsOf,
|
||||
BehaviorRegistry,
|
||||
} from './behavior';
|
||||
import { BehaviorOptionsOf, BehaviorRegistry } from './behavior';
|
||||
import { ComboModel, ComboUserModel } from './combo';
|
||||
import { Padding, Point } from './common';
|
||||
import { GraphData } from './data';
|
||||
@ -17,7 +13,7 @@ import { LayoutOptions } from './layout';
|
||||
import { NodeModel, NodeUserModel } from './node';
|
||||
import { RendererName } from './render';
|
||||
import { Specification } from './spec';
|
||||
import { ThemeRegistry } from './theme';
|
||||
import { ThemeOptionsOf, ThemeRegistry } from './theme';
|
||||
import { FitViewRules, GraphTransformOptions } from './view';
|
||||
|
||||
export interface IGraph<
|
||||
@ -37,16 +33,26 @@ export interface IGraph<
|
||||
* @returns
|
||||
* @group Graph Instance
|
||||
*/
|
||||
destroy: () => void;
|
||||
destroy: (callback?: Function) => void;
|
||||
/**
|
||||
* Update the specs(configurations).
|
||||
* Update the specs (configurations).
|
||||
*/
|
||||
updateSpecification: (spec: Specification<B, T>) => void;
|
||||
updateSpecification: (spec: Specification<B, T>) => Specification<B, T>;
|
||||
/**
|
||||
* Update the theme specs (configurations).
|
||||
*/
|
||||
updateTheme: (theme: ThemeOptionsOf<T>) => void;
|
||||
/**
|
||||
* Get the copy of specs(configurations).
|
||||
* @returns graph specs
|
||||
*/
|
||||
getSpecification: () => Specification<B, T>;
|
||||
/**
|
||||
* Change the renderer at runtime.
|
||||
* @param type renderer name
|
||||
* @returns
|
||||
*/
|
||||
changeRenderer: (type: RendererName) => void;
|
||||
|
||||
// ====== data operations ====
|
||||
/**
|
||||
@ -230,8 +236,11 @@ export interface IGraph<
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
translate: (
|
||||
dx: number,
|
||||
dy: number,
|
||||
distance: Partial<{
|
||||
dx: number;
|
||||
dy: number;
|
||||
dz: number;
|
||||
}>,
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
@ -513,7 +522,7 @@ export interface IGraph<
|
||||
* @returns
|
||||
* @group Interaction
|
||||
*/
|
||||
updateBehavior: (behavior: BehaviorObjectOptionsOf<B>, mode?: string) => void;
|
||||
updateBehavior: (behavior: BehaviorOptionsOf<B>, mode?: string) => void;
|
||||
|
||||
/**
|
||||
* Draw or update a G shape or group to the transient canvas.
|
||||
|
@ -21,7 +21,7 @@ export interface IHook<T> {
|
||||
|
||||
export type ViewportChangeHookParams = {
|
||||
transform: GraphTransformOptions;
|
||||
effectTiming?: CameraAnimationOptions;
|
||||
effectTiming?: Partial<CameraAnimationOptions>;
|
||||
};
|
||||
|
||||
export interface Hooks {
|
||||
@ -85,6 +85,14 @@ export interface Hooks {
|
||||
| { key: string; type: string; [cfgName: string]: unknown }
|
||||
)[];
|
||||
}>;
|
||||
themechange: IHook<{
|
||||
theme?: ThemeSpecification;
|
||||
canvases?: {
|
||||
background: Canvas;
|
||||
main: Canvas;
|
||||
transient: Canvas;
|
||||
};
|
||||
}>;
|
||||
destroy: IHook<{}>;
|
||||
// TODO: more timecycles here
|
||||
}
|
||||
|
@ -61,7 +61,8 @@ export type GShapeStyle = CircleStyleProps &
|
||||
export type ShapeStyle = Partial<
|
||||
GShapeStyle & {
|
||||
animates?: IAnimates;
|
||||
showLevel?: number;
|
||||
lod?: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
>;
|
||||
export interface Encode<T> {
|
||||
@ -153,7 +154,7 @@ export type ItemShapeStyles = {
|
||||
ImageStyleProps & {
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
showLevel?: number;
|
||||
lod?: number;
|
||||
}
|
||||
>;
|
||||
haloShape?: ShapeStyle;
|
||||
@ -164,15 +165,15 @@ export type ItemShapeStyles = {
|
||||
animates?: IAnimates;
|
||||
};
|
||||
|
||||
export interface ZoomStrategy {
|
||||
export interface LodStrategy {
|
||||
levels: {
|
||||
range: [number, number];
|
||||
primary: boolean;
|
||||
zoomRange: [number, number];
|
||||
primary?: boolean;
|
||||
}[];
|
||||
animateCfg: AnimateCfg;
|
||||
}
|
||||
|
||||
export interface ZoomStrategyObj {
|
||||
export interface lodStrategyObj {
|
||||
levels: {
|
||||
[levelIdx: number]: [number, number];
|
||||
};
|
||||
@ -221,8 +222,8 @@ export interface IItem {
|
||||
default?: ItemShapeStyles;
|
||||
[stateName: string]: ItemShapeStyles;
|
||||
};
|
||||
/** The zoom strategy to show and hide shapes according to their showLevel. */
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
/** The zoom strategy to show and hide shapes according to their lod. */
|
||||
lodStrategy: lodStrategyObj;
|
||||
/** Last zoom ratio. */
|
||||
zoom: number;
|
||||
/** Cache the chaging states which are not consomed by draw */
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
ShapeAttrEncode,
|
||||
ShapesEncode,
|
||||
ShapeStyle,
|
||||
LodStrategy,
|
||||
} from './item';
|
||||
|
||||
export type NodeLabelPosition = 'bottom' | 'center' | 'top' | 'left' | 'right';
|
||||
@ -78,6 +79,7 @@ export interface NodeShapeStyles extends ItemShapeStyles {
|
||||
position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
// string means the percentage of the keyShape, number means pixel
|
||||
maxWidth?: string | number;
|
||||
};
|
||||
@ -102,6 +104,7 @@ export interface NodeShapeStyles extends ItemShapeStyles {
|
||||
size?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
// individual styles and their position
|
||||
[key: number]: ShapeStyle & {
|
||||
position?: BadgePosition;
|
||||
@ -110,12 +113,14 @@ export interface NodeShapeStyles extends ItemShapeStyles {
|
||||
size?: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
offsetZ?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** Data in display model. */
|
||||
export type NodeDisplayModelData = NodeModelData & NodeShapeStyles;
|
||||
export type NodeDisplayModelData = NodeModelData &
|
||||
NodeShapeStyles & { lodStrategy?: LodStrategy };
|
||||
|
||||
/** User input model. */
|
||||
export type NodeUserModel = GNode<NodeUserModelData>;
|
||||
@ -131,6 +136,7 @@ interface NodeLabelShapeAttrEncode extends ShapeAttrEncode {
|
||||
position?: NodeLabelPosition | Encode<NodeLabelPosition>;
|
||||
offsetX?: number | Encode<number>;
|
||||
offsetY?: number | Encode<number>;
|
||||
offsetZ?: number | Encode<number>;
|
||||
}
|
||||
export interface NodeShapesEncode extends ShapesEncode {
|
||||
labelShape?: NodeLabelShapeAttrEncode | Encode<ShapeStyle>;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { BehaviorRegistry } from './behavior';
|
||||
import { ThemeRegistry } from './theme';
|
||||
|
||||
export type StdLibCategory =
|
||||
| 'transform'
|
||||
@ -12,12 +13,13 @@ export type StdLibCategory =
|
||||
| 'plugin';
|
||||
|
||||
export interface Lib {
|
||||
transforms?: Record<string, unknown>;
|
||||
behaviors?: BehaviorRegistry;
|
||||
themes?: ThemeRegistry;
|
||||
// TODO: type templates
|
||||
transforms?: Record<string, unknown>;
|
||||
layouts?: Record<string, unknown>;
|
||||
nodes?: Record<string, unknown>;
|
||||
edges?: Record<string, unknown>;
|
||||
combos?: Record<string, unknown>;
|
||||
themes?: Record<string, unknown>;
|
||||
plugins?: Record<string, unknown>;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import BaseThemeSolver from 'stdlib/themeSolver/base';
|
||||
import { ComboShapeStyles } from './combo';
|
||||
import { EdgeShapeStyles } from './edge';
|
||||
import { ZoomStrategy } from './item';
|
||||
import { LodStrategy } from './item';
|
||||
import { NodeShapeStyles } from './node';
|
||||
|
||||
export interface ThemeOption {}
|
||||
@ -9,21 +10,21 @@ export interface ThemeOption {}
|
||||
* Two implementing ways: getSpec or getEvents
|
||||
*/
|
||||
export abstract class Theme {
|
||||
protected options: ThemeOption = {};
|
||||
constructor(options: ThemeOption) {
|
||||
options: any = {};
|
||||
constructor(options: any) {
|
||||
this.options = options;
|
||||
}
|
||||
public updateConfig = (options: ThemeOption) => {
|
||||
updateConfig = (options: any) => {
|
||||
this.options = Object.assign(this.options, options);
|
||||
};
|
||||
abstract destroy(): void;
|
||||
destroy() {}
|
||||
}
|
||||
|
||||
/** Theme regisry table.
|
||||
* @example { 'drag-node': DragNodeBehavior, 'my-drag-node': MyDragNodeBehavior }
|
||||
*/
|
||||
export interface ThemeRegistry {
|
||||
[type: string]: typeof Theme;
|
||||
[type: string]: typeof BaseThemeSolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,8 +35,8 @@ export type ThemeOptionsOf<T extends ThemeRegistry = {}> =
|
||||
| Extract<keyof T, string>
|
||||
| {
|
||||
[K in keyof T]: T[K] extends { new (options: infer O): any }
|
||||
? O & { type: K; key: string }
|
||||
: never;
|
||||
? O & { type: K }
|
||||
: { type: K };
|
||||
}[Extract<keyof T, string>];
|
||||
|
||||
export type ThemeObjectOptionsOf<T extends ThemeRegistry = {}> = {
|
||||
@ -73,19 +74,19 @@ export interface NodeThemeSpecifications {
|
||||
dataTypeField?: string;
|
||||
palette?: string[] | { [dataTypeValue: string]: string };
|
||||
styles?: NodeStyleSets;
|
||||
zoomStrategy?: ZoomStrategy;
|
||||
lodStrategy?: LodStrategy;
|
||||
}
|
||||
export interface EdgeThemeSpecifications {
|
||||
dataTypeField?: string;
|
||||
palette?: string[] | { [dataTypeValue: string]: string };
|
||||
styles?: EdgeStyleSets;
|
||||
zoomStrategy?: ZoomStrategy;
|
||||
lodStrategy?: LodStrategy;
|
||||
}
|
||||
export interface ComboThemeSpecifications {
|
||||
dataTypeField?: string;
|
||||
palette?: string[] | { [dataTypeValue: string]: string };
|
||||
styles?: ComboStyleSets;
|
||||
zoomStrategy?: ZoomStrategy;
|
||||
lodStrategy?: LodStrategy;
|
||||
}
|
||||
/**
|
||||
* Theme specification
|
||||
|
@ -13,10 +13,11 @@ export type GraphAlignment =
|
||||
| [number, number];
|
||||
|
||||
export type GraphTransformOptions = {
|
||||
translate?: {
|
||||
translate?: Partial<{
|
||||
dx: number;
|
||||
dy: number;
|
||||
};
|
||||
dz: number;
|
||||
}>;
|
||||
rotate?: {
|
||||
angle: number;
|
||||
};
|
||||
|
@ -275,6 +275,7 @@ const runAnimateOnShape = (
|
||||
beginStyle: ShapeStyle,
|
||||
animateConfig,
|
||||
) => {
|
||||
if (!shape.isVisible()) return;
|
||||
let animateArr;
|
||||
if (!fields?.length) {
|
||||
animateArr = getStyleDiff(shape.attributes, targetStyle);
|
||||
@ -284,7 +285,8 @@ const runAnimateOnShape = (
|
||||
animateArr[0][key] = shape.attributes.hasOwnProperty(key)
|
||||
? shape.style[key]
|
||||
: beginStyle[key];
|
||||
animateArr[1][key] = targetStyle[key];
|
||||
animateArr[1][key] =
|
||||
targetStyle[key] === undefined ? animateArr[0][key] : targetStyle[key];
|
||||
if (key === 'lineDash' && animateArr[1][key].includes('100%')) {
|
||||
const totalLength = (shape as Line | Polyline | Path).getTotalLength();
|
||||
replaceElements(animateArr[1][key], '100%', totalLength);
|
||||
@ -329,7 +331,7 @@ export const animateShapes = (
|
||||
let i = 0;
|
||||
const groupKeys = Object.keys(timingAnimateGroups);
|
||||
if (!groupKeys.length) return;
|
||||
let animations = [];
|
||||
const animations = [];
|
||||
let canceled = false;
|
||||
const onfinish = () => {
|
||||
if (i >= groupKeys.length) {
|
||||
@ -348,11 +350,8 @@ export const animateShapes = (
|
||||
).filter(Boolean);
|
||||
groupAnimations.forEach((animation) => {
|
||||
animation.onframe = onAnimatesFrame;
|
||||
animations.push(animation);
|
||||
});
|
||||
if (i === 0) {
|
||||
// collect the first group animations
|
||||
animations = groupAnimations;
|
||||
}
|
||||
i++;
|
||||
};
|
||||
onfinish();
|
||||
@ -408,7 +407,12 @@ export const getAnimatesExcludePosition = (animates) => {
|
||||
export const fadeIn = (id, shape, style, hiddenShape, animateConfig) => {
|
||||
// omit inexist shape and the shape which is not hidden by zoom changing
|
||||
if (!shape || !hiddenShape[id]) return;
|
||||
shape.show();
|
||||
if (!shape?.isVisible()) {
|
||||
shape.style.opacity = 0;
|
||||
shape.show();
|
||||
}
|
||||
const { opacity: oriOpacity = 1 } = shape.attributes;
|
||||
if (oriOpacity === 1) return;
|
||||
const { opacity = 1 } = style;
|
||||
shape.animate([{ opacity: 0 }, { opacity }], animateConfig);
|
||||
};
|
||||
@ -421,3 +425,13 @@ export const fadeOut = (id, shape, hiddenShape, animateConfig) => {
|
||||
const animation = shape.animate([{ opacity }, { opacity: 0 }], animateConfig);
|
||||
animation.onfinish = () => shape.hide();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the animation to the end frame and clear it from the target shape.
|
||||
* @param animation
|
||||
*/
|
||||
export const stopAnimate = (animation) => {
|
||||
const timing = animation.effect.getTiming();
|
||||
animation.currentTime = Number(timing.duration) + Number(timing.delay || 0);
|
||||
animation.cancel();
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export const createCanvas = (
|
||||
pixelRatio?: number,
|
||||
customCanvasTag = true,
|
||||
style: any = {},
|
||||
) => {
|
||||
): Canvas => {
|
||||
let renderer;
|
||||
switch (rendererType.toLowerCase()) {
|
||||
case 'svg':
|
||||
@ -65,3 +65,31 @@ export const createCanvas = (
|
||||
renderer,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change renderer type for the canvas.
|
||||
* @param rendererType renderer type
|
||||
* @param canvas Canvas instance
|
||||
* @returns
|
||||
*/
|
||||
export const changeRenderer = (
|
||||
rendererType: RendererName,
|
||||
canvas: Canvas,
|
||||
): Canvas => {
|
||||
let renderer;
|
||||
switch (rendererType.toLowerCase()) {
|
||||
case 'svg':
|
||||
renderer = new SVGRenderer();
|
||||
break;
|
||||
case 'webgl-3d':
|
||||
case 'webgl':
|
||||
renderer = new WebGLRenderer();
|
||||
renderer.registerPlugin(new Plugin3D());
|
||||
break;
|
||||
default:
|
||||
renderer = new CanvasRenderer();
|
||||
break;
|
||||
}
|
||||
canvas.setRenderer(renderer);
|
||||
return canvas;
|
||||
};
|
||||
|
@ -14,15 +14,17 @@ import registry from '../stdlib';
|
||||
export const extend = <
|
||||
B1 extends BehaviorRegistry,
|
||||
B2 extends BehaviorRegistry,
|
||||
T extends ThemeRegistry = ThemeRegistry,
|
||||
T1 extends ThemeRegistry,
|
||||
T2 extends ThemeRegistry,
|
||||
>(
|
||||
GraphClass: typeof Graph<B2, T>,
|
||||
GraphClass: typeof Graph<B2, T2>,
|
||||
extendLibrary: {
|
||||
behaviors?: B1;
|
||||
themeSolvers?: T1;
|
||||
nodes?: any; // TODO
|
||||
edges?: any; // TODO
|
||||
},
|
||||
): typeof Graph<B1 & B2, T> => {
|
||||
): typeof Graph<B1 & B2, T1 & T2> => {
|
||||
// merged the extendLibrary to useLib for global usage
|
||||
Object.keys(extendLibrary).forEach((cat) => {
|
||||
registry.useLib[cat] = Object.assign(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isFunction } from 'util';
|
||||
import { isFunction } from '@antv/util';
|
||||
import { StdLibCategory } from '../types/stdlib';
|
||||
|
||||
/**
|
||||
|
@ -36,8 +36,9 @@ export const upsertTransientItem = (
|
||||
return transientItem;
|
||||
}
|
||||
if (item.type === 'node') {
|
||||
const transientNode = item.clone(nodeGroup, onlyDrawKeyShape, true);
|
||||
const transientNode = item.clone(nodeGroup, onlyDrawKeyShape, true) as Node;
|
||||
transientItemMap[item.model.id] = transientNode;
|
||||
transientNode.transient = true;
|
||||
return transientNode;
|
||||
} else if (item.type === 'edge') {
|
||||
const source = upsertTransientItem(
|
||||
@ -60,7 +61,8 @@ export const upsertTransientItem = (
|
||||
target,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
) as Edge;
|
||||
transientEdge.transient = true;
|
||||
transientItemMap[item.model.id] = transientEdge;
|
||||
return transientEdge;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
AABB,
|
||||
} from '@antv/g';
|
||||
import { clone, isArray, isNumber } from '@antv/util';
|
||||
import { DEFAULT_LABEL_BG_PADDING } from '../constant';
|
||||
import { DEFAULT_LABEL_BG_PADDING, RESERVED_SHAPE_IDS } from '../constant';
|
||||
import { Point } from '../types/common';
|
||||
import { EdgeDisplayModel, EdgeShapeMap } from '../types/edge';
|
||||
import {
|
||||
@ -29,6 +29,7 @@ import { ComboDisplayModel } from '../types';
|
||||
import { getShapeAnimateBeginStyles } from './animate';
|
||||
import { isArrayOverlap } from './array';
|
||||
import { isBetween } from './math';
|
||||
import { getZoomLevel } from './zoom';
|
||||
|
||||
export const ShapeTagMap = {
|
||||
circle: Circle,
|
||||
@ -129,7 +130,9 @@ export const upsertShape = (
|
||||
const updateStyles = {};
|
||||
const oldStyles = shape.attributes;
|
||||
// update
|
||||
if (disableAnimate || !animates?.update) {
|
||||
// update the styles excludes the ones in the animate fields
|
||||
const animateFields = findAnimateFields(animates, 'update', id);
|
||||
if (disableAnimate || !animates?.update || !animateFields.length) {
|
||||
// update all the style directly when there are no animates for update timing
|
||||
Object.keys(style).forEach((key) => {
|
||||
if (oldStyles[key] !== style[key]) {
|
||||
@ -138,9 +141,6 @@ export const upsertShape = (
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// update the styles excludes the ones in the animate fields
|
||||
const animateFields = findAnimateFields(animates, 'update', id);
|
||||
if (!animateFields.length) return shape;
|
||||
Object.keys(style).forEach((key) => {
|
||||
if (oldStyles[key] !== style[key]) {
|
||||
updateStyles[key] = style[key];
|
||||
@ -292,7 +292,7 @@ const merge2Styles = (
|
||||
export const isPolygonsIntersect = (
|
||||
points1: number[][],
|
||||
points2: number[][],
|
||||
): boolean => {
|
||||
): Boolean => {
|
||||
const getBBox = (points): Partial<AABB> => {
|
||||
const xArr = points.map((p) => p[0]);
|
||||
const yArr = points.map((p) => p[1]);
|
||||
@ -532,3 +532,78 @@ export const isStyleAffectBBox = (
|
||||
) => {
|
||||
return isArrayOverlap(Object.keys(style), FEILDS_AFFECT_BBOX[type]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Estimate the width of the shape according to the given style.
|
||||
* @param shape target shape
|
||||
* @param style computed merged style
|
||||
* @param bounds shape's local bounds
|
||||
* @returns
|
||||
*/
|
||||
export const getShapeLocalBoundsByStyle = (
|
||||
shape: DisplayObject,
|
||||
style: ShapeStyle,
|
||||
bbox?: AABB,
|
||||
): {
|
||||
min: number[];
|
||||
max: number[];
|
||||
center: number[];
|
||||
} => {
|
||||
const {
|
||||
r,
|
||||
rx,
|
||||
ry,
|
||||
width,
|
||||
height,
|
||||
depth = 0,
|
||||
x1,
|
||||
x2,
|
||||
y1,
|
||||
y2,
|
||||
z1 = 0,
|
||||
z2 = 0,
|
||||
} = style;
|
||||
const radius = Number(r);
|
||||
const radiusX = Number(rx);
|
||||
const radiusY = Number(ry);
|
||||
switch (shape.nodeName) {
|
||||
case 'circle':
|
||||
return {
|
||||
min: [-radius, -radius, 0],
|
||||
max: [radius, radius, 0],
|
||||
center: [0, 0, 0],
|
||||
};
|
||||
case 'sphere':
|
||||
return {
|
||||
min: [-radius, -radius, -radius],
|
||||
max: [radius, radius, radius],
|
||||
center: [0, 0, 0],
|
||||
};
|
||||
case 'image':
|
||||
case 'rect':
|
||||
case 'cube':
|
||||
case 'plane':
|
||||
return {
|
||||
min: [-width / 2, -height / 2, -depth / 2],
|
||||
max: [width / 2, height / 2, depth / 2],
|
||||
center: [0, 0, 0],
|
||||
};
|
||||
case 'ellipse':
|
||||
return {
|
||||
min: [-radiusX, -radiusY, 0],
|
||||
max: [radiusX, radiusY, 0],
|
||||
center: [0, 0, 0],
|
||||
};
|
||||
case 'line':
|
||||
return {
|
||||
min: [x1, y1, z1],
|
||||
max: [x2, y2, z2],
|
||||
center: [(x1 + x2) / 2, (y1 + y2) / 2, (z1 + z2) / 2],
|
||||
};
|
||||
case 'text':
|
||||
case 'polyline':
|
||||
case 'path':
|
||||
case 'polygon':
|
||||
return bbox || shape.getLocalBounds();
|
||||
}
|
||||
};
|
||||
|
@ -92,6 +92,9 @@ export const createShape3D = (
|
||||
id,
|
||||
});
|
||||
|
||||
// set origin for shape
|
||||
shape.setOrigin(0, 0, 0);
|
||||
|
||||
// Scale the shape to the correct size.
|
||||
switch (type) {
|
||||
case 'cube':
|
||||
|
@ -15,8 +15,7 @@ export const getWordWrapWidthByBox = (
|
||||
zoom = 1,
|
||||
) => {
|
||||
const keyShapeWidth = (keyShapeBox.max[0] - keyShapeBox.min[0]) * zoom;
|
||||
const wordWrapWidth = 2 * keyShapeWidth;
|
||||
return getWordWrapWidthWithBase(wordWrapWidth, maxWidth);
|
||||
return getWordWrapWidthWithBase(keyShapeWidth, maxWidth);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,20 +1,18 @@
|
||||
import { ZoomStrategy, ZoomStrategyObj } from '../types/item';
|
||||
import { LodStrategy, lodStrategyObj } from '../types/item';
|
||||
|
||||
/**
|
||||
* Format zoomStrategy to the pattern that ratio 1 (primary level) at level 0, and higher the ratio, higher the level.
|
||||
* @param zoomStrategy
|
||||
* Format lodStrategy to the pattern that ratio 1 (primary level) at level 0, and higher the ratio, higher the level.
|
||||
* @param lodStrategy
|
||||
* @returns
|
||||
*/
|
||||
export const formatZoomStrategy = (
|
||||
zoomStrategy: ZoomStrategy,
|
||||
): ZoomStrategyObj => {
|
||||
const { levels, animateCfg } = zoomStrategy || {};
|
||||
export const formatLodStrategy = (lodStrategy: LodStrategy): lodStrategyObj => {
|
||||
const { levels, animateCfg } = lodStrategy || {};
|
||||
if (!levels) return undefined;
|
||||
const primaryLevel = levels.find((level) => level.primary);
|
||||
const primaryIndex = levels.indexOf(primaryLevel);
|
||||
const formattedLevels = {};
|
||||
levels.forEach((level, i) => {
|
||||
formattedLevels[i - primaryIndex] = level.range;
|
||||
formattedLevels[i - primaryIndex] = level.zoomRange;
|
||||
});
|
||||
return {
|
||||
animateCfg,
|
||||
@ -34,8 +32,8 @@ export const getZoomLevel = (
|
||||
) => {
|
||||
let level = 0;
|
||||
Object.keys(levels).forEach((idx) => {
|
||||
const range = levels[idx];
|
||||
if (zoom >= range[0] && zoom < range[1]) level = Number(idx);
|
||||
const zoomRange = levels[idx];
|
||||
if (zoom >= zoomRange[0] && zoom < zoomRange[1]) level = Number(idx);
|
||||
});
|
||||
return level;
|
||||
};
|
||||
|
6
packages/g6/tests/intergration/demo/data.ts
Normal file
6
packages/g6/tests/intergration/demo/data.ts
Normal file
File diff suppressed because one or more lines are too long
75047
packages/g6/tests/intergration/demo/data3d.ts
Normal file
75047
packages/g6/tests/intergration/demo/data3d.ts
Normal file
File diff suppressed because it is too large
Load Diff
765
packages/g6/tests/intergration/demo/demo.ts
Normal file
765
packages/g6/tests/intergration/demo/demo.ts
Normal file
@ -0,0 +1,765 @@
|
||||
import { initThreads, supportsThreads, ForceLayout } from '@antv/layout-wasm';
|
||||
// import G6, { Graph, GraphData } from '../../../esm';
|
||||
import G6, { Graph, GraphData } from '../../../src';
|
||||
import { container, height, width } from '../../datasets/const';
|
||||
import data from './data';
|
||||
import data3d from './data3d';
|
||||
import { labelPropagation } from '@antv/algorithm';
|
||||
import { RendererName } from '../../../src/types/render';
|
||||
import { Point } from '../../../src/types/common';
|
||||
// import Stats from 'stats.js';
|
||||
|
||||
let graph: typeof Graph;
|
||||
let degrees = {};
|
||||
let dataFor2D: GraphData = { nodes: [], edges: [] };
|
||||
let dataFor3D: GraphData = { nodes: [], edges: [] };
|
||||
let colorSelects = [];
|
||||
const { nodes, edges } = data;
|
||||
export { nodes, edges, degrees };
|
||||
|
||||
const getDefaultNodeAnimates = (delay?: number) => ({
|
||||
buildIn: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 1000,
|
||||
delay: delay === undefined ? 1000 + Math.random() * 1000 : delay,
|
||||
},
|
||||
],
|
||||
buildOut: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
fields: ['fill', 'r', 'lineWidth'],
|
||||
shapeId: 'keyShape',
|
||||
duration: 500,
|
||||
},
|
||||
{
|
||||
fields: ['fontSize'],
|
||||
shapeId: 'iconShape',
|
||||
},
|
||||
{
|
||||
fields: ['opacity'],
|
||||
shapeId: 'haloShape',
|
||||
},
|
||||
],
|
||||
hide: [
|
||||
{
|
||||
fields: ['size'],
|
||||
duration: 200,
|
||||
},
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
shapeId: 'keyShape',
|
||||
},
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
shapeId: 'labelShape',
|
||||
},
|
||||
],
|
||||
show: [
|
||||
{
|
||||
fields: ['size'],
|
||||
duration: 200,
|
||||
},
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
shapeId: 'keyShape',
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const getDefaultEdgeAnimates = (delay?: number) => ({
|
||||
buildIn: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 300,
|
||||
delay: delay === undefined ? 1000 + Math.random() * 1000 : delay,
|
||||
},
|
||||
],
|
||||
buildOut: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
fields: ['lineWidth'],
|
||||
shapeId: 'keyShape',
|
||||
},
|
||||
{
|
||||
fields: ['opacity'],
|
||||
shapeId: 'haloShape',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const defaultTheme = {
|
||||
// : ThemeOptionsOf<any>
|
||||
type: 'spec',
|
||||
base: 'light',
|
||||
specification: {
|
||||
node: {
|
||||
dataTypeField: 'cluster',
|
||||
},
|
||||
},
|
||||
};
|
||||
let currentTheme = defaultTheme;
|
||||
|
||||
const create2DGraph = (
|
||||
getNodeAnimates = getDefaultNodeAnimates,
|
||||
getEdgeAnimates = getDefaultEdgeAnimates,
|
||||
theme = defaultTheme,
|
||||
rendererType: RendererName = 'canvas',
|
||||
) => {
|
||||
const graph = new Graph({
|
||||
container: container as HTMLElement,
|
||||
width,
|
||||
height: 1400,
|
||||
type: 'graph',
|
||||
renderer: rendererType,
|
||||
data: dataFor2D,
|
||||
modes: {
|
||||
default: [
|
||||
{ type: 'zoom-canvas', key: '123', triggerOnItems: true },
|
||||
'drag-node',
|
||||
'drag-canvas',
|
||||
'hover-activate',
|
||||
'brush-select',
|
||||
'click-select',
|
||||
],
|
||||
},
|
||||
theme: { ...defaultTheme, ...theme },
|
||||
edge: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
type: 'line-edge',
|
||||
animates: getEdgeAnimates(),
|
||||
},
|
||||
};
|
||||
},
|
||||
// 节点配置
|
||||
node: (innerModel) => {
|
||||
const degree = degrees[innerModel.id] || 0;
|
||||
let labelLod = 3;
|
||||
if (degree > 40) labelLod = -2;
|
||||
else if (degree > 20) labelLod = -1;
|
||||
else if (degree > 10) labelLod = 0;
|
||||
else if (degree > 5) labelLod = 1;
|
||||
else if (degree > 2) labelLod = 2;
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
animates: getNodeAnimates(),
|
||||
...innerModel.data,
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ zoomRange: [0, 0.16] }, // -2
|
||||
{ zoomRange: [0.16, 0.2] }, // -1
|
||||
{ zoomRange: [0.2, 0.3], primary: true }, // 0
|
||||
{ zoomRange: [0.3, 0.5] }, // 1
|
||||
{ zoomRange: [0.5, 0.8] }, // 2
|
||||
{ zoomRange: [0.8, 1.5] }, // 3
|
||||
{ zoomRange: [1.5, 1.8] }, // 4
|
||||
{ zoomRange: [1.8, 2] }, // 5
|
||||
{ zoomRange: [2, Infinity] }, // 6
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 500,
|
||||
},
|
||||
},
|
||||
labelShape:
|
||||
degree !== 0
|
||||
? {
|
||||
text: innerModel.data.label,
|
||||
maxWidth: '400%',
|
||||
offsetY: 8,
|
||||
lod: labelLod,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
labelBackgroundShape:
|
||||
degree !== 0
|
||||
? {
|
||||
lod: labelLod,
|
||||
}
|
||||
: undefined,
|
||||
iconShape:
|
||||
degree !== 0
|
||||
? {
|
||||
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
fontSize: 12 + degree / 4,
|
||||
opacity: 0.8,
|
||||
lod: labelLod + 2,
|
||||
}
|
||||
: undefined,
|
||||
keyShape: {
|
||||
r: 12 + degree / 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
graph.zoom(0.15);
|
||||
return graph;
|
||||
};
|
||||
|
||||
const create3DGraph = async () => {
|
||||
G6.stdLib.layouts['force-wasm'] = ForceLayout;
|
||||
const supported = await supportsThreads();
|
||||
const threads = await initThreads(supported);
|
||||
const newGraph = new Graph({
|
||||
container: container as HTMLDivElement,
|
||||
width,
|
||||
height: 1400,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
data: dataFor3D,
|
||||
// layout: {
|
||||
// type: 'force-wasm',
|
||||
// threads,
|
||||
// dimensions: 2,
|
||||
// maxIteration: 5000,
|
||||
// minMovement: 0.1,
|
||||
// distanceThresholdMode: 'mean',
|
||||
// height,
|
||||
// width,
|
||||
// center: [width / 2, height / 2],
|
||||
// factor: 1,
|
||||
// gravity: 5,
|
||||
// linkDistance: 200,
|
||||
// edgeStrength: 200,
|
||||
// nodeStrength: 1000,
|
||||
// coulombDisScale: 0.005,
|
||||
// damping: 0.9,
|
||||
// maxSpeed: 2000,
|
||||
// interval: 0.02,
|
||||
// },
|
||||
// layout: {
|
||||
// type: 'force-wasm',
|
||||
// threads,
|
||||
// dimensions: 3,
|
||||
// iterations: 300,
|
||||
// minMovement: 10,
|
||||
// height,
|
||||
// width,
|
||||
// linkDistance: 200,
|
||||
// edgeStrength: 100,
|
||||
// nodeStrength: 2000,
|
||||
// center: [width / 2, height / 2, 0],
|
||||
// },
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'orbit-canvas-3d',
|
||||
trigger: 'drag',
|
||||
},
|
||||
'zoom-canvas-3d',
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
type: 'spec',
|
||||
base: 'dark',
|
||||
specification: {
|
||||
node: {
|
||||
dataTypeField: 'cluster',
|
||||
},
|
||||
},
|
||||
},
|
||||
edge: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
keyShape: {
|
||||
lineWidth: 0.6,
|
||||
opacity: 0.6,
|
||||
stroke: '#fff',
|
||||
},
|
||||
type: 'line-edge',
|
||||
},
|
||||
};
|
||||
},
|
||||
node: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
type: 'sphere-node',
|
||||
keyShape: {
|
||||
r: 12 + degrees[innerModel.id] / 2,
|
||||
},
|
||||
labelShape:
|
||||
degrees[innerModel.id] > 20
|
||||
? {
|
||||
text: innerModel.data.label,
|
||||
fontSize: 100,
|
||||
lod: -1,
|
||||
fill: 'rgba(255,255,255,0.85)',
|
||||
wordWrap: false, // FIXME: mesh.getBounds() returns an empty AABB
|
||||
isBillboard: true,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const rotate = (camera, rx: number, ry: number, graph) => {
|
||||
const { width, height } = graph.canvas.getConfig();
|
||||
const dx = 20.0 / height;
|
||||
const dy = 20.0 / width;
|
||||
let motionFactorX = 10;
|
||||
let motionFactorY = 10;
|
||||
if (rx * rx > 2 * ry * ry) {
|
||||
motionFactorY *= 0.5;
|
||||
} else if (ry * ry > 2 * rx * rx) {
|
||||
motionFactorX *= 0.5;
|
||||
}
|
||||
|
||||
const rotX = rx * dx * motionFactorX;
|
||||
const rotY = ry * dy * motionFactorY;
|
||||
|
||||
camera.rotate(rotX, 0);
|
||||
};
|
||||
|
||||
let timer;
|
||||
setTimeout(() => {
|
||||
const camera = newGraph.canvas.getCamera();
|
||||
const oripos = camera.getPosition();
|
||||
let k = 0;
|
||||
let i = 0;
|
||||
const tick = () => {
|
||||
camera.setPosition([oripos[0], oripos[1], oripos[2] + k]);
|
||||
const rdx =
|
||||
i < 100 ? Math.min(i * 0.5, 20) : Math.min((200 - i) * 0.2, 20);
|
||||
rotate(camera, rdx, rdx, newGraph);
|
||||
timer = requestAnimationFrame(tick);
|
||||
if (i > 200) cancelAnimationFrame(timer);
|
||||
const param = i < 50 ? 3 : 0.5;
|
||||
k += 50 * param;
|
||||
i++;
|
||||
};
|
||||
tick();
|
||||
}, 1000);
|
||||
newGraph.once('canvas:pointerdown', (e) => {
|
||||
if (timer) cancelAnimationFrame(timer);
|
||||
});
|
||||
newGraph.once('wheel', (e) => {
|
||||
if (timer) cancelAnimationFrame(timer);
|
||||
});
|
||||
// });
|
||||
|
||||
return newGraph;
|
||||
};
|
||||
|
||||
const generateColorSelect = (id, container) => {
|
||||
const colorSelect = document.createElement('input');
|
||||
colorSelect.style.width = '25px';
|
||||
colorSelect.style.height = '25px';
|
||||
colorSelect.style.border = '0';
|
||||
colorSelect.style.background = 'rgba(0, 0, 0, 0)';
|
||||
colorSelect.type = 'color';
|
||||
colorSelect.id = `color-${id}`;
|
||||
colorSelect.value = id === 'bg' ? '#ffffff' : '#cccccc';
|
||||
container.appendChild(colorSelect);
|
||||
return colorSelect;
|
||||
};
|
||||
|
||||
const addButtons = () => {
|
||||
const btn = document.createElement('button');
|
||||
btn.innerHTML = '全屏';
|
||||
btn.style.position = 'absolute';
|
||||
btn.style.top = '56px';
|
||||
btn.style.left = '16px';
|
||||
btn.style.zIndex = '100';
|
||||
document.body.appendChild(btn);
|
||||
btn.addEventListener('click', (e) => {
|
||||
const canvasEl = graph.canvas.context.config.canvas;
|
||||
const requestMethod =
|
||||
canvasEl.requestFullScreen ||
|
||||
canvasEl.webkitRequestFullScreen ||
|
||||
canvasEl.mozRequestFullScreen ||
|
||||
canvasEl.msRequestFullScreen;
|
||||
if (requestMethod) {
|
||||
// Native full screen.
|
||||
requestMethod.call(canvasEl);
|
||||
} else if (typeof window.ActiveXObject !== 'undefined') {
|
||||
// Older IE.
|
||||
const wscript = new ActiveXObject('WScript.Shell');
|
||||
if (wscript !== null) {
|
||||
wscript.SendKeys('{F11}');
|
||||
}
|
||||
}
|
||||
});
|
||||
const btnZoomIn = document.createElement('button');
|
||||
btnZoomIn.innerHTML = '放大';
|
||||
btnZoomIn.style.position = 'absolute';
|
||||
btnZoomIn.style.top = '56px';
|
||||
btnZoomIn.style.left = '62px';
|
||||
btnZoomIn.style.width = '48px';
|
||||
btnZoomIn.style.zIndex = '100';
|
||||
document.body.appendChild(btnZoomIn);
|
||||
const btnZoomOut = document.createElement('button');
|
||||
btnZoomOut.innerHTML = '缩小';
|
||||
btnZoomOut.style.position = 'absolute';
|
||||
btnZoomOut.style.top = '56px';
|
||||
btnZoomOut.style.left = '112px';
|
||||
btnZoomOut.style.width = '48px';
|
||||
btnZoomOut.style.zIndex = '100';
|
||||
document.body.appendChild(btnZoomOut);
|
||||
|
||||
const rendererSelect = document.createElement('select');
|
||||
rendererSelect.style.position = 'absolute';
|
||||
rendererSelect.style.top = '86px';
|
||||
rendererSelect.style.left = '16px';
|
||||
rendererSelect.style.width = '143px';
|
||||
rendererSelect.style.height = '25px';
|
||||
rendererSelect.style.zIndex = '100';
|
||||
const option1 = document.createElement('option');
|
||||
option1.innerHTML = 'Canvas';
|
||||
const option2 = document.createElement('option');
|
||||
option2.innerHTML = 'WebGL';
|
||||
const option3 = document.createElement('option');
|
||||
option3.innerHTML = 'WebGL-3D';
|
||||
const option4 = document.createElement('option');
|
||||
option4.innerHTML = 'SVG(coming soon)';
|
||||
option4.disabled = true;
|
||||
rendererSelect.appendChild(option1);
|
||||
rendererSelect.appendChild(option2);
|
||||
rendererSelect.appendChild(option3);
|
||||
rendererSelect.appendChild(option4);
|
||||
document.body.appendChild(rendererSelect);
|
||||
|
||||
const themeSelect = document.createElement('select');
|
||||
themeSelect.style.position = 'absolute';
|
||||
themeSelect.style.top = '116px';
|
||||
themeSelect.style.left = '16px';
|
||||
themeSelect.style.width = '143px';
|
||||
themeSelect.style.height = '25px';
|
||||
themeSelect.style.zIndex = '100';
|
||||
const themeOption0 = document.createElement('option');
|
||||
themeOption0.innerHTML = '亮色主题';
|
||||
const themeOption1 = document.createElement('option');
|
||||
themeOption1.innerHTML = '暗色主题';
|
||||
const themeOption2 = document.createElement('option');
|
||||
themeOption2.innerHTML = '蓝色主题';
|
||||
const themeOption3 = document.createElement('option');
|
||||
themeOption3.innerHTML = '橙色主题';
|
||||
const themeOption4 = document.createElement('option');
|
||||
themeOption4.innerHTML = '自定义';
|
||||
themeSelect.appendChild(themeOption0);
|
||||
themeSelect.appendChild(themeOption1);
|
||||
themeSelect.appendChild(themeOption2);
|
||||
themeSelect.appendChild(themeOption3);
|
||||
themeSelect.appendChild(themeOption4);
|
||||
document.body.appendChild(themeSelect);
|
||||
|
||||
// 自定义色板
|
||||
const customThemeSelect = document.createElement('div');
|
||||
const paletteContainer = document.createElement('div');
|
||||
paletteContainer.style.display = 'inline-flex';
|
||||
customThemeSelect.appendChild(paletteContainer);
|
||||
|
||||
const addColorBtn = document.createElement('a');
|
||||
addColorBtn.innerHTML = '+';
|
||||
addColorBtn.style.margin = '4px';
|
||||
addColorBtn.style.cursor = 'pointer';
|
||||
addColorBtn.style.border = '1px dashed rgba(34, 126, 255, 0.5)';
|
||||
addColorBtn.style.padding = '2px 8px';
|
||||
addColorBtn.style.color = 'rgb(34, 126, 255)';
|
||||
paletteContainer.appendChild(addColorBtn);
|
||||
addColorBtn.addEventListener('click', (e) => {
|
||||
colorSelects.push(
|
||||
generateColorSelect(`${colorSelects.length}`, colorsContainer),
|
||||
);
|
||||
});
|
||||
|
||||
const colorsContainer = document.createElement('div');
|
||||
colorsContainer.style.display = 'inline-flex';
|
||||
colorsContainer.style.margin = '4px 0';
|
||||
paletteContainer.appendChild(colorsContainer);
|
||||
colorSelects = [generateColorSelect('0', colorsContainer)];
|
||||
|
||||
const removeColorBtn = document.createElement('a');
|
||||
removeColorBtn.innerHTML = '-';
|
||||
removeColorBtn.style.margin = '4px';
|
||||
removeColorBtn.style.cursor = 'pointer';
|
||||
removeColorBtn.style.border = '1px dashed rgba(34, 126, 255, 0.5)';
|
||||
removeColorBtn.style.padding = '2px 10px';
|
||||
removeColorBtn.style.color = 'rgb(34, 126, 255)';
|
||||
paletteContainer.appendChild(removeColorBtn);
|
||||
removeColorBtn.addEventListener('click', (e) => {
|
||||
if (colorSelects.length <= 1) return;
|
||||
const removingSelect = colorSelects.splice(colorSelects.length - 1, 1)[0];
|
||||
removingSelect.remove();
|
||||
});
|
||||
const backgroundColorContainer = document.createElement('div');
|
||||
backgroundColorContainer.style.margin = '8px 0';
|
||||
const backgroundLabel = document.createElement('div');
|
||||
backgroundLabel.innerHTML = '背景色:';
|
||||
backgroundLabel.style.display = 'inline-flex';
|
||||
backgroundLabel.style.fontSize = '14px';
|
||||
backgroundColorContainer.appendChild(backgroundLabel);
|
||||
const bgColorSelect = generateColorSelect('bg', colorsContainer);
|
||||
backgroundColorContainer.appendChild(bgColorSelect);
|
||||
bgColorSelect.style.display = 'inline-flex';
|
||||
customThemeSelect.appendChild(backgroundColorContainer);
|
||||
|
||||
const customConfirmBtn = document.createElement('button');
|
||||
customConfirmBtn.innerHTML = '应用';
|
||||
customConfirmBtn.style.cursor = 'pointer';
|
||||
customConfirmBtn.style.width = '109px';
|
||||
customConfirmBtn.style.border = '0';
|
||||
customConfirmBtn.style.backgroundColor = 'rgba(34, 126, 255, 0.5)';
|
||||
customThemeSelect.appendChild(customConfirmBtn);
|
||||
customConfirmBtn.addEventListener('click', (e) => {
|
||||
graph.updateTheme({
|
||||
type: 'spec',
|
||||
specification: {
|
||||
canvas: {
|
||||
backgroundColor: bgColorSelect.value || '#fff',
|
||||
},
|
||||
node: {
|
||||
dataTypeField: 'cluster',
|
||||
palette: colorSelects.map((dom) => dom.value),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
customThemeSelect.style.position = 'absolute';
|
||||
customThemeSelect.style.display = 'none';
|
||||
customThemeSelect.style.top = '146px';
|
||||
customThemeSelect.style.left = '16px';
|
||||
customThemeSelect.style.zIndex = '100';
|
||||
customThemeSelect.style.padding = '8px';
|
||||
customThemeSelect.style.backgroundColor = 'rgb(212, 230, 255)';
|
||||
document.body.appendChild(customThemeSelect);
|
||||
|
||||
return {
|
||||
rendererSelect,
|
||||
themeSelect,
|
||||
customThemeSelect,
|
||||
zoomIn: btnZoomIn,
|
||||
zoomOut: btnZoomOut,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSwitchRenderer = (rendererName, oldgraph) => {
|
||||
switch (rendererName) {
|
||||
case 'webgl-3d':
|
||||
oldgraph.destroy(async () => {
|
||||
graph = await create3DGraph();
|
||||
});
|
||||
break;
|
||||
case 'canvas':
|
||||
oldgraph.destroy(() => {
|
||||
graph = create2DGraph(undefined, undefined, currentTheme);
|
||||
});
|
||||
break;
|
||||
case 'webgl':
|
||||
oldgraph.destroy(() => {
|
||||
const currentZoom = oldgraph.getZoom();
|
||||
const position = oldgraph.canvas.getCamera().getPosition();
|
||||
const zoomOpt = {
|
||||
zoom: currentZoom,
|
||||
center: { x: position[0], y: position[1] },
|
||||
};
|
||||
graph = create2DGraph(undefined, undefined, currentTheme, 'webgl');
|
||||
});
|
||||
// oldgraph.changeRenderer('webgl');
|
||||
break;
|
||||
case 'svg':
|
||||
// comming soon
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return graph;
|
||||
};
|
||||
|
||||
const handleSwitchTheme = (themeType, customThemeSelect) => {
|
||||
customThemeSelect.style.display = 'none';
|
||||
switch (themeType) {
|
||||
case '亮色主题':
|
||||
currentTheme = defaultTheme;
|
||||
graph.updateTheme(defaultTheme);
|
||||
return;
|
||||
case '暗色主题':
|
||||
currentTheme = {
|
||||
...defaultTheme,
|
||||
base: 'dark',
|
||||
};
|
||||
graph.updateTheme(currentTheme);
|
||||
return;
|
||||
case '蓝色主题':
|
||||
currentTheme = {
|
||||
type: 'spec',
|
||||
base: 'light',
|
||||
specification: {
|
||||
canvas: {
|
||||
backgroundColor: '#f3faff',
|
||||
},
|
||||
node: {
|
||||
dataTypeField: 'cluster',
|
||||
palette: [
|
||||
'#bae0ff',
|
||||
'#91caff',
|
||||
'#69b1ff',
|
||||
'#4096ff',
|
||||
'#1677ff',
|
||||
'#0958d9',
|
||||
'#003eb3',
|
||||
'#002c8c',
|
||||
'#001d66',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
graph.updateTheme(currentTheme);
|
||||
return;
|
||||
case '橙色主题':
|
||||
currentTheme = {
|
||||
type: 'spec',
|
||||
base: 'light',
|
||||
specification: {
|
||||
canvas: {
|
||||
backgroundColor: '#fcf9f1',
|
||||
},
|
||||
node: {
|
||||
dataTypeField: 'cluster',
|
||||
palette: [
|
||||
'#ffe7ba',
|
||||
'#ffd591',
|
||||
'#ffc069',
|
||||
'#ffa940',
|
||||
'#fa8c16',
|
||||
'#d46b08',
|
||||
'#ad4e00',
|
||||
'#873800',
|
||||
'#612500',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
graph.updateTheme(currentTheme);
|
||||
return;
|
||||
case '自定义':
|
||||
customThemeSelect.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const zoomLevels = [0.15, 0.16, 0.2, 0.3, 0.5, 0.8, 1.5, 2];
|
||||
const handleZoom = (graph, isIn = true) => {
|
||||
let toZoom = 0.15;
|
||||
const currentZoom = graph.getZoom();
|
||||
if (isIn) {
|
||||
for (let i = 0; i < zoomLevels.length - 1; i++) {
|
||||
if (currentZoom <= zoomLevels[i]) {
|
||||
toZoom = zoomLevels[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = zoomLevels.length - 1; i >= 1; i--) {
|
||||
if (currentZoom >= zoomLevels[i]) {
|
||||
toZoom = zoomLevels[i - 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
graph.zoomTo(toZoom, { x: 2194, y: -1347 }, { duration: 500 });
|
||||
};
|
||||
|
||||
const getDataFor2D = (inputData) => {
|
||||
const clusteredData = labelPropagation(inputData, false);
|
||||
clusteredData.clusters.forEach((cluster, i) => {
|
||||
cluster.nodes.forEach((node) => {
|
||||
node.data.cluster = `c${i}`;
|
||||
});
|
||||
});
|
||||
// for 性能测试
|
||||
// data.nodes.forEach((node) => {
|
||||
// delete node.data.x;
|
||||
// delete node.data.y;
|
||||
// delete node.data.z;
|
||||
// });
|
||||
const degrees = {};
|
||||
inputData.edges.forEach((edge) => {
|
||||
const { source, target } = edge;
|
||||
degrees[source] = degrees[source] || 0;
|
||||
degrees[target] = degrees[target] || 0;
|
||||
degrees[source]++;
|
||||
degrees[target]++;
|
||||
});
|
||||
inputData.nodes.forEach((node) => delete node.data.z);
|
||||
return { degrees, data: inputData };
|
||||
};
|
||||
|
||||
const getDataFor3D = (inputData) => {
|
||||
const clusteredData3D = labelPropagation(inputData, false);
|
||||
clusteredData3D.clusters.forEach((cluster, i) => {
|
||||
cluster.nodes.forEach((node) => {
|
||||
node.data.cluster = `c${i}`;
|
||||
});
|
||||
});
|
||||
// data3d.nodes.forEach((node) => {
|
||||
// delete node.data.x;
|
||||
// delete node.data.y;
|
||||
// delete node.data.z;
|
||||
// });
|
||||
|
||||
return inputData;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const result2d = getDataFor2D(data);
|
||||
degrees = result2d.degrees;
|
||||
dataFor2D = result2d.data;
|
||||
dataFor3D = getDataFor3D(data3d);
|
||||
|
||||
graph = create2DGraph();
|
||||
const { rendererSelect, themeSelect, customThemeSelect, zoomIn, zoomOut } =
|
||||
addButtons();
|
||||
|
||||
rendererSelect.addEventListener('change', (e: any) => {
|
||||
const type = e.target.value;
|
||||
console.log('changerenderer', graph);
|
||||
handleSwitchRenderer(type.toLowerCase(), graph);
|
||||
// graph.changeRenderer(type.toLowerCase());
|
||||
});
|
||||
themeSelect.addEventListener('change', (e: any) => {
|
||||
const type = e.target.value;
|
||||
handleSwitchTheme(type, customThemeSelect);
|
||||
});
|
||||
zoomIn.addEventListener('click', () => handleZoom(graph, true));
|
||||
zoomOut.addEventListener('click', () => handleZoom(graph, false));
|
||||
|
||||
// stats
|
||||
// const stats = new Stats();
|
||||
// stats.showPanel(0);
|
||||
// const $stats = stats.dom;
|
||||
// $stats.style.position = 'absolute';
|
||||
// $stats.style.left = '0px';
|
||||
// $stats.style.top = '0px';
|
||||
// document.body.appendChild($stats);
|
||||
// graph.canvas.addEventListener('afterrender', () => {
|
||||
// if (stats) {
|
||||
// stats.update();
|
||||
// }
|
||||
// });
|
||||
|
||||
return graph;
|
||||
};
|
125
packages/g6/tests/intergration/demo/demoFor4.ts
Normal file
125
packages/g6/tests/intergration/demo/demoFor4.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import G6 from '@antv/G6';
|
||||
import { container, width } from '../../datasets/const';
|
||||
import data from './data';
|
||||
import { labelPropagation } from '@antv/algorithm';
|
||||
import Stats from 'stats.js';
|
||||
|
||||
const nodeIds = [];
|
||||
|
||||
const edges = data.edges.map((edge) => {
|
||||
return {
|
||||
id: `edge-${Math.random()}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
data: {},
|
||||
};
|
||||
});
|
||||
|
||||
const degrees = {};
|
||||
edges.forEach((edge) => {
|
||||
const { source, target } = edge;
|
||||
degrees[source] = degrees[source] || 0;
|
||||
degrees[target] = degrees[target] || 0;
|
||||
degrees[source]++;
|
||||
degrees[target]++;
|
||||
});
|
||||
|
||||
const nodes = data.nodes
|
||||
.map((node) => {
|
||||
const id = node.id || `node-${Math.random()}`;
|
||||
if (nodeIds.includes(id)) return;
|
||||
nodeIds.push(id);
|
||||
return {
|
||||
id: id as string,
|
||||
// label: node.olabel,
|
||||
x: node.x * 10 - 3000,
|
||||
y: node.y * 10 - 5000,
|
||||
icon: {
|
||||
show: true,
|
||||
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
width: 12 + degrees[id] / 4,
|
||||
height: 12 + degrees[id] / 4,
|
||||
opacity: 0.8,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const clusteredData = labelPropagation({ nodes, edges }, false);
|
||||
|
||||
const subjectColors = [
|
||||
'#5F95FF', // blue
|
||||
'#61DDAA',
|
||||
'#65789B',
|
||||
'#F6BD16',
|
||||
'#7262FD',
|
||||
'#78D3F8',
|
||||
'#9661BC',
|
||||
'#F6903D',
|
||||
'#008685',
|
||||
'#F08BB4',
|
||||
];
|
||||
const colorSets = G6.Util.getColorSetsBySubjectColors(
|
||||
subjectColors,
|
||||
'#fff',
|
||||
'default',
|
||||
'#777',
|
||||
);
|
||||
|
||||
clusteredData.clusters.forEach((cluster, i) => {
|
||||
const colorSet = colorSets[i % colorSets.length];
|
||||
cluster.nodes.forEach((node) => {
|
||||
node.cluster = `c${i}`;
|
||||
node.style = {
|
||||
fill: colorSet.mainFill,
|
||||
stroke: colorSet.mainStroke,
|
||||
r: 12 + degrees[node.id] / 4,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const create2DGraph = () => {
|
||||
const graph = new G6.Graph({
|
||||
container: container,
|
||||
width,
|
||||
height: 800,
|
||||
fitView: true,
|
||||
minZoom: 0.0001,
|
||||
modes: {
|
||||
default: [
|
||||
{ type: 'zoom-canvas' },
|
||||
// @ts-ignore
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
enableOptimize: false,
|
||||
},
|
||||
'drag-node',
|
||||
'brush-select',
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.read({ nodes, edges });
|
||||
// graph.zoom(0.15);
|
||||
return graph;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const graph = create2DGraph();
|
||||
// stats
|
||||
const stats = new Stats();
|
||||
stats.showPanel(0);
|
||||
const $stats = stats.dom;
|
||||
$stats.style.position = 'absolute';
|
||||
$stats.style.left = '0px';
|
||||
$stats.style.top = '0px';
|
||||
document.body.appendChild($stats);
|
||||
const update = () => {
|
||||
if (stats) {
|
||||
stats.update();
|
||||
}
|
||||
requestAnimationFrame(update);
|
||||
};
|
||||
update();
|
||||
|
||||
return graph;
|
||||
};
|
@ -10,6 +10,10 @@ 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 demo from './demo/demo';
|
||||
import demoFor4 from './demo/demoFor4';
|
||||
export {
|
||||
behaviors_activateRelations,
|
||||
layouts_circular,
|
||||
@ -23,4 +27,8 @@ export {
|
||||
behaviors_brush_select,
|
||||
behaviors_click_select,
|
||||
performance,
|
||||
performance_layout,
|
||||
performance_layout_3d,
|
||||
demo,
|
||||
demoFor4,
|
||||
};
|
||||
|
@ -57,18 +57,27 @@ export default async () => {
|
||||
strokeOpacity: 0.6,
|
||||
},
|
||||
},
|
||||
node: {
|
||||
type: 'sphere-node',
|
||||
keyShape: {
|
||||
r: 10,
|
||||
opacity: 0.6,
|
||||
},
|
||||
// labelShape: {
|
||||
// text: 'node-label',
|
||||
// },
|
||||
// iconShape: {
|
||||
// img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
// },
|
||||
node: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
type: 'sphere-node',
|
||||
keyShape: {
|
||||
r: 10,
|
||||
opacity: 0.6,
|
||||
},
|
||||
labelShape: {
|
||||
text: innerModel.id,
|
||||
fill: 'white',
|
||||
maxWidth: '400%',
|
||||
lod: -1,
|
||||
offsetY: 20,
|
||||
wordWrap: false, // FIXME: mesh.getBounds() returns an empty AABB
|
||||
isBillboard: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
nodeState: {
|
||||
selected: {
|
||||
|
207
packages/g6/tests/intergration/performance/layout-3d.ts
Normal file
207
packages/g6/tests/intergration/performance/layout-3d.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { supportsThreads, initThreads, ForceLayout } from '@antv/layout-wasm';
|
||||
import { loadDataset } from '../../datasets/legacy-format';
|
||||
|
||||
export default async () => {
|
||||
const $container = document.getElementById('container')!;
|
||||
$container.style.display = 'none';
|
||||
|
||||
const WIDTH = 500;
|
||||
const HEIGHT = 500;
|
||||
|
||||
const $app = document.getElementById('app')!;
|
||||
const $containers = document.createElement('div');
|
||||
$app.appendChild($containers);
|
||||
$containers.style.display = 'flex';
|
||||
|
||||
const $container1 = document.createElement('div');
|
||||
const $container2 = document.createElement('div');
|
||||
$containers.appendChild($container1);
|
||||
$containers.appendChild($container2);
|
||||
$container1.style.flex = '1';
|
||||
$container1.style.position = 'relative';
|
||||
$container2.style.flex = '1';
|
||||
$container2.style.position = 'relative';
|
||||
|
||||
const $timer1 = document.createElement('div');
|
||||
$timer1.style.cssText = `
|
||||
font-size: 26px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;`;
|
||||
$container1.appendChild($timer1);
|
||||
const $timer2 = document.createElement('div');
|
||||
$timer2.style.cssText = `
|
||||
font-size: 26px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;`;
|
||||
$container2.appendChild($timer2);
|
||||
|
||||
const data = await loadDataset(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/da5a1b47-37d6-44d7-8d10-f3e046dabf82.json',
|
||||
);
|
||||
|
||||
const layoutOptions = {
|
||||
dimensions: 3,
|
||||
maxIteration: 500,
|
||||
minMovement: 0.4,
|
||||
distanceThresholdMode: 'mean',
|
||||
height: HEIGHT,
|
||||
width: WIDTH,
|
||||
center: [WIDTH / 2, HEIGHT / 2, 0],
|
||||
factor: 1,
|
||||
gravity: 10,
|
||||
linkDistance: 200,
|
||||
edgeStrength: 200,
|
||||
nodeStrength: 1000,
|
||||
coulombDisScale: 0.005,
|
||||
damping: 0.9,
|
||||
maxSpeed: 1000,
|
||||
interval: 0.02,
|
||||
};
|
||||
|
||||
// Force layout WASM
|
||||
(async () => {
|
||||
const supported = await supportsThreads();
|
||||
const threads = await initThreads(supported);
|
||||
|
||||
// Register custom layout
|
||||
G6.stdLib.layouts['force-wasm'] = ForceLayout;
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: $container1,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'orbit-canvas-3d',
|
||||
trigger: 'drag',
|
||||
},
|
||||
'zoom-canvas-3d',
|
||||
],
|
||||
},
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
edge: {
|
||||
type: 'line-edge',
|
||||
keyShape: {
|
||||
lineWidth: 1,
|
||||
stroke: 'grey',
|
||||
strokeOpacity: 0.6,
|
||||
},
|
||||
},
|
||||
node: {
|
||||
type: 'sphere-node',
|
||||
keyShape: {
|
||||
r: 10,
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: 'force-wasm',
|
||||
threads,
|
||||
...layoutOptions,
|
||||
},
|
||||
});
|
||||
|
||||
let timer;
|
||||
graph.on('startlayout', () => {
|
||||
const startTime = performance.now();
|
||||
timer = setInterval(() => {
|
||||
$timer1.innerHTML = `@antv/layout-wasm: ${(
|
||||
performance.now() - startTime
|
||||
).toFixed(2)}ms`;
|
||||
}, 1);
|
||||
});
|
||||
|
||||
graph.on('endlayout', () => {
|
||||
clearInterval(timer);
|
||||
|
||||
const camera = graph.canvas.getCamera();
|
||||
let counter = 0;
|
||||
const tick = () => {
|
||||
if (counter < 80) {
|
||||
camera.dolly(3);
|
||||
counter++;
|
||||
}
|
||||
camera.rotate(0.4, 0);
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
})();
|
||||
|
||||
// Force layout
|
||||
(() => {
|
||||
const graph = new G6.Graph({
|
||||
container: $container2,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'orbit-canvas-3d',
|
||||
trigger: 'drag',
|
||||
},
|
||||
'zoom-canvas-3d',
|
||||
],
|
||||
},
|
||||
edge: {
|
||||
type: 'line-edge',
|
||||
keyShape: {
|
||||
lineWidth: 1,
|
||||
stroke: 'grey',
|
||||
strokeOpacity: 0.6,
|
||||
},
|
||||
},
|
||||
node: {
|
||||
type: 'sphere-node',
|
||||
keyShape: {
|
||||
r: 10,
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'force',
|
||||
workerEnabled: true,
|
||||
...layoutOptions,
|
||||
},
|
||||
});
|
||||
|
||||
let timer;
|
||||
graph.on('startlayout', () => {
|
||||
const startTime = performance.now();
|
||||
timer = setInterval(() => {
|
||||
$timer2.innerHTML = `@antv/layout: ${(
|
||||
performance.now() - startTime
|
||||
).toFixed(2)}ms`;
|
||||
}, 1);
|
||||
});
|
||||
|
||||
graph.on('endlayout', () => {
|
||||
clearInterval(timer);
|
||||
|
||||
const camera = graph.canvas.getCamera();
|
||||
let counter = 0;
|
||||
const tick = () => {
|
||||
if (counter < 80) {
|
||||
camera.dolly(3);
|
||||
counter++;
|
||||
}
|
||||
camera.rotate(0.4, 0);
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
})();
|
||||
};
|
204
packages/g6/tests/intergration/performance/layout.ts
Normal file
204
packages/g6/tests/intergration/performance/layout.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { supportsThreads, initThreads, ForceLayout } from '@antv/layout-wasm';
|
||||
import { loadDataset } from '../../datasets/legacy-format';
|
||||
import { labelPropagation } from '@antv/algorithm';
|
||||
|
||||
export default async () => {
|
||||
const $container = document.getElementById('container')!;
|
||||
$container.style.display = 'none';
|
||||
|
||||
const WIDTH = 500;
|
||||
const HEIGHT = 500;
|
||||
|
||||
const $app = document.getElementById('app')!;
|
||||
const $containers = document.createElement('div');
|
||||
$app.appendChild($containers);
|
||||
$containers.style.display = 'flex';
|
||||
|
||||
const $container1 = document.createElement('div');
|
||||
const $container2 = document.createElement('div');
|
||||
$containers.appendChild($container1);
|
||||
$containers.appendChild($container2);
|
||||
$container1.style.flex = '1';
|
||||
$container1.style.position = 'relative';
|
||||
$container1.id = 'wasm';
|
||||
$container2.style.flex = '1';
|
||||
$container2.style.position = 'relative';
|
||||
$container2.id = 'cpu';
|
||||
|
||||
const $timer1 = document.createElement('div');
|
||||
$timer1.style.cssText = `
|
||||
font-size: 26px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;`;
|
||||
$container1.appendChild($timer1);
|
||||
const $timer2 = document.createElement('div');
|
||||
$timer2.style.cssText = `
|
||||
font-size: 26px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;`;
|
||||
$container2.appendChild($timer2);
|
||||
|
||||
const data = await loadDataset(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/da5a1b47-37d6-44d7-8d10-f3e046dabf82.json',
|
||||
);
|
||||
|
||||
const clusteredData = labelPropagation(data, false);
|
||||
clusteredData.clusters.forEach((cluster, i) => {
|
||||
cluster.nodes.forEach((node) => {
|
||||
node.data.cluster = `c${i}`;
|
||||
});
|
||||
});
|
||||
const degrees = {};
|
||||
data.edges.forEach((edge) => {
|
||||
const { source, target } = edge;
|
||||
degrees[source] = degrees[source] || 0;
|
||||
degrees[target] = degrees[target] || 0;
|
||||
degrees[source]++;
|
||||
degrees[target]++;
|
||||
});
|
||||
|
||||
const configures = {
|
||||
modes: {
|
||||
default: ['zoom-canvas', 'drag-node'],
|
||||
},
|
||||
theme: {
|
||||
type: 'spec',
|
||||
specification: {
|
||||
node: {
|
||||
dataTypeField: 'cluster',
|
||||
},
|
||||
},
|
||||
},
|
||||
node: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
keyShape: {
|
||||
...innerModel.data.keyShape,
|
||||
r: 12 + degrees[innerModel.id] / 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
edge: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
type: 'line-edge',
|
||||
keyShape: {
|
||||
lineWidth: 1,
|
||||
stroke: '#fff',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const layoutOptions = {
|
||||
dimensions: 2,
|
||||
maxIteration: 200,
|
||||
minMovement: 0.4,
|
||||
distanceThresholdMode: 'mean',
|
||||
height: HEIGHT,
|
||||
width: WIDTH,
|
||||
center: [WIDTH / 2, HEIGHT / 2],
|
||||
factor: 1,
|
||||
gravity: 10,
|
||||
linkDistance: 200,
|
||||
edgeStrength: 200,
|
||||
nodeStrength: 1000,
|
||||
coulombDisScale: 0.005,
|
||||
damping: 0.9,
|
||||
maxSpeed: 1000,
|
||||
interval: 0.02,
|
||||
};
|
||||
|
||||
// Force layout WASM
|
||||
(async () => {
|
||||
const supported = await supportsThreads();
|
||||
const threads = await initThreads(supported);
|
||||
|
||||
// Register custom layout
|
||||
G6.stdLib.layouts['force-wasm'] = ForceLayout;
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: $container1,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'force-wasm',
|
||||
threads,
|
||||
...layoutOptions,
|
||||
maxIteration: 400,
|
||||
minMovement: 0.8,
|
||||
},
|
||||
...configures,
|
||||
});
|
||||
|
||||
let timer;
|
||||
graph.on('startlayout', () => {
|
||||
const startTime = performance.now();
|
||||
timer = setInterval(() => {
|
||||
$timer1.innerHTML = `Time: ${(performance.now() - startTime).toFixed(
|
||||
2,
|
||||
)}ms`;
|
||||
}, 1);
|
||||
});
|
||||
|
||||
graph.on('endlayout', () => {
|
||||
clearInterval(timer);
|
||||
graph.zoom(0.1, undefined, {
|
||||
duration: 1000,
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Force layout
|
||||
(() => {
|
||||
const graph = new G6.Graph({
|
||||
container: $container2,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
layout: {
|
||||
type: 'force',
|
||||
workerEnabled: true,
|
||||
...layoutOptions,
|
||||
maxIteration: 8000,
|
||||
minMovement: 0.2,
|
||||
},
|
||||
...configures,
|
||||
});
|
||||
|
||||
let timer;
|
||||
graph.on('startlayout', () => {
|
||||
const startTime = performance.now();
|
||||
timer = setInterval(() => {
|
||||
$timer2.innerHTML = `Time: ${(performance.now() - startTime).toFixed(
|
||||
2,
|
||||
)}ms`;
|
||||
}, 1);
|
||||
});
|
||||
|
||||
graph.on('endlayout', () => {
|
||||
clearInterval(timer);
|
||||
|
||||
graph.zoom(0.1, undefined, {
|
||||
duration: 1000,
|
||||
});
|
||||
});
|
||||
})();
|
||||
};
|
@ -1,10 +1,12 @@
|
||||
import G6 from '../../../src/index';
|
||||
import {
|
||||
BadgePosition,
|
||||
IBadgePosition,
|
||||
ShapeStyle,
|
||||
} from '../../../src/types/item';
|
||||
import { container, height, width } from '../../datasets/const';
|
||||
ForceLayout,
|
||||
FruchtermanLayout,
|
||||
initThreads,
|
||||
supportsThreads,
|
||||
} from '@antv/layout-wasm';
|
||||
import G6 from '../../../src/index';
|
||||
import { IBadgePosition } from '../../../src/types/item';
|
||||
import { container, width } from '../../datasets/const';
|
||||
const data = {
|
||||
nodes: [
|
||||
{ id: 'Myriel', data: { x: 122.619579008568, y: -121.31805520154471 } },
|
||||
@ -1744,9 +1746,7 @@ const clusters = [
|
||||
'Brujon',
|
||||
],
|
||||
];
|
||||
let nodes = data.nodes.map((node) => {
|
||||
node.data.x += 400;
|
||||
node.data.y += 250;
|
||||
const nodes = data.nodes.map((node) => {
|
||||
let nocluster = true;
|
||||
clusters.forEach((cluster, i) => {
|
||||
if (cluster.includes(node.id)) {
|
||||
@ -1759,196 +1759,221 @@ let nodes = data.nodes.map((node) => {
|
||||
// @ts-ignore
|
||||
node.data.cluster = 0;
|
||||
}
|
||||
node.data.layout = {
|
||||
id: node.id,
|
||||
x: node.data.x,
|
||||
y: node.data.y,
|
||||
};
|
||||
return node;
|
||||
});
|
||||
|
||||
nodes = nodes
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-2`,
|
||||
data: {
|
||||
...node.data,
|
||||
x: node.data.x + 500,
|
||||
},
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-3`,
|
||||
data: {
|
||||
...node.data,
|
||||
x: node.data.x + 1000,
|
||||
},
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-4`,
|
||||
data: {
|
||||
...node.data,
|
||||
x: node.data.x + 1500,
|
||||
},
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-5`,
|
||||
data: {
|
||||
...node.data,
|
||||
y: node.data.y + 1000,
|
||||
},
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-6`,
|
||||
data: {
|
||||
...node.data,
|
||||
x: node.data.x + 500,
|
||||
y: node.data.y + 1000,
|
||||
},
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-7`,
|
||||
data: {
|
||||
...node.data,
|
||||
x: node.data.x + 1000,
|
||||
y: node.data.y + 1000,
|
||||
},
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
id: `${node.id}-8`,
|
||||
data: {
|
||||
...node.data,
|
||||
x: node.data.x + 1500,
|
||||
y: node.data.y + 1000,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
const edges = data.edges
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-2`,
|
||||
source: `${edge.source}-2`,
|
||||
target: `${edge.target}-2`,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-3`,
|
||||
source: `${edge.source}-3`,
|
||||
target: `${edge.target}-3`,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-4`,
|
||||
source: `${edge.source}-4`,
|
||||
target: `${edge.target}-4`,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-5`,
|
||||
source: `${edge.source}-5`,
|
||||
target: `${edge.target}-5`,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-6`,
|
||||
source: `${edge.source}-6`,
|
||||
target: `${edge.target}-6`,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-7`,
|
||||
source: `${edge.source}-7`,
|
||||
target: `${edge.target}-7`,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
data.edges.map((edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id: `${edge.id}-8`,
|
||||
source: `${edge.source}-8`,
|
||||
target: `${edge.target}-8`,
|
||||
};
|
||||
}),
|
||||
);
|
||||
export default () => {
|
||||
console.log(
|
||||
'graphsize: #NODE:',
|
||||
nodes.length,
|
||||
', #EDGE:',
|
||||
edges.length,
|
||||
'#SHAPES',
|
||||
nodes.length * 10 + edges.length * 4,
|
||||
);
|
||||
return new G6.Graph({
|
||||
// nodes = nodes.concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-2`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// x: node.data.x + 500,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// );
|
||||
// .concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-3`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// x: node.data.x + 1000,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// );
|
||||
// .concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-4`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// x: node.data.x + 1500,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-5`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// y: node.data.y + 1000,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-6`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// x: node.data.x + 500,
|
||||
// y: node.data.y + 1000,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-7`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// x: node.data.x + 1000,
|
||||
// y: node.data.y + 1000,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// nodes.map((node) => {
|
||||
// return {
|
||||
// ...node,
|
||||
// id: `${node.id}-8`,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// x: node.data.x + 1500,
|
||||
// y: node.data.y + 1000,
|
||||
// },
|
||||
// };
|
||||
// }),
|
||||
// );
|
||||
const edges = data.edges;
|
||||
// const edges = data.edges.concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-2`,
|
||||
// source: `${edge.source}-2`,
|
||||
// target: `${edge.target}-2`,
|
||||
// };
|
||||
// }),
|
||||
// );
|
||||
// .concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-3`,
|
||||
// source: `${edge.source}-3`,
|
||||
// target: `${edge.target}-3`,
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-4`,
|
||||
// source: `${edge.source}-4`,
|
||||
// target: `${edge.target}-4`,
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-5`,
|
||||
// source: `${edge.source}-5`,
|
||||
// target: `${edge.target}-5`,
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-6`,
|
||||
// source: `${edge.source}-6`,
|
||||
// target: `${edge.target}-6`,
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-7`,
|
||||
// source: `${edge.source}-7`,
|
||||
// target: `${edge.target}-7`,
|
||||
// };
|
||||
// }),
|
||||
// )
|
||||
// .concat(
|
||||
// data.edges.map((edge) => {
|
||||
// return {
|
||||
// ...edge,
|
||||
// id: `${edge.id}-8`,
|
||||
// source: `${edge.source}-8`,
|
||||
// target: `${edge.target}-8`,
|
||||
// };
|
||||
// }),
|
||||
// );
|
||||
|
||||
const degrees = {};
|
||||
edges.forEach((edge) => {
|
||||
const { source, target } = edge;
|
||||
degrees[source] = degrees[source] || 0;
|
||||
degrees[target] = degrees[target] || 0;
|
||||
degrees[source]++;
|
||||
degrees[target]++;
|
||||
});
|
||||
|
||||
const createGraph = async () => {
|
||||
const supported = await supportsThreads();
|
||||
const threads = await initThreads(supported);
|
||||
G6.stdLib.layouts['force-wasm'] = ForceLayout;
|
||||
G6.stdLib.layouts['fruchterman-wasm'] = FruchtermanLayout;
|
||||
const graph = new G6.Graph({
|
||||
container: container as HTMLElement,
|
||||
width,
|
||||
height: 1200,
|
||||
type: 'graph',
|
||||
renderer: 'webgl',
|
||||
// renderer: 'webgl',
|
||||
data: { nodes, edges },
|
||||
layout: {
|
||||
type: 'force-wasm',
|
||||
threads,
|
||||
dimensions: 2,
|
||||
maxIteration: 800,
|
||||
minMovement: 0.4,
|
||||
distanceThresholdMode: 'mean',
|
||||
factor: 1,
|
||||
gravity: 10,
|
||||
linkDistance: 80,
|
||||
edgeStrength: 200,
|
||||
nodeStrength: 1000,
|
||||
coulombDisScale: 0.005,
|
||||
damping: 0.9,
|
||||
maxSpeed: 1000,
|
||||
interval: 0.02,
|
||||
},
|
||||
modes: {
|
||||
default: [
|
||||
'zoom-canvas',
|
||||
// @ts-ignore
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
scalableRange: 0.9,
|
||||
},
|
||||
'drag-canvas',
|
||||
'drag-node',
|
||||
'brush-select',
|
||||
'click-select',
|
||||
'hover-activate',
|
||||
],
|
||||
},
|
||||
// @ts-ignore
|
||||
theme: {
|
||||
type: 'spec',
|
||||
specification: {
|
||||
@ -1962,54 +1987,96 @@ export default () => {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
labelShape: {
|
||||
position: 'end',
|
||||
text: 'edge-label',
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
fill: '#fff',
|
||||
},
|
||||
iconShape: {
|
||||
text: 'A',
|
||||
keyShape: {
|
||||
lineWidth: 0.5,
|
||||
},
|
||||
haloShape: {},
|
||||
animates: {
|
||||
buildIn: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 300,
|
||||
shapeId: 'keyShape',
|
||||
duration: 500,
|
||||
delay: 1000,
|
||||
},
|
||||
],
|
||||
buildOut: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
fields: ['lineWidth'],
|
||||
shapeId: 'keyShape',
|
||||
},
|
||||
{
|
||||
fields: ['opacity'], // 'r' error, 'lineWidth' has no effect
|
||||
shapeId: 'haloShape',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
node: (innerModel) => {
|
||||
const badgeShapes: (ShapeStyle & {
|
||||
position?: IBadgePosition;
|
||||
color?: string;
|
||||
textColor?: string;
|
||||
})[] = [
|
||||
{
|
||||
const degree = degrees[innerModel.id] || 0;
|
||||
let labelLod = 4;
|
||||
if (degree > 20) labelLod = -1;
|
||||
else if (degree > 15) labelLod = 0;
|
||||
else if (degree > 10) labelLod = 1;
|
||||
else if (degree > 6) labelLod = 2;
|
||||
else if (degree > 3) labelLod = 3;
|
||||
|
||||
const badgeShapes = {};
|
||||
|
||||
if (degree > 20) {
|
||||
badgeShapes[0] = {
|
||||
text: '核心人员',
|
||||
position: 'right',
|
||||
position: 'right' as IBadgePosition,
|
||||
color: '#389e0d',
|
||||
},
|
||||
{
|
||||
lod: labelLod - 2,
|
||||
};
|
||||
}
|
||||
if (degree > 15) {
|
||||
badgeShapes[1] = {
|
||||
text: 'A',
|
||||
position: 'rightTop',
|
||||
position: 'rightTop' as IBadgePosition,
|
||||
color: '#d4380d',
|
||||
},
|
||||
{
|
||||
lod: labelLod - 1,
|
||||
};
|
||||
}
|
||||
if (degree > 10) {
|
||||
badgeShapes[2] = {
|
||||
text: 'B',
|
||||
position: 'rightBottom',
|
||||
color: '#ccc',
|
||||
},
|
||||
];
|
||||
position: 'rightBottom' as IBadgePosition,
|
||||
color: '#aaa',
|
||||
lod: labelLod - 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ zoomRange: [0, 0.8] }, // -2
|
||||
{ zoomRange: [0.8, 0.9] }, // -1
|
||||
{ zoomRange: [0.9, 1], primary: true }, // 0
|
||||
{ zoomRange: [1, 1.1] }, // 1
|
||||
{ zoomRange: [1.1, 0.2] }, // 2
|
||||
{ zoomRange: [1.2, 1.3] }, // 3
|
||||
{ zoomRange: [1.3, 1.4] }, // 4
|
||||
{ zoomRange: [1.4, 1.5] }, // 5
|
||||
{ zoomRange: [1.5, Infinity] }, // 6
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 500,
|
||||
},
|
||||
},
|
||||
|
||||
animates: {
|
||||
buildIn: [
|
||||
{
|
||||
@ -2018,6 +2085,12 @@ export default () => {
|
||||
delay: 500 + Math.random() * 500,
|
||||
},
|
||||
],
|
||||
buildOut: [
|
||||
{
|
||||
fields: ['opacity'],
|
||||
duration: 200,
|
||||
},
|
||||
],
|
||||
hide: [
|
||||
{
|
||||
fields: ['size'],
|
||||
@ -2046,40 +2119,117 @@ export default () => {
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
fields: ['fill', 'r'],
|
||||
shapeId: 'keyShape',
|
||||
},
|
||||
{
|
||||
fields: ['lineWidth'],
|
||||
shapeId: 'keyShape',
|
||||
duration: 100,
|
||||
},
|
||||
{
|
||||
fields: ['fontSize'],
|
||||
shapeId: 'iconShape',
|
||||
},
|
||||
{
|
||||
fields: ['opacity'], // 'r' error, 'lineWidth' has no effect
|
||||
shapeId: 'haloShape',
|
||||
},
|
||||
],
|
||||
},
|
||||
haloShape: {},
|
||||
// animate in shapes, unrelated to each other, excuted parallely
|
||||
keyShape: {
|
||||
r: 15,
|
||||
r: innerModel.data.size ? innerModel.data.size / 2 : 15,
|
||||
},
|
||||
iconShape: {
|
||||
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
fill: '#fff',
|
||||
lod: labelLod - 1,
|
||||
fontSize: innerModel.data.size ? innerModel.data.size / 3 + 5 : 13,
|
||||
},
|
||||
labelShape: {
|
||||
text: innerModel.id,
|
||||
opacity: 0.8,
|
||||
maxWidth: '150%',
|
||||
lod: labelLod,
|
||||
},
|
||||
labelBackgroundShape: {},
|
||||
badgeShapes: {
|
||||
0: {
|
||||
text: '核心人员',
|
||||
position: 'right' as IBadgePosition,
|
||||
color: '#389e0d',
|
||||
},
|
||||
1: {
|
||||
text: 'A',
|
||||
position: 'rightTop' as IBadgePosition,
|
||||
color: '#d4380d',
|
||||
},
|
||||
2: {
|
||||
text: 'B',
|
||||
position: 'rightBottom' as IBadgePosition,
|
||||
color: '#aaa',
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
lod: labelLod,
|
||||
},
|
||||
badgeShapes,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
graph.zoomTo(0.7);
|
||||
return graph;
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
console.log(
|
||||
'graphsize: #NODE:',
|
||||
nodes.length,
|
||||
', #EDGE:',
|
||||
edges.length,
|
||||
'#SHAPES',
|
||||
nodes.length * 10 + edges.length * 4,
|
||||
);
|
||||
let graph = await createGraph();
|
||||
|
||||
const btnImportance = document.createElement('button');
|
||||
btnImportance.innerHTML = '节点重要性分析';
|
||||
btnImportance.style.position = 'absolute';
|
||||
btnImportance.style.top = '164px';
|
||||
btnImportance.style.left = '373px';
|
||||
btnImportance.style.zIndex = '100';
|
||||
document.body.appendChild(btnImportance);
|
||||
btnImportance.addEventListener('click', (e) => {
|
||||
graph.updateData(
|
||||
'node',
|
||||
nodes.map((node) => ({
|
||||
id: node.id,
|
||||
data: {
|
||||
size: degrees[node.id] + 24,
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
const btnColor = document.createElement('button');
|
||||
btnColor.innerHTML = '更换颜色顺序';
|
||||
btnColor.style.position = 'absolute';
|
||||
btnColor.style.top = '164px';
|
||||
btnColor.style.left = '573px';
|
||||
btnColor.style.zIndex = '100';
|
||||
document.body.appendChild(btnColor);
|
||||
btnColor.addEventListener('click', (e) => {
|
||||
graph.updateData(
|
||||
'node',
|
||||
nodes.map((node) => ({
|
||||
id: node.id,
|
||||
data: {
|
||||
cluster: node.data.cluster + 1,
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
const btnDestroy = document.createElement('button');
|
||||
btnDestroy.innerHTML = '销毁图';
|
||||
btnDestroy.style.position = 'absolute';
|
||||
btnDestroy.style.top = '164px';
|
||||
btnDestroy.style.left = '773px';
|
||||
btnDestroy.style.zIndex = '100';
|
||||
document.body.appendChild(btnDestroy);
|
||||
btnDestroy.addEventListener('click', async (e) => {
|
||||
if (graph.destroyed) {
|
||||
graph = await createGraph();
|
||||
} else {
|
||||
graph.destroy();
|
||||
}
|
||||
});
|
||||
return graph;
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as graphs from './intergration/index';
|
||||
|
||||
performance.mark('create select');
|
||||
const SelectGraph = document.getElementById('select') as HTMLSelectElement;
|
||||
const Options = Object.keys(graphs).map((key) => {
|
||||
const option = document.createElement('option');
|
||||
@ -7,6 +8,7 @@ const Options = Object.keys(graphs).map((key) => {
|
||||
option.textContent = key;
|
||||
return option;
|
||||
});
|
||||
performance.mark('create select');
|
||||
|
||||
SelectGraph.replaceChildren(...Options);
|
||||
|
||||
@ -19,8 +21,16 @@ SelectGraph.onchange = (e) => {
|
||||
graphs[value]();
|
||||
};
|
||||
|
||||
performance.mark('init');
|
||||
// 初始化
|
||||
const params = new URL(location.href).searchParams;
|
||||
const initialExampleName = params.get('name');
|
||||
SelectGraph.value = initialExampleName || Options[0].value;
|
||||
graphs[SelectGraph.value]();
|
||||
performance.mark('init');
|
||||
|
||||
console.log(
|
||||
'create select',
|
||||
performance.measure('create select'),
|
||||
performance.measure('init'),
|
||||
);
|
||||
|
@ -50,7 +50,7 @@ describe('plugin', () => {
|
||||
expect(pxCompare(viewport.style.height, 120)).toBe(true);
|
||||
|
||||
graph.zoom(3);
|
||||
graph.translate(50, 250);
|
||||
graph.translate({ dx: 50, dy: 250 });
|
||||
// setTimeout for: 1. zoom an translate are async function; 2. minimap viewport debounce update
|
||||
setTimeout(() => {
|
||||
expect(pxCompare(viewport.style.left, 100)).toBe(true);
|
||||
|
@ -1925,13 +1925,13 @@ describe('graph show up animations', () => {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
// zoomStrategy: {
|
||||
// lodStrategy: {
|
||||
// levels: [
|
||||
// { range: [0, 0.65] },
|
||||
// { range: [0.65, 0.8] },
|
||||
// { range: [0.8, 1.6], primary: true },
|
||||
// { range: [1.6, 2] },
|
||||
// { range: [2, Infinity] },
|
||||
// { zoomRange: [0, 0.65] },
|
||||
// { zoomRange: [0.65, 0.8] },
|
||||
// { zoomRange: [0.8, 1.6], primary: true },
|
||||
// { zoomRange: [1.6, 2] },
|
||||
// { zoomRange: [2, Infinity] },
|
||||
// ],
|
||||
// animateCfg: {
|
||||
// duration: 200,
|
||||
@ -1986,13 +1986,13 @@ describe('graph show up animations', () => {
|
||||
x,
|
||||
y,
|
||||
// TODO: different for nodes, and config in theme
|
||||
// zoomStrategy: {
|
||||
// lodStrategy: {
|
||||
// levels: [
|
||||
// { range: [0, 0.65] },
|
||||
// { range: [0.65, 0.8] },
|
||||
// { range: [0.8, 1.6], primary: true },
|
||||
// { range: [1.6, 2] },
|
||||
// { range: [2, Infinity] },
|
||||
// { zoomRange: [0, 0.65] },
|
||||
// { zoomRange: [0.65, 0.8] },
|
||||
// { zoomRange: [0.8, 1.6], primary: true },
|
||||
// { zoomRange: [1.6, 2] },
|
||||
// { zoomRange: [2, Infinity] },
|
||||
// ],
|
||||
// animateCfg: {
|
||||
// duration: 200,
|
||||
@ -2071,13 +2071,13 @@ describe('graph show up animations', () => {
|
||||
// default: ['zoom-canvas'],
|
||||
// },
|
||||
// node: d => {
|
||||
// zoomStrategy: {
|
||||
// lodStrategy: {
|
||||
// levels: [
|
||||
// { range: [0, 0.65] },
|
||||
// { range: [0.65, 0.8] },
|
||||
// { range: [0.8, 1.6], primary: true },
|
||||
// { range: [1.6, 2] },
|
||||
// { range: [2, Infinity] },
|
||||
// { zoomRange: [0, 0.65] },
|
||||
// { zoomRange: [0.65, 0.8] },
|
||||
// { zoomRange: [0.8, 1.6], primary: true },
|
||||
// { zoomRange: [1.6, 2] },
|
||||
// { zoomRange: [2, Infinity] },
|
||||
// ],
|
||||
// animateCfg: {
|
||||
// duration: 200,
|
||||
|
@ -22,12 +22,12 @@ describe('viewport', () => {
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
graph.translate(250, 250);
|
||||
graph.translate({ dx: 250, dy: 250 });
|
||||
let [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(0, 1);
|
||||
expect(py).toBeCloseTo(0, 1);
|
||||
|
||||
graph.translate(-250, -250);
|
||||
graph.translate({ dx: -250, dy: -250 });
|
||||
[px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(250, 1);
|
||||
expect(py).toBeCloseTo(250, 1);
|
||||
@ -52,9 +52,12 @@ describe('viewport', () => {
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
await graph.translate(249, 249, {
|
||||
duration: 1000,
|
||||
});
|
||||
await graph.translate(
|
||||
{ dx: 249, dy: 249 },
|
||||
{
|
||||
duration: 1000,
|
||||
},
|
||||
);
|
||||
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBeCloseTo(-250, 1);
|
||||
@ -429,9 +432,12 @@ describe('viewport', () => {
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
await graph.translate(249, 249, {
|
||||
duration: 1000,
|
||||
});
|
||||
await graph.translate(
|
||||
{ dx: 249, dy: 249 },
|
||||
{
|
||||
duration: 1000,
|
||||
},
|
||||
);
|
||||
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBeCloseTo(-249, 1);
|
||||
@ -447,7 +453,7 @@ describe('viewport', () => {
|
||||
});
|
||||
|
||||
await graph.zoom(0.5);
|
||||
await graph.translate(249, 249);
|
||||
await graph.translate({ dx: 249, dy: 249 });
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBeCloseTo(-249, 1);
|
||||
expect(translate.dy).toBeCloseTo(-249, 1);
|
||||
@ -577,10 +583,13 @@ describe('viewport', () => {
|
||||
easing: 'ease-in',
|
||||
},
|
||||
);
|
||||
await graph.translate(100, 100, {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
await graph.translate(
|
||||
{ dx: 100, dy: 100 },
|
||||
{
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
},
|
||||
);
|
||||
await graph.fitView(
|
||||
{
|
||||
padding: [150, 100],
|
||||
|
@ -10,7 +10,16 @@ export default defineConfig({
|
||||
// @see https://github.com/vitejs/vite/issues/10839#issuecomment-1345193175
|
||||
// @see https://vitejs.dev/guide/dep-pre-bundling.html#customizing-the-behavior
|
||||
// @see https://vitejs.dev/config/dep-optimization-options.html#optimizedeps-exclude
|
||||
exclude: ['@antv/layout-wasm'],
|
||||
exclude: [
|
||||
'@antv/layout-wasm',
|
||||
// '@antv/gui',
|
||||
// '@antv/layout',
|
||||
// '@antv/algorithm',
|
||||
// '@antv/layout-gpu',
|
||||
// '@antv/g',
|
||||
// '@antv/g-canvas',
|
||||
// '@antv/g-webgl',
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user