chore: merge v5
24
CHANGELOG.md
@ -81,8 +81,8 @@
|
||||
- fix: image lost while updating the size for an image node, closes: #3938;
|
||||
|
||||
### 4.7.7
|
||||
|
||||
- feat: getContentPlaceholder and getTitlePlaceholder for Annotation plugin;
|
||||
|
||||
- feat: getContentPlaceholder and getTitlePlaceholder for Annotation plugin;
|
||||
|
||||
### 4.7.6
|
||||
|
||||
@ -93,9 +93,9 @@
|
||||
|
||||
- perf: Annotation support updating positions for outside cards by calling updateOutsideCards;
|
||||
|
||||
### 4.7.4
|
||||
|
||||
- perf: Annotation min-width and input width;
|
||||
### 4.7.4
|
||||
|
||||
- perf: Annotation min-width and input width;
|
||||
|
||||
### 4.7.3
|
||||
|
||||
@ -192,7 +192,7 @@
|
||||
|
||||
- fix: destroyLayout error, closes: #3727;
|
||||
- fix: drag combo with stack problem, closes: #3699;
|
||||
- fix: updateLayout does not take effect if update layout with same type as graph instance configuration, closes: #3706;
|
||||
- fix: updateLayout does not take effect if update layout with same type as graph instance configuration, closes: #3706;
|
||||
- fix: legendStateStyles typo, closes: #3705;
|
||||
- perf: zoom-canvas take the maximum and minimum values instead of return directly;
|
||||
- perf: minimap cursor move;
|
||||
@ -205,7 +205,6 @@
|
||||
- chore: improve the types of graph events;
|
||||
- fix: position animate considers origin attributes;
|
||||
|
||||
|
||||
#### 4.6.3
|
||||
|
||||
- feat: shouldDeselect param for lasso-select;
|
||||
@ -250,18 +249,17 @@
|
||||
|
||||
- feat: translate graph with animation;
|
||||
- feat: zoom graph with animation;
|
||||
- feat: timebar supports filterItemTypes to configure the types of the graph items to be filtered; only nodes can be filtered before;
|
||||
- feat: timebar supports filterItemTypes to configure the types of the graph items to be filtered; only nodes can be filtered before;
|
||||
- feat: timebar supports to configure the rotate of the tick labels by tickLabelStyle[dot]rotate;
|
||||
- feat: timebar supports container CSS configuration by containerCSS;
|
||||
- feat: timebar supports a function getDate to returns the date value according to each node or edge by user;
|
||||
- feat: timebar supports afunction getValue to returns the value (for trend line of trend timebar) according to each node or edge by user;
|
||||
- feat: timebar supports afunction getValue to returns the value (for trend line of trend timebar) according to each node or edge by user;
|
||||
- feat: timebar supports to configure a boolean changeData to control the filter way, true means filters by graph[dot]changeData, false means filters by graph[dot]showItem and graph[dot]hideItem;
|
||||
- feat: timebar supports to configure a function shouldIgnore to return true or false by user to decide whether the node or the edge should be ignored while filtering;
|
||||
- fix: simple timebar silder text position strategy and expand the lineAppendWidth for the slider;
|
||||
- fix: edge label padding bug, closes: #3346;
|
||||
- fix: update node with iconfont icon, the icon is updated to a wrong position, closes: #3348;
|
||||
|
||||
|
||||
#### 4.5.0
|
||||
|
||||
- fix: add item type to the parameter of afterremoveitem event;
|
||||
@ -312,7 +310,6 @@
|
||||
- fix: update node position with wrong position;
|
||||
- feat: enableStack for drag-node behavior, closes: #3128;
|
||||
|
||||
|
||||
#### 4.3.5
|
||||
|
||||
- fix: drag a node without comboId by drag-node with onlyChangeComboSize;
|
||||
@ -326,7 +323,7 @@
|
||||
- fix: when select a node with click-select, selected combos should be deselected;
|
||||
- fix: contextmenu with click trigger does not show the menu up, closes: #2982;
|
||||
- fix: layout with collapsed combo, closes: #2988;
|
||||
- fix: zoom-canvas with optimizeZoom, drag-canvas shows the node shapes hiden by zoom-canvas optimizeZoom, closes: #2996;
|
||||
- fix: zoom-canvas with optimizeZoom, drag-canvas shows the node shapes hiden by zoom-canvas optimizeZoom, closes: #2996;
|
||||
|
||||
#### 4.3.3
|
||||
|
||||
@ -406,6 +403,7 @@
|
||||
- feat: tooltip with trigger configuration, supports mouseenter and click;
|
||||
|
||||
#### 4.2.0
|
||||
|
||||
#### 4.1.14
|
||||
|
||||
- fix: combo edge link position problem;
|
||||
@ -1057,7 +1055,7 @@
|
||||
- feat: collapse-expand tree support click and dblclick by trigger option
|
||||
- fix: drag group bug fix
|
||||
|
||||
#### 3.0.5-beta.9
|
||||
#### 3.0.5-beta.10
|
||||
|
||||
- feat: support render group
|
||||
- feat: support drag group, collapse and expand group, drag node in/out group
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@antv/g6",
|
||||
"version": "5.0.0-beta.4",
|
||||
"version": "5.0.0-beta.11",
|
||||
"description": "A Graph Visualization Framework in JavaScript",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
@ -15,7 +15,7 @@ runtime.enableCSSParsing = false;
|
||||
* Extend the graph class with std lib
|
||||
*/
|
||||
|
||||
const version = '5.0.0-beta.4';
|
||||
const version = '5.0.0-beta.11';
|
||||
|
||||
const Graph = extend(EmptyGraph, stdLib);
|
||||
|
||||
|
@ -264,7 +264,7 @@ export default class Combo extends Node {
|
||||
// @ts-ignore
|
||||
public clone(
|
||||
containerGroup: Group,
|
||||
onlyKeyShape?: boolean,
|
||||
shapeIds?: string[],
|
||||
disableAnimate?: boolean,
|
||||
getCombinedBounds?: () =>
|
||||
| {
|
||||
@ -276,14 +276,19 @@ export default class Combo extends Node {
|
||||
| false,
|
||||
getChildren?: () => (Node | Combo)[],
|
||||
) {
|
||||
if (onlyKeyShape) {
|
||||
const clonedKeyShape = this.shapeMap.keyShape.cloneNode();
|
||||
const pos = this.group.getPosition();
|
||||
const clonedGroup = new Group();
|
||||
clonedGroup.setPosition(pos);
|
||||
clonedGroup.appendChild(clonedKeyShape);
|
||||
containerGroup.appendChild(clonedGroup);
|
||||
return clonedGroup;
|
||||
if (shapeIds?.length) {
|
||||
const group = new Group();
|
||||
shapeIds.forEach((shapeId) => {
|
||||
if (!this.shapeMap[shapeId] || this.shapeMap[shapeId].destroyed) return;
|
||||
const clonedKeyShape = this.shapeMap[shapeId].cloneNode();
|
||||
// TODO: other animating attributes?
|
||||
clonedKeyShape.style.opacity =
|
||||
this.renderExt.mergedStyles[shapeId]?.opacity || 1;
|
||||
group.appendChild(clonedKeyShape);
|
||||
});
|
||||
group.setPosition(this.group.getPosition());
|
||||
containerGroup.appendChild(group);
|
||||
return group;
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Circle, Group } from '@antv/g';
|
||||
import { clone, throttle } from '@antv/util';
|
||||
import { EdgeDisplayModel, EdgeModel, ID } from '../types';
|
||||
import { EdgeDisplayModel, EdgeModel, ID, Point } from '../types';
|
||||
import { EdgeModelData } from '../types/edge';
|
||||
import { DisplayMapper, State, LodStrategyObj } from '../types/item';
|
||||
import { updateShapes } from '../util/shape';
|
||||
@ -43,6 +43,15 @@ export default class Edge extends Item {
|
||||
public sourceItem: Node | Combo;
|
||||
public targetItem: Node | Combo;
|
||||
|
||||
/** Caches to avoid unnecessary calculations. */
|
||||
private cache: {
|
||||
sourcePositionCache?: Point;
|
||||
targetPositionCache?: Point;
|
||||
controlPointsCache?: Point;
|
||||
sourcePointCache?: Point;
|
||||
targetPointCache?: Point;
|
||||
} = {};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
this.init({ ...props, type: 'edge' });
|
||||
@ -68,40 +77,7 @@ export default class Edge extends Item {
|
||||
animate = true,
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
// get the point near the other end
|
||||
const { sourceAnchor, targetAnchor, keyShape } = displayModel.data;
|
||||
const sourcePosition = this.sourceItem.getPosition();
|
||||
const targetPosition = this.targetItem.getPosition();
|
||||
|
||||
let targetPrevious = sourcePosition;
|
||||
let sourcePrevious = targetPosition;
|
||||
|
||||
// TODO: type
|
||||
// @ts-ignore
|
||||
if (keyShape?.controlPoints?.length) {
|
||||
// @ts-ignore
|
||||
const controlPointsBesideEnds = keyShape.controlPoints.filter(
|
||||
(point) =>
|
||||
!isSamePoint(point, sourcePosition) &&
|
||||
!isSamePoint(point, targetPosition),
|
||||
);
|
||||
sourcePrevious = getNearestPoint(
|
||||
controlPointsBesideEnds,
|
||||
sourcePosition,
|
||||
).nearestPoint;
|
||||
targetPrevious = getNearestPoint(
|
||||
controlPointsBesideEnds,
|
||||
targetPosition,
|
||||
).nearestPoint;
|
||||
}
|
||||
const sourcePoint = this.sourceItem.getAnchorPoint(
|
||||
sourcePrevious,
|
||||
sourceAnchor,
|
||||
);
|
||||
const targetPoint = this.targetItem.getAnchorPoint(
|
||||
targetPrevious,
|
||||
targetAnchor,
|
||||
);
|
||||
const { sourcePoint, targetPoint } = this.getEndPoints(displayModel);
|
||||
this.renderExt.mergeStyles(displayModel);
|
||||
const firstRendering = !this.shapeMap?.keyShape;
|
||||
this.renderExt.setSourcePoint(sourcePoint);
|
||||
@ -164,16 +140,23 @@ export default class Edge extends Item {
|
||||
* Sometimes no changes on edge data, but need to re-draw it
|
||||
* e.g. source and target nodes' position changed
|
||||
*/
|
||||
public forceUpdate = throttle(
|
||||
() => {
|
||||
if (!this.destroyed) this.draw(this.displayModel);
|
||||
},
|
||||
16,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
public forceUpdate() {
|
||||
if (this.destroyed) return;
|
||||
const { sourcePoint, targetPoint, changed } = this.getEndPoints(
|
||||
this.displayModel,
|
||||
);
|
||||
if (!changed) return;
|
||||
this.renderExt.setSourcePoint(sourcePoint);
|
||||
this.renderExt.setTargetPoint(targetPoint);
|
||||
const shapeMap = this.renderExt.draw(
|
||||
this.displayModel,
|
||||
sourcePoint,
|
||||
targetPoint,
|
||||
this.shapeMap,
|
||||
);
|
||||
// add shapes to group, and update shapeMap
|
||||
this.shapeMap = updateShapes(this.shapeMap, shapeMap, this.group);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update end item for item and re-draw the edge
|
||||
@ -186,23 +169,111 @@ export default class Edge extends Item {
|
||||
this.draw(this.displayModel);
|
||||
}
|
||||
|
||||
// public update(model: EdgeModel) {
|
||||
// super.update(model);
|
||||
// }
|
||||
/**
|
||||
* Calculate the source and target points according to the source and target nodes and the anchorPoints and controlPoints.
|
||||
* @param displayModel
|
||||
* @returns
|
||||
*/
|
||||
private getEndPoints(displayModel: EdgeDisplayModel) {
|
||||
// get the point near the other end
|
||||
const { sourceAnchor, targetAnchor, keyShape } = displayModel.data;
|
||||
const sourcePosition = this.sourceItem.getPosition();
|
||||
const targetPosition = this.targetItem.getPosition();
|
||||
|
||||
if (
|
||||
!this.shouldUpdatePoints(
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
// @ts-ignore
|
||||
keyShape?.controlPoints,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
sourcePoint: this.cache.sourcePointCache,
|
||||
targetPoint: this.cache.targetPointCache,
|
||||
changed: false,
|
||||
};
|
||||
}
|
||||
|
||||
let targetPrevious = sourcePosition;
|
||||
let sourcePrevious = targetPosition;
|
||||
|
||||
// TODO: type
|
||||
// @ts-ignore
|
||||
if (keyShape?.controlPoints?.length) {
|
||||
// @ts-ignore
|
||||
const controlPointsBesideEnds = keyShape.controlPoints.filter(
|
||||
(point) =>
|
||||
!isSamePoint(point, sourcePosition) &&
|
||||
!isSamePoint(point, targetPosition),
|
||||
);
|
||||
sourcePrevious = getNearestPoint(
|
||||
controlPointsBesideEnds,
|
||||
sourcePosition,
|
||||
).nearestPoint;
|
||||
targetPrevious = getNearestPoint(
|
||||
controlPointsBesideEnds,
|
||||
targetPosition,
|
||||
).nearestPoint;
|
||||
}
|
||||
this.cache.sourcePointCache = this.sourceItem.getAnchorPoint(
|
||||
sourcePrevious,
|
||||
sourceAnchor,
|
||||
);
|
||||
this.cache.targetPointCache = this.targetItem.getAnchorPoint(
|
||||
targetPrevious,
|
||||
targetAnchor,
|
||||
);
|
||||
return {
|
||||
sourcePoint: this.cache.sourcePointCache,
|
||||
targetPoint: this.cache.targetPointCache,
|
||||
changed: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the source, target, controlPoints are not changed, avoiding unnecessary computations.
|
||||
* @param sourcePosition
|
||||
* @param targetPosition
|
||||
* @param controlPoints
|
||||
* @returns
|
||||
*/
|
||||
private shouldUpdatePoints(sourcePosition, targetPosition, controlPoints) {
|
||||
const isComboEnd =
|
||||
this.sourceItem.type === 'combo' || this.targetItem.type === 'combo';
|
||||
const changed =
|
||||
!(
|
||||
isSamePoint(sourcePosition, this.cache.sourcePositionCache) &&
|
||||
isSamePoint(targetPosition, this.cache.targetPositionCache) &&
|
||||
controlPoints === this.cache.controlPointsCache
|
||||
) || isComboEnd;
|
||||
if (changed) {
|
||||
this.cache.sourcePositionCache = sourcePosition;
|
||||
this.cache.targetPositionCache = targetPosition;
|
||||
this.cache.controlPointsCache = controlPoints;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public clone(
|
||||
containerGroup: Group,
|
||||
sourceItem: Node | Combo,
|
||||
targetItem: Node | Combo,
|
||||
onlyKeyShape?: boolean,
|
||||
shapeIds?: string[],
|
||||
disableAnimate?: boolean,
|
||||
) {
|
||||
if (onlyKeyShape) {
|
||||
const clonedKeyShape = this.shapeMap.keyShape.cloneNode();
|
||||
const clonedGroup = new Group();
|
||||
clonedGroup.appendChild(clonedKeyShape);
|
||||
containerGroup.appendChild(clonedGroup);
|
||||
return clonedGroup;
|
||||
if (shapeIds?.length) {
|
||||
const group = new Group();
|
||||
shapeIds.forEach((shapeId) => {
|
||||
if (!this.shapeMap[shapeId] || this.shapeMap[shapeId].destroyed) return;
|
||||
const clonedKeyShape = this.shapeMap[shapeId].cloneNode();
|
||||
// TODO: other animating attributes?
|
||||
clonedKeyShape.style.opacity =
|
||||
this.renderExt.mergedStyles[shapeId]?.opacity || 1;
|
||||
group.appendChild(clonedKeyShape);
|
||||
});
|
||||
containerGroup.appendChild(group);
|
||||
return group;
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
|
@ -412,7 +412,7 @@ export default abstract class Item implements IItem {
|
||||
/** Show the item. */
|
||||
public show(animate = true) {
|
||||
Promise.all(this.stopAnimations()).finally(() => {
|
||||
if (this.destroyed || this.visible) return;
|
||||
if (this.destroyed) return;
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.show?.length) {
|
||||
const showAnimateFieldsMap: any = {};
|
||||
@ -440,7 +440,7 @@ export default abstract class Item implements IItem {
|
||||
shape.show();
|
||||
}
|
||||
});
|
||||
if (showAnimateFieldsMap.group) {
|
||||
if (showAnimateFieldsMap.group && !this.shapeMap.keyShape.isVisible()) {
|
||||
showAnimateFieldsMap.group.forEach((field) => {
|
||||
const usingField = field === 'size' ? 'transform' : field;
|
||||
if (GROUP_ANIMATE_STYLES[0].hasOwnProperty(usingField)) {
|
||||
@ -450,11 +450,13 @@ export default abstract class Item implements IItem {
|
||||
});
|
||||
}
|
||||
|
||||
this.animations = this.runWithAnimates(
|
||||
animates,
|
||||
'show',
|
||||
targetStyleMap,
|
||||
);
|
||||
if (Object.keys(targetStyleMap).length) {
|
||||
this.animations = this.runWithAnimates(
|
||||
animates,
|
||||
'show',
|
||||
targetStyleMap,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
@ -474,14 +476,16 @@ export default abstract class Item implements IItem {
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
if (keepKeyShape && id === 'keyShape') return;
|
||||
const shape = this.shapeMap[id];
|
||||
if (!this.visible && !shape.isVisible())
|
||||
if (!shape.isVisible()) {
|
||||
this.cacheNotHiddenByItem[id] = true;
|
||||
return;
|
||||
}
|
||||
shape.hide();
|
||||
this.cacheHiddenByItem[id] = true;
|
||||
});
|
||||
};
|
||||
Promise.all(this.stopAnimations()).then(() => {
|
||||
if (this.destroyed || !this.visible) return;
|
||||
if (this.destroyed) return;
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.hide?.length) {
|
||||
this.animations = this.runWithAnimates(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Circle, Group, Rect } from '@antv/g';
|
||||
import { Group } from '@antv/g';
|
||||
import { clone } from '@antv/util';
|
||||
import { Point } from '../types/common';
|
||||
import { ComboDisplayModel, ComboModel, NodeModel } from '../types';
|
||||
@ -14,7 +14,6 @@ import {
|
||||
getRectIntersectByPoint,
|
||||
} from '../util/point';
|
||||
import { ComboModelData } from '../types/combo';
|
||||
import { isArraySame } from '../util/array';
|
||||
import Item from './item';
|
||||
|
||||
interface IProps {
|
||||
@ -79,7 +78,9 @@ export default class Node extends Item {
|
||||
const { animates, disableAnimate, x = 0, y = 0, z = 0 } = displayModel.data;
|
||||
if (firstRendering) {
|
||||
// first rendering, move the group
|
||||
group.setLocalPosition(x, y, z);
|
||||
group.style.x = x;
|
||||
group.style.y = y;
|
||||
group.style.z = z;
|
||||
} else {
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
@ -195,27 +196,30 @@ export default class Node extends Item {
|
||||
return;
|
||||
}
|
||||
}
|
||||
group.setLocalPosition([
|
||||
position.x as number,
|
||||
position.y as number,
|
||||
position.z,
|
||||
]);
|
||||
group.style.x = position.x;
|
||||
group.style.y = position.y;
|
||||
group.style.z = position.z;
|
||||
onfinish(displayModel.id, !animate);
|
||||
}
|
||||
|
||||
public clone(
|
||||
containerGroup: Group,
|
||||
onlyKeyShape?: boolean,
|
||||
shapeIds?: string[],
|
||||
disableAnimate?: boolean,
|
||||
) {
|
||||
if (onlyKeyShape) {
|
||||
const clonedKeyShape = this.shapeMap.keyShape.cloneNode();
|
||||
const pos = this.group.getPosition();
|
||||
const clonedGroup = new Group();
|
||||
clonedGroup.setPosition(pos);
|
||||
clonedGroup.appendChild(clonedKeyShape);
|
||||
containerGroup.appendChild(clonedGroup);
|
||||
return clonedGroup;
|
||||
if (shapeIds?.length) {
|
||||
const group = new Group();
|
||||
shapeIds.forEach((shapeId) => {
|
||||
if (!this.shapeMap[shapeId] || this.shapeMap[shapeId].destroyed) return;
|
||||
const clonedKeyShape = this.shapeMap[shapeId].cloneNode();
|
||||
// TODO: other animating attributes?
|
||||
clonedKeyShape.style.opacity =
|
||||
this.renderExt.mergedStyles[shapeId]?.opacity || 1;
|
||||
group.appendChild(clonedKeyShape);
|
||||
});
|
||||
group.setPosition(this.group.getPosition());
|
||||
containerGroup.appendChild(group);
|
||||
return group;
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
@ -329,15 +333,16 @@ export default class Node extends Item {
|
||||
}
|
||||
if (!linkPoint) {
|
||||
// If the calculations above are all failed, return the data's position
|
||||
return { x, y };
|
||||
return { x, y, z };
|
||||
}
|
||||
if (!isNaN(z)) linkPoint.z = z;
|
||||
return linkPoint;
|
||||
}
|
||||
|
||||
public getPosition(): Point {
|
||||
const initiated =
|
||||
this.shapeMap.keyShape && this.group.attributes.x !== undefined;
|
||||
if (initiated) {
|
||||
if (initiated && this.renderExt.dimensions !== 3) {
|
||||
const { center } = this.shapeMap.keyShape.getRenderBounds();
|
||||
return { x: center[0], y: center[1], z: center[2] };
|
||||
}
|
||||
|
@ -44,10 +44,6 @@ import { hasTreeBehaviors } from '../../util/behavior';
|
||||
export class DataController {
|
||||
public graph: IGraph;
|
||||
public extensions = [];
|
||||
/**
|
||||
* User input data.
|
||||
*/
|
||||
public userGraphCore: GraphCore;
|
||||
/**
|
||||
* Inner data stored in graphCore structure.
|
||||
*/
|
||||
@ -58,6 +54,11 @@ export class DataController {
|
||||
*/
|
||||
private treeDirtyFlag: boolean;
|
||||
|
||||
/**
|
||||
* Cache the current data type;
|
||||
*/
|
||||
private dataType: 'treeData' | 'graphData' | 'fetch';
|
||||
|
||||
constructor(graph: IGraph<any, any>) {
|
||||
this.graph = graph;
|
||||
this.tap();
|
||||
@ -181,7 +182,7 @@ export class DataController {
|
||||
*/
|
||||
private onDataChange(param: { data: DataConfig; type: DataChangeType }) {
|
||||
const { data, type: changeType } = param;
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
const change = () => {
|
||||
switch (changeType) {
|
||||
case 'remove':
|
||||
@ -202,8 +203,8 @@ export class DataController {
|
||||
break;
|
||||
}
|
||||
};
|
||||
if (userGraphCore) {
|
||||
userGraphCore.batch(change);
|
||||
if (graphCore) {
|
||||
graphCore.batch(change);
|
||||
} else {
|
||||
change();
|
||||
}
|
||||
@ -215,7 +216,7 @@ export class DataController {
|
||||
}) {
|
||||
const { ids, action } = params;
|
||||
ids.forEach((id) => {
|
||||
this.userGraphCore.mergeNodeData(id, {
|
||||
this.graphCore.mergeNodeData(id, {
|
||||
collapsed: action === 'collapse',
|
||||
});
|
||||
});
|
||||
@ -223,7 +224,7 @@ export class DataController {
|
||||
|
||||
private onLayout({ options }) {
|
||||
if (this.treeDirtyFlag && isTreeLayout(options)) {
|
||||
this.establishUserGraphCoreTree();
|
||||
this.establishGraphCoreTree();
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,7 +232,7 @@ export class DataController {
|
||||
const { modes = {} } = this.graph.getSpecification();
|
||||
const mode = this.graph.getMode() || 'default';
|
||||
if (hasTreeBehaviors(modes[mode])) {
|
||||
this.establishUserGraphCoreTree();
|
||||
this.establishGraphCoreTree();
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,31 +245,29 @@ export class DataController {
|
||||
const mode = this.graph.getMode() || 'default';
|
||||
if (action !== 'add' || !modes.includes(mode)) return;
|
||||
if (hasTreeBehaviors(behaviors)) {
|
||||
this.establishUserGraphCoreTree();
|
||||
this.establishGraphCoreTree();
|
||||
}
|
||||
}
|
||||
|
||||
private onModeChange(param: { mode: string }) {
|
||||
const { modes = {} } = this.graph.getSpecification();
|
||||
if (hasTreeBehaviors(modes[param.mode])) {
|
||||
this.establishUserGraphCoreTree();
|
||||
this.establishGraphCoreTree();
|
||||
}
|
||||
}
|
||||
|
||||
private establishUserGraphCoreTree() {
|
||||
private establishGraphCoreTree() {
|
||||
if (!this.treeDirtyFlag) return;
|
||||
const nodes = this.userGraphCore.getAllNodes();
|
||||
const edges = this.userGraphCore.getAllEdges();
|
||||
this.userGraphCore.batch(() => {
|
||||
// graph data to tree structure and storing
|
||||
const rootIds = nodes
|
||||
.filter((node) => node.data.isRoot)
|
||||
.map((node) => node.id);
|
||||
graphData2TreeData({}, { nodes, edges }, rootIds).forEach((tree) => {
|
||||
traverse(tree, (node) => {
|
||||
node.children?.forEach((child) => {
|
||||
this.userGraphCore.setParent(child.id, node.id, 'tree');
|
||||
});
|
||||
const nodes = this.graphCore.getAllNodes();
|
||||
const edges = this.graphCore.getAllEdges();
|
||||
// graph data to tree structure and storing
|
||||
const rootIds = nodes
|
||||
.filter((node) => node.data.isRoot)
|
||||
.map((node) => node.id);
|
||||
graphData2TreeData({}, { nodes, edges }, rootIds).forEach((tree) => {
|
||||
traverse(tree, (node) => {
|
||||
node.children?.forEach((child) => {
|
||||
this.graphCore.setParent(child.id, node.id, 'tree');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -286,20 +285,11 @@ export class DataController {
|
||||
) {
|
||||
const { type: dataType, data } = this.formatData(dataConfig) || {};
|
||||
if (!dataType) return;
|
||||
this.dataType = dataType;
|
||||
|
||||
const { nodes = [], edges = [], combos = [] } = this.transformData(data);
|
||||
|
||||
if (changeType === 'replace') {
|
||||
this.userGraphCore = new GraphLib<NodeUserModelData, EdgeUserModelData>({
|
||||
nodes: nodes.concat(
|
||||
combos?.map((combo) => ({
|
||||
id: combo.id,
|
||||
data: { ...combo.data, _isCombo: true },
|
||||
})) || [],
|
||||
),
|
||||
edges,
|
||||
onChanged: (event) => this.updateGraphCore(event),
|
||||
});
|
||||
this.graphCore = new GraphLib<NodeModelData, EdgeModelData>(
|
||||
clone({
|
||||
nodes: nodes.concat(
|
||||
@ -341,9 +331,9 @@ export class DataController {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
const prevData = deconstructData({
|
||||
nodes: userGraphCore.getAllNodes(),
|
||||
nodes: graphCore.getAllNodes(),
|
||||
edges: [],
|
||||
});
|
||||
const nodesAndCombos = nodes.concat(
|
||||
@ -355,93 +345,134 @@ export class DataController {
|
||||
|
||||
// =========== node & combos ============
|
||||
if (!prevData.nodes.length) {
|
||||
userGraphCore.addNodes(nodesAndCombos);
|
||||
graphCore.addNodes(nodesAndCombos);
|
||||
} else {
|
||||
if (changeType === 'mergeReplace') {
|
||||
// remove the nodes which are not in data but in userGraphCore
|
||||
// remove the nodes which are not in incoming data but in graphCore
|
||||
const nodeAndComboIds = nodesAndCombos.map((node) => node.id);
|
||||
prevData.nodes.forEach((prevNode) => {
|
||||
if (!nodeAndComboIds.includes(prevNode.id))
|
||||
userGraphCore.removeNode(prevNode.id);
|
||||
if (!nodeAndComboIds.includes(prevNode.id)) {
|
||||
this.removeNode(prevNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
// add or update node
|
||||
nodesAndCombos.forEach((item) => {
|
||||
if (userGraphCore.hasNode(item.id)) {
|
||||
if (graphCore.hasNode(item.id)) {
|
||||
// update node which is in the graphCore
|
||||
userGraphCore.mergeNodeData(item.id, item.data);
|
||||
graphCore.mergeNodeData(item.id, item.data);
|
||||
} else {
|
||||
// add node which is in data but not in graphCore
|
||||
userGraphCore.addNode(item);
|
||||
graphCore.addNode(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========== edge ============
|
||||
prevData.edges = userGraphCore.getAllEdges();
|
||||
prevData.edges = graphCore.getAllEdges();
|
||||
if (!prevData.edges.length) {
|
||||
userGraphCore.addEdges(edges);
|
||||
graphCore.addEdges(edges);
|
||||
} else {
|
||||
if (changeType === 'mergeReplace') {
|
||||
// remove the edges which are not in data but in userGraphCore
|
||||
// remove the edges which are not in incoming data but in graphCore
|
||||
const edgeIds = edges.map((edge) => edge.id);
|
||||
prevData.edges.forEach((prevEdge) => {
|
||||
if (!edgeIds.includes(prevEdge.id))
|
||||
userGraphCore.removeEdge(prevEdge.id);
|
||||
graphCore.removeEdge(prevEdge.id);
|
||||
});
|
||||
}
|
||||
// add or update edge
|
||||
edges.forEach((edge) => {
|
||||
if (userGraphCore.hasEdge(edge.id)) {
|
||||
if (graphCore.hasEdge(edge.id)) {
|
||||
// update edge which is in the graphCore
|
||||
userGraphCore.mergeEdgeData(edge.id, edge.data);
|
||||
graphCore.mergeEdgeData(edge.id, edge.data);
|
||||
} else {
|
||||
// add edge which is in data but not in graphCore
|
||||
userGraphCore.addEdge(edge);
|
||||
graphCore.addEdge(edge);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.edges?.length) {
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
// convert and store tree structure to graphCore
|
||||
this.updateTreeGraph(dataType, {
|
||||
nodes: userGraphCore.getAllNodes(),
|
||||
edges: userGraphCore.getAllEdges(),
|
||||
nodes: graphCore.getAllNodes(),
|
||||
edges: graphCore.getAllEdges(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private removeNode(nodeModel: NodeModel) {
|
||||
const { id, data } = nodeModel;
|
||||
const { graphCore } = this;
|
||||
if (graphCore.hasTreeStructure('combo')) {
|
||||
// remove from its parent's children list
|
||||
graphCore.setParent(id, undefined, 'combo');
|
||||
const { parentId } = data;
|
||||
// move the children to the grandparent's children list
|
||||
graphCore.getChildren(id, 'combo').forEach((child) => {
|
||||
graphCore.setParent(child.id, parentId, 'combo');
|
||||
graphCore.mergeNodeData(child.id, { parentId });
|
||||
});
|
||||
}
|
||||
if (graphCore.hasTreeStructure('tree')) {
|
||||
const succeedIds = [];
|
||||
graphCore.dfsTree(
|
||||
id,
|
||||
(child) => {
|
||||
succeedIds.push(child.id);
|
||||
},
|
||||
'tree',
|
||||
);
|
||||
const succeedEdgeIds = graphCore
|
||||
.getAllEdges()
|
||||
.filter(
|
||||
({ source, target }) =>
|
||||
succeedIds.includes(source) && succeedIds.includes(target),
|
||||
)
|
||||
.map((edge) => edge.id);
|
||||
this.graph.showItem(
|
||||
succeedIds
|
||||
.filter((succeedId) => succeedId !== id)
|
||||
.concat(succeedEdgeIds),
|
||||
);
|
||||
|
||||
// for tree graph view, remove the node from the parent's children list
|
||||
graphCore.setParent(id, undefined, 'tree');
|
||||
// for tree graph view, make the its children to be roots
|
||||
graphCore
|
||||
.getChildren(id, 'tree')
|
||||
.forEach((child) => graphCore.setParent(child.id, undefined, 'tree'));
|
||||
}
|
||||
graphCore.removeNode(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove part of old data.
|
||||
* @param data data to be removed which is part of old one
|
||||
*/
|
||||
private removeData(data: GraphData) {
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
const { nodes = [], edges = [], combos = [] } = data;
|
||||
const nodesAndCombos = nodes.concat(combos);
|
||||
const prevNodesAndCombos = userGraphCore.getAllNodes();
|
||||
const prevEdges = userGraphCore.getAllEdges();
|
||||
const prevNodesAndCombos = graphCore.getAllNodes();
|
||||
const prevEdges = graphCore.getAllEdges();
|
||||
if (prevNodesAndCombos.length && nodesAndCombos.length) {
|
||||
// update the parentId
|
||||
if (this.graphCore.hasTreeStructure('combo')) {
|
||||
nodesAndCombos.forEach((item) => {
|
||||
const { parentId } = item.data;
|
||||
this.graphCore.getChildren(item.id, 'combo').forEach((child) => {
|
||||
userGraphCore.mergeNodeData(child.id, { parentId });
|
||||
});
|
||||
});
|
||||
}
|
||||
// update combo tree view and tree graph view
|
||||
nodesAndCombos.forEach((item) => {
|
||||
this.removeNode(item);
|
||||
});
|
||||
// remove the node
|
||||
userGraphCore.removeNodes(nodesAndCombos.map((node) => node.id));
|
||||
// graphCore.removeNodes(nodesAndCombos.map((node) => node.id));
|
||||
}
|
||||
if (prevEdges.length && edges.length) {
|
||||
// add or update edge
|
||||
const ids = edges
|
||||
.map((edge) => edge.id)
|
||||
.filter((id) => userGraphCore.hasEdge(id));
|
||||
userGraphCore.removeEdges(ids);
|
||||
.filter((id) => graphCore.hasEdge(id));
|
||||
graphCore.removeEdges(ids);
|
||||
}
|
||||
}
|
||||
|
||||
@ -450,7 +481,7 @@ export class DataController {
|
||||
* @param data data to be updated which is part of old one
|
||||
*/
|
||||
private updateData(dataConfig: DataConfig) {
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
const { type: dataType, data } = this.formatData(dataConfig);
|
||||
if (!dataType) return;
|
||||
const { nodes = [], edges = [], combos = [] } = data; //this.transformData(data as GraphData);
|
||||
@ -459,38 +490,35 @@ export class DataController {
|
||||
edges: prevEdges,
|
||||
combos: prevCombos,
|
||||
} = deconstructData({
|
||||
nodes: userGraphCore.getAllNodes(),
|
||||
edges: userGraphCore.getAllEdges(),
|
||||
nodes: graphCore.getAllNodes(),
|
||||
edges: graphCore.getAllEdges(),
|
||||
});
|
||||
if (prevNodes.length) {
|
||||
// update node
|
||||
nodes.forEach((newModel) => {
|
||||
const { id, data } = newModel;
|
||||
if (data) {
|
||||
const mergedData = mergeOneLevelData(
|
||||
userGraphCore.getNode(id),
|
||||
newModel,
|
||||
);
|
||||
userGraphCore.mergeNodeData(id, mergedData);
|
||||
const mergedData = mergeOneLevelData(graphCore.getNode(id), newModel);
|
||||
graphCore.mergeNodeData(id, mergedData);
|
||||
}
|
||||
if (data.hasOwnProperty('parentId')) {
|
||||
graphCore.setParent(id, data.parentId, 'combo');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (prevEdges.length) {
|
||||
// update edge
|
||||
edges.forEach((newModel) => {
|
||||
const oldModel = userGraphCore.getEdge(newModel.id);
|
||||
const oldModel = graphCore.getEdge(newModel.id);
|
||||
if (!oldModel) return;
|
||||
const { id, source, target, data } = newModel;
|
||||
if (source && oldModel.source !== source)
|
||||
userGraphCore.updateEdgeSource(id, source);
|
||||
graphCore.updateEdgeSource(id, source);
|
||||
if (target && oldModel.target !== target)
|
||||
userGraphCore.updateEdgeTarget(id, target);
|
||||
graphCore.updateEdgeTarget(id, target);
|
||||
if (data) {
|
||||
const mergedData = mergeOneLevelData(
|
||||
userGraphCore.getEdge(id),
|
||||
newModel,
|
||||
);
|
||||
userGraphCore.mergeEdgeData(id, mergedData);
|
||||
const mergedData = mergeOneLevelData(graphCore.getEdge(id), newModel);
|
||||
graphCore.mergeEdgeData(id, mergedData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -499,6 +527,7 @@ export class DataController {
|
||||
const modelsToMove = [];
|
||||
combos.forEach((newModel) => {
|
||||
const { id, data } = newModel;
|
||||
if (!data) return;
|
||||
const { x: comboNewX, y: comboNewY, ...others } = data;
|
||||
if (comboNewX !== undefined || comboNewY !== undefined) {
|
||||
if (this.graphCore.getChildren(id, 'combo').length) {
|
||||
@ -538,17 +567,20 @@ export class DataController {
|
||||
}
|
||||
// update other properties
|
||||
if (Object.keys(others).length) {
|
||||
const mergedData = mergeOneLevelData(userGraphCore.getNode(id), {
|
||||
const mergedData = mergeOneLevelData(graphCore.getNode(id), {
|
||||
id,
|
||||
data: others,
|
||||
});
|
||||
userGraphCore.mergeNodeData(id, mergedData);
|
||||
graphCore.mergeNodeData(id, mergedData);
|
||||
if (others.hasOwnProperty('parentId')) {
|
||||
graphCore.setParent(id, others.parentId, 'combo');
|
||||
}
|
||||
}
|
||||
});
|
||||
// update succeed nodes
|
||||
modelsToMove.forEach((newModel) => {
|
||||
const { id, data } = newModel;
|
||||
userGraphCore.mergeNodeData(id, data);
|
||||
graphCore.mergeNodeData(id, data);
|
||||
});
|
||||
}
|
||||
|
||||
@ -560,8 +592,8 @@ export class DataController {
|
||||
) {
|
||||
// convert and store tree structure to graphCore
|
||||
this.updateTreeGraph(dataType, {
|
||||
nodes: this.userGraphCore.getAllNodes(),
|
||||
edges: this.userGraphCore.getAllEdges(),
|
||||
nodes: this.graphCore.getAllNodes(),
|
||||
edges: this.graphCore.getAllEdges(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -589,7 +621,7 @@ export class DataController {
|
||||
}
|
||||
|
||||
return {
|
||||
type: type || 'graphData',
|
||||
type: type || this.dataType || 'graphData',
|
||||
data: data as GraphData,
|
||||
};
|
||||
}
|
||||
@ -640,15 +672,12 @@ export class DataController {
|
||||
}
|
||||
});
|
||||
// update succeed nodes
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
succeedNodesModels.forEach((newModel) => {
|
||||
const { id, data } = newModel;
|
||||
if (data) {
|
||||
const mergedData = mergeOneLevelData(
|
||||
userGraphCore.getNode(id),
|
||||
newModel,
|
||||
);
|
||||
userGraphCore.mergeNodeData(id, mergedData);
|
||||
const mergedData = mergeOneLevelData(graphCore.getNode(id), newModel);
|
||||
graphCore.mergeNodeData(id, mergedData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -656,17 +685,21 @@ export class DataController {
|
||||
private addCombo(data: GraphData) {
|
||||
const { combos = [] } = data;
|
||||
if (!combos?.length) return;
|
||||
const { userGraphCore } = this;
|
||||
const { graphCore } = this;
|
||||
const { id, data: comboData } = combos[0];
|
||||
const { _children = [], ...others } = comboData;
|
||||
|
||||
if (!graphCore.hasTreeStructure('combo')) {
|
||||
graphCore.attachTreeStructure('combo');
|
||||
}
|
||||
|
||||
// add or update combo
|
||||
if (userGraphCore.hasNode(id)) {
|
||||
if (graphCore.hasNode(id)) {
|
||||
// update node which is in the graphCore
|
||||
userGraphCore.mergeNodeData(id, { ...others, _isCombo: true });
|
||||
graphCore.mergeNodeData(id, { ...others, _isCombo: true });
|
||||
} else {
|
||||
// add node which is in data but not in graphCore
|
||||
userGraphCore.addNode({ id, data: { ...others, _isCombo: true } });
|
||||
graphCore.addNode({ id, data: { ...others, _isCombo: true } });
|
||||
}
|
||||
|
||||
// update strucutre
|
||||
@ -677,252 +710,20 @@ export class DataController {
|
||||
);
|
||||
return;
|
||||
}
|
||||
userGraphCore.mergeNodeData(childId, { parentId: id });
|
||||
graphCore.setParent(childId, id, 'combo');
|
||||
graphCore.mergeNodeData(childId, { parentId: id });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update graphCore with transformed userGraphCore data.
|
||||
*/
|
||||
private updateGraphCore(event) {
|
||||
const { graphCore } = this;
|
||||
|
||||
const {
|
||||
nodes = [],
|
||||
edges = [],
|
||||
combos = [],
|
||||
} = deconstructData({
|
||||
nodes: this.userGraphCore.getAllNodes(),
|
||||
edges: this.userGraphCore.getAllEdges(),
|
||||
});
|
||||
|
||||
const prevNodesAndCombos = graphCore.getAllNodes();
|
||||
|
||||
// function to update one data in graphCore with different model type ('node' or 'edge')
|
||||
const syncUpdateToGraphCore = (
|
||||
id,
|
||||
newValue,
|
||||
oldValue,
|
||||
isNodeOrCombo,
|
||||
diff = [],
|
||||
) => {
|
||||
if (isNodeOrCombo) {
|
||||
if (newValue.data) graphCore.updateNodeData(id, { ...newValue.data });
|
||||
} else {
|
||||
if (diff.includes('data'))
|
||||
graphCore.updateEdgeData(id, { ...newValue.data });
|
||||
// source and target may be changed
|
||||
if (diff.includes('source'))
|
||||
graphCore.updateEdgeSource(id, newValue.source);
|
||||
if (diff.includes('target'))
|
||||
graphCore.updateEdgeTarget(id, newValue.target);
|
||||
}
|
||||
};
|
||||
|
||||
graphCore.batch(() => {
|
||||
// === step 3: sync to graphCore according to the changes in userGraphCore ==
|
||||
const newModelMap: {
|
||||
[id: string]: {
|
||||
type: 'node' | 'edge' | 'combo';
|
||||
model: NodeModel | EdgeModel | ComboModel;
|
||||
};
|
||||
} = {};
|
||||
const parentMap: {
|
||||
[id: string]: {
|
||||
new: ID;
|
||||
old?: ID;
|
||||
};
|
||||
} = {};
|
||||
const changeMap: {
|
||||
[id: string]: boolean;
|
||||
} = {};
|
||||
const treeChanges = [];
|
||||
event.changes.forEach((change) => {
|
||||
const id = change.id || change.value?.id;
|
||||
if (id !== undefined) {
|
||||
changeMap[id] = true;
|
||||
return;
|
||||
}
|
||||
if (
|
||||
[
|
||||
'TreeStructureAttached',
|
||||
'TreeStructureChanged',
|
||||
'TreeStructureChanged',
|
||||
].includes(change.type)
|
||||
) {
|
||||
treeChanges.push(change);
|
||||
}
|
||||
});
|
||||
nodes.forEach((model) => {
|
||||
newModelMap[model.id] = { type: 'node', model };
|
||||
if (model.data.hasOwnProperty('parentId')) {
|
||||
parentMap[model.id] = {
|
||||
new: model.data.parentId,
|
||||
old: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
edges.forEach(
|
||||
(model) => (newModelMap[model.id] = { type: 'edge', model }),
|
||||
);
|
||||
combos.forEach((model) => {
|
||||
newModelMap[model.id] = { type: 'combo', model };
|
||||
if (model.data.hasOwnProperty('parentId')) {
|
||||
parentMap[model.id] = {
|
||||
new: model.data.parentId,
|
||||
old: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
prevNodesAndCombos.forEach((prevModel) => {
|
||||
const { id } = prevModel;
|
||||
if (
|
||||
parentMap[id]?.new !== undefined ||
|
||||
prevModel.data.parentId !== undefined
|
||||
) {
|
||||
parentMap[id] = {
|
||||
new: parentMap[id]?.new,
|
||||
old: prevModel.data.parentId,
|
||||
};
|
||||
} else {
|
||||
delete parentMap[id];
|
||||
}
|
||||
const { model: newModel } = newModelMap[id] || {};
|
||||
// remove
|
||||
if (!newModel) {
|
||||
// remove a combo, put the children to upper parent
|
||||
if (prevModel.data._isCombo) {
|
||||
graphCore.getChildren(id, 'combo').forEach((child) => {
|
||||
parentMap[child.id] = {
|
||||
...parentMap[child.id],
|
||||
new: prevModel.data.parentId,
|
||||
};
|
||||
});
|
||||
}
|
||||
// if it has combo parent, remove it from the parent's children list
|
||||
if (prevModel.data.parentId) {
|
||||
graphCore.setParent(id, undefined, 'combo');
|
||||
}
|
||||
|
||||
// for tree graph view, show the succeed nodes and edges
|
||||
const succeedIds = [];
|
||||
if (graphCore.hasTreeStructure('tree')) {
|
||||
graphCore.dfsTree(
|
||||
id,
|
||||
(child) => {
|
||||
succeedIds.push(child.id);
|
||||
},
|
||||
'tree',
|
||||
);
|
||||
const succeedEdgeIds = graphCore
|
||||
.getAllEdges()
|
||||
.filter(
|
||||
({ source, target }) =>
|
||||
succeedIds.includes(source) && succeedIds.includes(target),
|
||||
)
|
||||
.map((edge) => edge.id);
|
||||
this.graph.showItem(
|
||||
succeedIds
|
||||
.filter((succeedId) => succeedId !== id)
|
||||
.concat(succeedEdgeIds),
|
||||
);
|
||||
|
||||
// for tree graph view, remove the node from the parent's children list
|
||||
graphCore.setParent(id, undefined, 'tree');
|
||||
// for tree graph view, make the its children to be roots
|
||||
graphCore
|
||||
.getChildren(id, 'tree')
|
||||
.forEach((child) =>
|
||||
graphCore.setParent(child.id, undefined, 'tree'),
|
||||
);
|
||||
}
|
||||
// remove the node data
|
||||
graphCore.removeNode(id);
|
||||
delete parentMap[prevModel.id];
|
||||
}
|
||||
// update
|
||||
// || diffAt(newModel, prevModel, true)?.length
|
||||
else if (changeMap[id])
|
||||
syncUpdateToGraphCore(id, newModel, prevModel, true);
|
||||
// delete from the map indicates this model is visited
|
||||
delete newModelMap[id];
|
||||
});
|
||||
graphCore.getAllEdges().forEach((prevEdge) => {
|
||||
const { id } = prevEdge;
|
||||
const { model: newModel } = newModelMap[id] || {};
|
||||
// remove
|
||||
if (!newModel) graphCore.removeEdge(id);
|
||||
// update
|
||||
else {
|
||||
const diff = diffAt(newModel, prevEdge, false);
|
||||
if (diff?.length)
|
||||
syncUpdateToGraphCore(id, newModel, prevEdge, false, diff);
|
||||
}
|
||||
// delete from the map indicates this model is visited
|
||||
delete newModelMap[id];
|
||||
});
|
||||
// add
|
||||
Object.values(newModelMap).forEach(({ type, model }) => {
|
||||
if (type === 'node' || type === 'combo') graphCore.addNode(model);
|
||||
else if (type === 'edge') graphCore.addEdge(model as EdgeModel);
|
||||
});
|
||||
// update parents after adding all items
|
||||
Object.keys(parentMap).forEach((id) => {
|
||||
if (parentMap[id].new === parentMap[id].old) return;
|
||||
if (!validateComboStrucutre(this.graph, id, parentMap[id].new)) {
|
||||
graphCore.mergeNodeData(id, { parentId: parentMap[id].old });
|
||||
return;
|
||||
}
|
||||
graphCore.mergeNodeData(id, { parentId: parentMap[id].new });
|
||||
graphCore.setParent(id, parentMap[id].new, 'combo');
|
||||
// after remove from parent's children list, check whether the parent is empty
|
||||
// if so, update parent's position to be the child's
|
||||
if (parentMap[id].old !== undefined) {
|
||||
const parentChildren = graphCore.getChildren(
|
||||
parentMap[id].old,
|
||||
'combo',
|
||||
);
|
||||
const {
|
||||
x = 0,
|
||||
y = 0,
|
||||
z = 0,
|
||||
} = this.graph.getDisplayModel(parentMap[id].old)?.data || {};
|
||||
if (!parentChildren.length) {
|
||||
graphCore.mergeNodeData(parentMap[id].old, {
|
||||
x: convertToNumber(x),
|
||||
y: convertToNumber(y),
|
||||
z: convertToNumber(z),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update tree structure
|
||||
treeChanges.forEach((change) => {
|
||||
const { type, treeKey, nodeId, newParentId } = change;
|
||||
if (type === 'TreeStructureAttached') {
|
||||
graphCore.attachTreeStructure(treeKey);
|
||||
return;
|
||||
} else if (type === 'TreeStructureChanged') {
|
||||
graphCore.setParent(nodeId, newParentId, treeKey);
|
||||
return;
|
||||
} else if (type === 'TreeStructureDetached') {
|
||||
graphCore.detachTreeStructure(treeKey);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone data from userGraphCore, and run transforms
|
||||
* Clone data from graphCore, and run transforms
|
||||
* @returns transformed data and the id map list
|
||||
*/
|
||||
private transformData(data): GraphData {
|
||||
let dataCloned: GraphData = clone(data);
|
||||
// transform the data with transform extensions, output innerData and idMaps ===
|
||||
this.extensions.forEach(({ func, config }) => {
|
||||
dataCloned = func(dataCloned, config, this.userGraphCore);
|
||||
dataCloned = func(dataCloned, config, this.graphCore);
|
||||
});
|
||||
return dataCloned;
|
||||
}
|
||||
@ -933,12 +734,12 @@ export class DataController {
|
||||
* @param data
|
||||
*/
|
||||
private updateTreeGraph(dataType, data) {
|
||||
this.userGraphCore.attachTreeStructure('tree');
|
||||
this.graphCore.attachTreeStructure('tree');
|
||||
if (dataType === 'treeData') {
|
||||
// tree structure storing
|
||||
data.edges.forEach((edge) => {
|
||||
const { source, target } = edge;
|
||||
this.userGraphCore.setParent(target, source, 'tree');
|
||||
this.graphCore.setParent(target, source, 'tree');
|
||||
});
|
||||
} else {
|
||||
this.treeDirtyFlag = true;
|
||||
|
@ -177,6 +177,7 @@ 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.mapperchange.tap(this.onMapperChange.bind(this));
|
||||
this.graph.hooks.treecollapseexpand.tap(
|
||||
this.onTreeCollapseExpand.bind(this),
|
||||
);
|
||||
@ -427,10 +428,6 @@ export class ItemController {
|
||||
});
|
||||
};
|
||||
const debounceUpdateRelates = debounce(updateRelates, 16, false);
|
||||
const throttleUpdateRelates = throttle(updateRelates, 16, {
|
||||
leading: true,
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
Object.values(nodeComboUpdate).forEach((updateObj: any) => {
|
||||
const { isReplace, previous, current, id } = updateObj;
|
||||
@ -526,7 +523,7 @@ export class ItemController {
|
||||
nodeRelatedIdsToUpdate.add(edge.id);
|
||||
});
|
||||
|
||||
item.onframe = () => throttleUpdateRelates(nodeRelatedIdsToUpdate);
|
||||
item.onframe = () => updateRelates(nodeRelatedIdsToUpdate);
|
||||
let statesCache;
|
||||
if (
|
||||
innerModel.data._isCombo &&
|
||||
@ -556,7 +553,7 @@ export class ItemController {
|
||||
},
|
||||
500,
|
||||
{
|
||||
leading: false,
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
),
|
||||
@ -850,6 +847,16 @@ export class ItemController {
|
||||
});
|
||||
};
|
||||
|
||||
private onMapperChange = ({ type, mapper }) => {
|
||||
if (!mapper) return;
|
||||
this.itemMap.forEach((item) => {
|
||||
const itemTye = item.getType();
|
||||
if (itemTye !== type) return;
|
||||
item.mapper = mapper;
|
||||
item.update(item.model, undefined, false);
|
||||
});
|
||||
};
|
||||
|
||||
private onDestroy = () => {
|
||||
Object.values(this.itemMap).forEach((item) => item.destroy());
|
||||
// Fix OOM problem, since this map will hold all the refs of items.
|
||||
@ -860,12 +867,17 @@ export class ItemController {
|
||||
type: ITEM_TYPE | SHAPE_TYPE;
|
||||
id: ID;
|
||||
config: {
|
||||
style?: ShapeStyle;
|
||||
// Data to be merged into the transient item.
|
||||
data?: Record<string, any>;
|
||||
action: 'remove' | 'add' | 'update' | undefined;
|
||||
onlyDrawKeyShape?: boolean;
|
||||
style?: ShapeStyle;
|
||||
/** Data to be merged into the transient item. */
|
||||
data?: Record<string, any>;
|
||||
shapeIds?: string[];
|
||||
/** For type: 'edge' */
|
||||
drawSource?: boolean;
|
||||
/** For type: 'edge' */
|
||||
drawTarget?: boolean;
|
||||
upsertAncestors?: boolean;
|
||||
visible?: boolean;
|
||||
[shapeConfig: string]: unknown;
|
||||
};
|
||||
canvas: Canvas;
|
||||
@ -878,8 +890,11 @@ export class ItemController {
|
||||
data = {},
|
||||
capture,
|
||||
action,
|
||||
onlyDrawKeyShape,
|
||||
shapeIds,
|
||||
drawSource,
|
||||
drawTarget,
|
||||
upsertAncestors,
|
||||
visible = true,
|
||||
} = config as any;
|
||||
const isItemType = type === 'node' || type === 'edge' || type === 'combo';
|
||||
// Removing
|
||||
@ -902,6 +917,9 @@ export class ItemController {
|
||||
);
|
||||
}
|
||||
if (transientItem && !transientItem.destroyed) {
|
||||
if (!(transientItem as Node | Edge | Combo).getType?.()) {
|
||||
(transientItem as Group).remove();
|
||||
}
|
||||
transientItem.destroy();
|
||||
}
|
||||
this.transientItemMap.delete(id);
|
||||
@ -930,11 +948,11 @@ export class ItemController {
|
||||
this.transientItemMap,
|
||||
this.itemMap,
|
||||
graphCore,
|
||||
onlyDrawKeyShape,
|
||||
{ shapeIds, drawSource, drawTarget, visible },
|
||||
upsertAncestors,
|
||||
);
|
||||
|
||||
if (onlyDrawKeyShape) {
|
||||
if (shapeIds) {
|
||||
// only update node positions to cloned node container(group)
|
||||
if (
|
||||
(type === 'node' || type === 'combo') &&
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
DisplayObject,
|
||||
PointLike,
|
||||
Rect,
|
||||
Cursor,
|
||||
} from '@antv/g';
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import {
|
||||
@ -55,7 +56,9 @@ import type {
|
||||
import { FitViewRules, GraphTransformOptions } from '../types/view';
|
||||
import { changeRenderer, createCanvas } from '../util/canvas';
|
||||
import { formatPadding } from '../util/shape';
|
||||
import { getLayoutBounds } from '../util/layout';
|
||||
import { Plugin as PluginBase } from '../types/plugin';
|
||||
import { ComboMapper, EdgeMapper, NodeMapper } from '../types/spec';
|
||||
import {
|
||||
DataController,
|
||||
ExtensionController,
|
||||
@ -357,7 +360,11 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
main: Canvas;
|
||||
transient: Canvas;
|
||||
};
|
||||
}>({ name: 'init' }),
|
||||
}>({ name: 'themechange' }),
|
||||
mapperchange: new Hook<{
|
||||
type: ITEM_TYPE;
|
||||
mapper: NodeMapper | EdgeMapper | ComboMapper;
|
||||
}>({ name: 'mapperchange' }),
|
||||
treecollapseexpand: new Hook<{
|
||||
ids: ID[];
|
||||
animate: boolean;
|
||||
@ -370,12 +377,14 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
|
||||
private formatSpecification(spec: Specification<B, T>) {
|
||||
return {
|
||||
...this.specification,
|
||||
...spec,
|
||||
optimize: {
|
||||
tileBehavior: 2000,
|
||||
tileBehaviorSize: 1000,
|
||||
tileFirstRender: 10000,
|
||||
tileFirstRenderSize: 1000,
|
||||
...this.specification?.optimize,
|
||||
...spec.optimize,
|
||||
},
|
||||
};
|
||||
@ -412,6 +421,32 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the item display mapper for a specific item type.
|
||||
* @param {ITEM_TYPE} type - The type of item (node, edge, or combo).
|
||||
* @param {NodeMapper | EdgeMapper | ComboMapper} mapper - The mapper to be updated.
|
||||
* */
|
||||
public updateMapper(
|
||||
type: ITEM_TYPE,
|
||||
mapper: NodeMapper | EdgeMapper | ComboMapper,
|
||||
) {
|
||||
switch (type) {
|
||||
case 'node':
|
||||
this.specification.node = mapper as NodeMapper;
|
||||
break;
|
||||
case 'edge':
|
||||
this.specification.edge = mapper as EdgeMapper;
|
||||
break;
|
||||
case 'combo':
|
||||
this.specification.combo = mapper as ComboMapper;
|
||||
break;
|
||||
}
|
||||
this.hooks.mapperchange.emit({
|
||||
type,
|
||||
mapper,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the copy of specs(configurations).
|
||||
* @returns graph specs
|
||||
@ -447,15 +482,28 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
const { autoFit } = this.specification;
|
||||
if (autoFit) {
|
||||
if (autoFit === 'view') {
|
||||
await this.fitView();
|
||||
await this.fitView({ rules: { boundsType: 'layout' } });
|
||||
} else if (autoFit === 'center') {
|
||||
await this.fitCenter();
|
||||
await this.fitCenter('layout');
|
||||
} else {
|
||||
const { type, effectTiming, ...others } = autoFit;
|
||||
if (type === 'view') {
|
||||
await this.fitView(others as any, effectTiming);
|
||||
const { padding, rules } = others as {
|
||||
padding: Padding;
|
||||
rules: FitViewRules;
|
||||
};
|
||||
await this.fitView(
|
||||
{
|
||||
padding,
|
||||
rules: {
|
||||
...rules,
|
||||
boundsType: 'layout',
|
||||
},
|
||||
},
|
||||
effectTiming,
|
||||
);
|
||||
} else if (type === 'center') {
|
||||
await this.fitCenter(effectTiming);
|
||||
await this.fitCenter('layout', effectTiming);
|
||||
} else if (type === 'position') {
|
||||
// TODO: align
|
||||
await this.translateTo((others as any).position, effectTiming);
|
||||
@ -681,8 +729,8 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
*/
|
||||
public async fitView(
|
||||
options?: {
|
||||
padding: Padding;
|
||||
rules: FitViewRules;
|
||||
padding?: Padding;
|
||||
rules?: FitViewRules;
|
||||
},
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) {
|
||||
@ -690,13 +738,21 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
const [top, right, bottom, left] = padding
|
||||
? formatPadding(padding)
|
||||
: [0, 0, 0, 0];
|
||||
const { direction = 'both', ratioRule = 'min' } = rules || {};
|
||||
const {
|
||||
direction = 'both',
|
||||
ratioRule = 'min',
|
||||
boundsType = 'render',
|
||||
} = rules || {};
|
||||
|
||||
// Get the bounds of the whole graph.
|
||||
const {
|
||||
center: [graphCenterX, graphCenterY],
|
||||
halfExtents,
|
||||
} = this.canvas.document.documentElement.getBounds();
|
||||
} =
|
||||
boundsType === 'render'
|
||||
? // Get the bounds of the whole graph content.
|
||||
this.canvas.document.documentElement.getBounds()
|
||||
: // Get the bounds of the nodes positions while the graph content is not ready.
|
||||
getLayoutBounds(this);
|
||||
const origin = this.canvas.canvas2Viewport({
|
||||
x: graphCenterX,
|
||||
y: graphCenterY,
|
||||
@ -747,11 +803,18 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* Fit the graph center to the view center.
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public async fitCenter(effectTiming?: CameraAnimationOptions) {
|
||||
// Get the bounds of the whole graph.
|
||||
public async fitCenter(
|
||||
boundsType: 'render' | 'layout' = 'render',
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) {
|
||||
const {
|
||||
center: [graphCenterX, graphCenterY],
|
||||
} = this.canvas.document.documentElement.getBounds();
|
||||
} =
|
||||
boundsType === 'render'
|
||||
? // Get the bounds of the whole graph content.
|
||||
this.canvas.document.documentElement.getBounds()
|
||||
: // Get the bounds of the nodes positions while the graph content is not ready.
|
||||
getLayoutBounds(this);
|
||||
await this.translateTo(
|
||||
this.canvas.canvas2Viewport({ x: graphCenterX, y: graphCenterY }),
|
||||
effectTiming,
|
||||
@ -1054,19 +1117,21 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
const changes = event.changes;
|
||||
const timingParameters = {
|
||||
type: itemType,
|
||||
action: 'add',
|
||||
models,
|
||||
apiName: 'addData',
|
||||
changes,
|
||||
};
|
||||
this.emit('beforeitemchange', timingParameters);
|
||||
this.hooks.itemchange.emit({
|
||||
type: itemType,
|
||||
changes: graphCore.reduceChanges(event.changes),
|
||||
graphCore,
|
||||
theme: specification,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type: itemType,
|
||||
action: 'add',
|
||||
models,
|
||||
apiName: 'addData',
|
||||
changes,
|
||||
});
|
||||
this.emit('afteritemchange', timingParameters);
|
||||
});
|
||||
|
||||
const modelArr = isArray(models) ? models : [models];
|
||||
@ -1091,27 +1156,39 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
public removeData(itemType: ITEM_TYPE, ids: ID | ID[]) {
|
||||
const idArr = isArray(ids) ? ids : [ids];
|
||||
const data = { nodes: [], edges: [], combos: [] };
|
||||
const { userGraphCore, graphCore } = this.dataController;
|
||||
const { graphCore } = this.dataController;
|
||||
const { specification } = this.themeController;
|
||||
const getItem =
|
||||
itemType === 'edge' ? userGraphCore.getEdge : userGraphCore.getNode;
|
||||
data[`${itemType}s`] = idArr.map((id) => getItem.bind(userGraphCore)(id));
|
||||
const getItem = itemType === 'edge' ? graphCore.getEdge : graphCore.getNode;
|
||||
const hasItem = itemType === 'edge' ? graphCore.hasEdge : graphCore.hasNode;
|
||||
data[`${itemType}s`] = idArr
|
||||
.map((id) => {
|
||||
if (!hasItem.bind(graphCore)(id)) {
|
||||
console.warn(
|
||||
`The ${itemType} data with id ${id} does not exist. It will be ignored`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
return getItem.bind(graphCore)(id);
|
||||
})
|
||||
.filter(Boolean);
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
const changes = event.changes;
|
||||
const timingParameters = {
|
||||
type: itemType,
|
||||
action: 'remove',
|
||||
ids: idArr,
|
||||
apiName: 'removeData',
|
||||
changes,
|
||||
};
|
||||
this.emit('beforeitemchange', timingParameters);
|
||||
this.hooks.itemchange.emit({
|
||||
type: itemType,
|
||||
changes: event.changes,
|
||||
graphCore,
|
||||
theme: specification,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type: itemType,
|
||||
action: 'remove',
|
||||
ids: idArr,
|
||||
apiName: 'removeData',
|
||||
changes,
|
||||
});
|
||||
this.emit('afteritemchange', timingParameters);
|
||||
});
|
||||
this.hooks.datachange.emit({
|
||||
data,
|
||||
@ -1204,19 +1281,21 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
const { specification } = this.themeController;
|
||||
graphCore.once('changed', (event) => {
|
||||
const changes = this.extendChanges(clone(event.changes));
|
||||
const timingParameters = {
|
||||
type: itemType,
|
||||
action: 'update',
|
||||
models,
|
||||
apiName: 'updateData',
|
||||
changes,
|
||||
};
|
||||
this.emit('beforeitemchange', timingParameters);
|
||||
this.hooks.itemchange.emit({
|
||||
type: itemType,
|
||||
changes: event.changes,
|
||||
graphCore,
|
||||
theme: specification,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type: itemType,
|
||||
action: 'update',
|
||||
models,
|
||||
apiName: 'updateData',
|
||||
changes,
|
||||
});
|
||||
this.emit('afteritemchange', timingParameters);
|
||||
});
|
||||
|
||||
this.hooks.datachange.emit({
|
||||
@ -1325,6 +1404,15 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
const changes = event.changes.filter(
|
||||
(change) => !isEqual(change.newValue, change.oldValue),
|
||||
);
|
||||
const timingParameters = {
|
||||
type,
|
||||
action: 'updatePosition',
|
||||
upsertAncestors,
|
||||
models,
|
||||
apiName: 'updatePosition',
|
||||
changes,
|
||||
};
|
||||
this.emit('beforeitemchange', timingParameters);
|
||||
this.hooks.itemchange.emit({
|
||||
type,
|
||||
changes: event.changes,
|
||||
@ -1335,14 +1423,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
animate: !disableAnimate,
|
||||
callback,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type,
|
||||
action: 'updatePosition',
|
||||
upsertAncestors,
|
||||
models,
|
||||
apiName: 'updatePosition',
|
||||
changes,
|
||||
});
|
||||
this.emit('afteritemchange', timingParameters);
|
||||
});
|
||||
|
||||
this.hooks.datachange.emit({
|
||||
@ -1617,19 +1698,21 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
const changes = event.changes;
|
||||
const timingParameters = {
|
||||
type: 'combo',
|
||||
action: 'add',
|
||||
models: [model],
|
||||
apiName: 'addCombo',
|
||||
changes,
|
||||
};
|
||||
this.emit('beforeitemchange', timingParameters);
|
||||
this.hooks.itemchange.emit({
|
||||
type: 'combo',
|
||||
changes: graphCore.reduceChanges(event.changes),
|
||||
graphCore,
|
||||
theme: specification,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type: 'combo',
|
||||
action: 'add',
|
||||
models: [model],
|
||||
apiName: 'addCombo',
|
||||
changes,
|
||||
});
|
||||
this.emit('afteritemchange', timingParameters);
|
||||
});
|
||||
|
||||
const data = {
|
||||
@ -1715,6 +1798,17 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
const changes = this.extendChanges(clone(event.changes));
|
||||
const timingParameters = {
|
||||
type: 'combo',
|
||||
ids: idArr,
|
||||
dx,
|
||||
dy,
|
||||
action: 'updatePosition',
|
||||
upsertAncestors,
|
||||
apiName: 'moveCombo',
|
||||
changes,
|
||||
};
|
||||
this.emit('beforeitemchange', timingParameters);
|
||||
this.hooks.itemchange.emit({
|
||||
type: 'combo',
|
||||
changes: event.changes,
|
||||
@ -1724,16 +1818,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
action: 'updatePosition',
|
||||
callback,
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type: 'combo',
|
||||
ids: idArr,
|
||||
dx,
|
||||
dy,
|
||||
action: 'updatePosition',
|
||||
upsertAncestors,
|
||||
apiName: 'moveCombo',
|
||||
changes,
|
||||
});
|
||||
this.emit('afteritemchange', timingParameters);
|
||||
});
|
||||
|
||||
this.hooks.datachange.emit({
|
||||
@ -1832,6 +1917,15 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
return this.interactionController.getMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cursor. But the cursor in item's style has higher priority.
|
||||
* @param cursor
|
||||
*/
|
||||
public setCursor(cursor: Cursor) {
|
||||
this.canvas.setCursor(cursor);
|
||||
this.transientCanvas.setCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add behavior(s) to mode(s).
|
||||
* @param behaviors behavior names or configs
|
||||
@ -2049,9 +2143,19 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
type: ITEM_TYPE | SHAPE_TYPE,
|
||||
id: ID,
|
||||
config: {
|
||||
action: 'remove' | 'add' | 'update' | undefined;
|
||||
style: ShapeStyle;
|
||||
onlyDrawKeyShape?: boolean;
|
||||
action?: 'remove' | 'add' | 'update' | undefined;
|
||||
/** Data to be merged into the transient item. */
|
||||
data?: Record<string, any>;
|
||||
/** Style to be merged into the transient shape. */
|
||||
style?: ShapeStyle;
|
||||
/** For type: 'edge' */
|
||||
drawSource?: boolean;
|
||||
/** For type: 'edge' */
|
||||
drawTarget?: boolean;
|
||||
/** Only shape with id in shapeIds will be cloned while type is ITEM_TYPE. If shapeIds is not assigned, the whole item will be cloned. */
|
||||
shapeIds?: string[];
|
||||
/** Whether show the shapes in shapeIds. True by default. */
|
||||
visible?: boolean;
|
||||
upsertAncestors?: boolean;
|
||||
},
|
||||
canvas?: Canvas,
|
||||
@ -2375,7 +2479,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
protected asyncToFullDataUrl(
|
||||
type?: DataURLType,
|
||||
imageConfig?: { padding?: number | number[] },
|
||||
callback?: Function,
|
||||
callback?: (dataUrl: string) => void,
|
||||
): void {
|
||||
let dataURL = '';
|
||||
if (!type) type = 'image/png';
|
||||
|
@ -345,8 +345,10 @@ export class BrushSelect extends Behavior {
|
||||
const { graph, options } = this;
|
||||
const { brushStyle } = options;
|
||||
const brush = graph.drawTransient('rect', BRUSH_SHAPE_ID, {
|
||||
style: brushStyle,
|
||||
capture: false,
|
||||
style: {
|
||||
...brushStyle,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
});
|
||||
return brush;
|
||||
}
|
||||
|
296
packages/g6/src/stdlib/behavior/create-edge.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import type { IG6GraphEvent } from '../../types';
|
||||
import { warn } from '../../util/warn';
|
||||
import { generateEdgeID } from '../../util/item';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { EdgeDisplayModelData } from '../../types/edge';
|
||||
|
||||
const KEYBOARD_TRIGGERS = ['shift', 'ctrl', 'control', 'alt', 'meta'] as const;
|
||||
const EVENT_TRIGGERS = ['click', 'drag'] as const;
|
||||
|
||||
const VIRTUAL_EDGE_ID = 'g6-create-edge-virtual-edge';
|
||||
const DUMMY_NODE_ID = 'g6-create-edge-dummy-node';
|
||||
|
||||
// type Trigger = (typeof EVENT_TRIGGERS)[number];
|
||||
|
||||
interface CreateEdgeOptions {
|
||||
/**
|
||||
* The triggering conditions for this interaction can be either 'click' or 'drag'.
|
||||
* Default to `click`.
|
||||
*/
|
||||
trigger: (typeof EVENT_TRIGGERS)[number];
|
||||
/**
|
||||
* The assistant secondary key on keyboard. If it is not assigned, the behavior will be triggered when trigger happens.
|
||||
* cound be 'shift', 'ctrl', 'control', 'alt', 'meta', undefined.
|
||||
*/
|
||||
secondaryKey?: (typeof KEYBOARD_TRIGGERS)[number];
|
||||
/**
|
||||
* Config of the created edge.
|
||||
*/
|
||||
edgeConfig: EdgeDisplayModelData;
|
||||
/**
|
||||
* The event name to trigger after creating the virtual edge.
|
||||
*/
|
||||
createVirtualEventName?: string;
|
||||
/**
|
||||
* The event name to trigger after creating the actual edge.
|
||||
*/
|
||||
createActualEventName?: string;
|
||||
/**
|
||||
* The event name to trigger after canceling the behavior.
|
||||
*/
|
||||
cancelCreateEventName?: string;
|
||||
/**
|
||||
* Whether allow the behavior happen on the current item.
|
||||
*/
|
||||
shouldBegin: (event: IG6GraphEvent) => boolean;
|
||||
/**
|
||||
*
|
||||
* Whether it is allowed to end the creation of edges under the current conditions being operated.
|
||||
*/
|
||||
shouldEnd: (event: IG6GraphEvent) => boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: CreateEdgeOptions = {
|
||||
trigger: 'click',
|
||||
secondaryKey: undefined,
|
||||
shouldBegin: () => true,
|
||||
shouldEnd: () => false,
|
||||
edgeConfig: {},
|
||||
};
|
||||
|
||||
export class CreateEdge extends Behavior {
|
||||
isKeyDown = false;
|
||||
addingEdge = null;
|
||||
dummyNode = null;
|
||||
|
||||
constructor(options: Partial<CreateEdgeOptions>) {
|
||||
super(Object.assign({}, DEFAULT_OPTIONS, options));
|
||||
this.validateOptions(options);
|
||||
}
|
||||
|
||||
validateOptions(options: Partial<CreateEdgeOptions>) {
|
||||
if (options.trigger && !EVENT_TRIGGERS.includes(options.trigger)) {
|
||||
warn({
|
||||
optionName: `create-edge.trigger`,
|
||||
shouldBe: EVENT_TRIGGERS,
|
||||
now: options.trigger,
|
||||
scope: 'behavior',
|
||||
});
|
||||
|
||||
this.options.trigger = DEFAULT_OPTIONS.trigger;
|
||||
}
|
||||
|
||||
if (
|
||||
options.secondaryKey &&
|
||||
!KEYBOARD_TRIGGERS.includes(options.secondaryKey)
|
||||
) {
|
||||
warn({
|
||||
optionName: `create-edge.secondaryKey`,
|
||||
shouldBe: KEYBOARD_TRIGGERS,
|
||||
now: options.secondaryKey,
|
||||
scope: 'behavior',
|
||||
});
|
||||
this.options.secondaryKey = DEFAULT_OPTIONS.secondaryKey;
|
||||
}
|
||||
}
|
||||
|
||||
getEvents = () => {
|
||||
const { trigger, secondaryKey } = this.options;
|
||||
const [CLICK_NAME] = EVENT_TRIGGERS;
|
||||
|
||||
const triggerEvents =
|
||||
trigger === CLICK_NAME
|
||||
? {
|
||||
'node:click': this.handleCreateEdge,
|
||||
pointermove: this.updateEndPoint,
|
||||
'edge:click': this.cancelCreating,
|
||||
'canvas:click': this.cancelCreating,
|
||||
'combo:click': this.handleCreateEdge,
|
||||
}
|
||||
: {
|
||||
'node:dragstart': this.handleCreateEdge,
|
||||
'combo:dragstart': this.handleCreateEdge,
|
||||
drag: this.updateEndPoint,
|
||||
drop: this.onDrop,
|
||||
};
|
||||
|
||||
const keyboardEvents = secondaryKey
|
||||
? {
|
||||
keydown: this.onKeyDown,
|
||||
keyup: this.onKeyUp,
|
||||
}
|
||||
: {};
|
||||
|
||||
return { ...triggerEvents, ...keyboardEvents } as Record<
|
||||
string,
|
||||
(e: IG6GraphEvent) => void
|
||||
>;
|
||||
};
|
||||
|
||||
handleCreateEdge = (e: IG6GraphEvent) => {
|
||||
if (this.options.secondaryKey && !this.isKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.shouldEnd(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { graph, options, addingEdge } = this;
|
||||
const currentNodeId = e.itemId;
|
||||
|
||||
const { edgeConfig, createVirtualEventName, createActualEventName } =
|
||||
options;
|
||||
|
||||
if (addingEdge) {
|
||||
// create edge end, add the actual edge to graph and remove the virtual edge and node
|
||||
const actualEdge = graph.addData('edge', {
|
||||
id: generateEdgeID(addingEdge.source, currentNodeId),
|
||||
source: addingEdge.source,
|
||||
target: currentNodeId,
|
||||
data: {
|
||||
...edgeConfig,
|
||||
type:
|
||||
currentNodeId === addingEdge.source ? 'loop-edge' : edgeConfig.type,
|
||||
},
|
||||
});
|
||||
if (createActualEventName) {
|
||||
graph.emit(createActualEventName, { edge: actualEdge });
|
||||
}
|
||||
this.cancelCreating();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.dummyNode = graph.addData('node', {
|
||||
id: DUMMY_NODE_ID,
|
||||
data: {
|
||||
x: e.canvas.x,
|
||||
y: e.canvas.y,
|
||||
keyShape: {
|
||||
opacity: 0,
|
||||
interactive: false,
|
||||
},
|
||||
labelShape: {
|
||||
opacity: 0,
|
||||
},
|
||||
anchorPoints: [[0.5, 0.5]],
|
||||
},
|
||||
});
|
||||
this.addingEdge = graph.addData('edge', {
|
||||
id: VIRTUAL_EDGE_ID,
|
||||
source: currentNodeId,
|
||||
target: DUMMY_NODE_ID,
|
||||
data: {
|
||||
...edgeConfig,
|
||||
},
|
||||
});
|
||||
if (createVirtualEventName) {
|
||||
graph.emit(createVirtualEventName, { edge: this.addingEdge });
|
||||
}
|
||||
};
|
||||
|
||||
onDrop = async (e: IG6GraphEvent) => {
|
||||
const { addingEdge, options, graph } = this;
|
||||
|
||||
const { edgeConfig, secondaryKey, createActualEventName } = options;
|
||||
if (secondaryKey && !this.isKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addingEdge) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = await this.graph.canvas.document.elementsFromPoint(
|
||||
e.canvas.x,
|
||||
e.canvas.y,
|
||||
);
|
||||
const currentIds = elements
|
||||
// @ts-ignore TODO: G type
|
||||
.map((ele) => ele.parentNode.getAttribute?.('data-item-id'))
|
||||
.filter((id) => id !== undefined && !DUMMY_NODE_ID !== id);
|
||||
const dropId = currentIds.find(
|
||||
(id) => this.graph.getComboData(id) || this.graph.getNodeData(id),
|
||||
);
|
||||
|
||||
if (!dropId) {
|
||||
this.cancelCreating();
|
||||
return;
|
||||
}
|
||||
|
||||
const actualEdge = graph.addData('edge', {
|
||||
id: generateEdgeID(addingEdge.source, dropId),
|
||||
source: addingEdge.source,
|
||||
target: dropId,
|
||||
data: {
|
||||
...edgeConfig,
|
||||
type: dropId === addingEdge.source ? 'loop-edge' : edgeConfig.type,
|
||||
},
|
||||
});
|
||||
if (createActualEventName) {
|
||||
graph.emit(createActualEventName, { edge: actualEdge });
|
||||
}
|
||||
this.cancelCreating();
|
||||
};
|
||||
|
||||
updateEndPoint = (e: IG6GraphEvent) => {
|
||||
const { options, graph, addingEdge, isKeyDown } = this;
|
||||
if (options.secondaryKey && !isKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addingEdge) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceId = addingEdge.source,
|
||||
targetId = addingEdge.target;
|
||||
|
||||
if (!graph.getItemById(sourceId)) {
|
||||
this.addingEdge = null;
|
||||
return;
|
||||
}
|
||||
|
||||
graph.updatePosition('node', {
|
||||
id: targetId,
|
||||
data: {
|
||||
x: e.canvas.x,
|
||||
y: e.canvas.y,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
cancelCreating = () => {
|
||||
if (this.addingEdge) {
|
||||
this.graph.removeData('edge', VIRTUAL_EDGE_ID);
|
||||
this.addingEdge = null;
|
||||
}
|
||||
if (this.dummyNode) {
|
||||
this.graph.removeData('node', DUMMY_NODE_ID);
|
||||
this.dummyNode = null;
|
||||
}
|
||||
if (this.options.cancelCreateEventName) {
|
||||
this.graph.emit(this.options.cancelCreateEventName, {});
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
const code = e.key;
|
||||
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.toLocaleLowerCase() === this.options.secondaryKey) {
|
||||
this.isKeyDown = true;
|
||||
}
|
||||
};
|
||||
|
||||
onKeyUp = (e: IG6GraphEvent) => {
|
||||
if (this.addingEdge) {
|
||||
this.cancelCreating();
|
||||
}
|
||||
this.isKeyDown = false;
|
||||
};
|
||||
}
|
@ -74,6 +74,7 @@ export class DragCanvas extends Behavior {
|
||||
private disableKeydown: boolean;
|
||||
private hiddenEdgeIds: ID[];
|
||||
private hiddenNodeIds: ID[];
|
||||
private tileRequestId?: number;
|
||||
|
||||
constructor(options: Partial<DragCanvasOptions>) {
|
||||
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options);
|
||||
@ -136,7 +137,10 @@ export class DragCanvas extends Behavior {
|
||||
|
||||
const { tileBehavior: graphBehaviorOptimize, tileBehaviorSize = 1000 } =
|
||||
graph.getSpecification().optimize || {};
|
||||
const optimize = this.options.enableOptimize || graphBehaviorOptimize;
|
||||
const optimize =
|
||||
this.options.enableOptimize !== undefined
|
||||
? this.options.enableOptimize
|
||||
: graphBehaviorOptimize;
|
||||
const shouldOptimize = isNumber(optimize)
|
||||
? graph.getAllNodesData().length > optimize
|
||||
: optimize;
|
||||
@ -150,7 +154,6 @@ export class DragCanvas extends Behavior {
|
||||
.getAllNodesData()
|
||||
.map((node) => node.id)
|
||||
.filter((id) => graph.getItemVisible(id) === true);
|
||||
let requestId;
|
||||
const hiddenIds = [...this.hiddenNodeIds];
|
||||
const sectionNum = Math.ceil(hiddenIds.length / tileBehaviorSize);
|
||||
const sections = Array.from({ length: sectionNum }, (v, i) =>
|
||||
@ -160,17 +163,18 @@ export class DragCanvas extends Behavior {
|
||||
),
|
||||
);
|
||||
const update = () => {
|
||||
if (!sections.length) {
|
||||
cancelAnimationFrame(requestId);
|
||||
if (!sections.length && this.tileRequestId) {
|
||||
cancelAnimationFrame(this.tileRequestId);
|
||||
this.tileRequestId = undefined;
|
||||
return;
|
||||
}
|
||||
const section = sections.shift();
|
||||
graph.hideItem(section, false, true);
|
||||
requestId = requestAnimationFrame(update);
|
||||
graph.executeWithNoStack(() => {
|
||||
graph.hideItem(section, false, true);
|
||||
});
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
};
|
||||
graph.executeWithNoStack(() => {
|
||||
requestId = requestAnimationFrame(update);
|
||||
});
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,14 +268,20 @@ export class DragCanvas extends Behavior {
|
||||
const { graph, hiddenNodeIds, hiddenEdgeIds = [] } = this;
|
||||
const { tileBehavior: graphBehaviorOptimize, tileBehaviorSize = 1000 } =
|
||||
graph.getSpecification().optimize || {};
|
||||
const optimize = this.options.enableOptimize || graphBehaviorOptimize;
|
||||
const optimize =
|
||||
this.options.enableOptimize !== undefined
|
||||
? this.options.enableOptimize
|
||||
: graphBehaviorOptimize;
|
||||
const shouldOptimize = isNumber(optimize)
|
||||
? graph.getAllNodesData().length > optimize
|
||||
: optimize;
|
||||
|
||||
if (shouldOptimize) {
|
||||
if (this.tileRequestId) {
|
||||
cancelAnimationFrame(this.tileRequestId);
|
||||
this.tileRequestId = undefined;
|
||||
}
|
||||
if (hiddenNodeIds) {
|
||||
let requestId;
|
||||
const hiddenIds = [...hiddenNodeIds, ...hiddenEdgeIds];
|
||||
const sectionNum = Math.ceil(hiddenIds.length / tileBehaviorSize);
|
||||
const sections = Array.from({ length: sectionNum }, (v, i) =>
|
||||
@ -281,16 +291,17 @@ export class DragCanvas extends Behavior {
|
||||
),
|
||||
);
|
||||
const update = () => {
|
||||
if (!sections.length) {
|
||||
cancelAnimationFrame(requestId);
|
||||
if (!sections.length && this.tileRequestId) {
|
||||
cancelAnimationFrame(this.tileRequestId);
|
||||
this.tileRequestId = undefined;
|
||||
return;
|
||||
}
|
||||
graph.startHistoryBatch();
|
||||
graph.showItem(sections.shift(), false);
|
||||
requestId = requestAnimationFrame(update);
|
||||
graph.stopHistoryBatch();
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
};
|
||||
graph.startHistoryBatch();
|
||||
requestId = requestAnimationFrame(update);
|
||||
graph.stopHistoryBatch();
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -526,8 +526,8 @@ export class DragCombo extends Behavior {
|
||||
this.originPositions = [];
|
||||
}
|
||||
|
||||
public onDropNode(event: IG6GraphEvent) {
|
||||
const elements = this.graph.canvas.document.elementsFromPointSync(
|
||||
public async onDropNode(event: IG6GraphEvent) {
|
||||
const elements = await this.graph.canvas.document.elementsFromPoint(
|
||||
event.canvas.x,
|
||||
event.canvas.y,
|
||||
);
|
||||
|
@ -457,7 +457,7 @@ export class DragNode extends Behavior {
|
||||
this.graph.drawTransient('rect', DELEGATE_SHAPE_ID, { action: 'remove' });
|
||||
}
|
||||
|
||||
public clearTransientItems() {
|
||||
public clearTransientItems(positions: Array<Position>) {
|
||||
this.hiddenEdges.forEach((edge) => {
|
||||
this.graph.drawTransient('node', edge.source, { action: 'remove' });
|
||||
this.graph.drawTransient('node', edge.target, { action: 'remove' });
|
||||
@ -472,7 +472,7 @@ export class DragNode extends Behavior {
|
||||
action: 'remove',
|
||||
});
|
||||
});
|
||||
this.originPositions.forEach(({ id }) => {
|
||||
positions.forEach(({ id }) => {
|
||||
this.graph.drawTransient('node', id, { action: 'remove' });
|
||||
});
|
||||
}
|
||||
@ -543,7 +543,7 @@ export class DragNode extends Behavior {
|
||||
debounce((positions) => {
|
||||
// restore the hidden items after move real nodes done
|
||||
if (enableTransient) {
|
||||
this.clearTransientItems();
|
||||
this.clearTransientItems(positions);
|
||||
}
|
||||
|
||||
if (this.options.enableDelegate) {
|
||||
@ -573,7 +573,7 @@ export class DragNode extends Behavior {
|
||||
return;
|
||||
}
|
||||
this.clearDelegate();
|
||||
this.clearTransientItems();
|
||||
this.clearTransientItems(this.originPositions);
|
||||
this.restoreHiddenItems();
|
||||
|
||||
const enableTransient =
|
||||
@ -589,8 +589,8 @@ export class DragNode extends Behavior {
|
||||
this.clearState();
|
||||
}
|
||||
|
||||
public onDropNode(event: IG6GraphEvent) {
|
||||
const elements = this.graph.canvas.document.elementsFromPointSync(
|
||||
public async onDropNode(event: IG6GraphEvent) {
|
||||
const elements = await this.graph.canvas.document.elementsFromPoint(
|
||||
event.canvas.x,
|
||||
event.canvas.y,
|
||||
);
|
||||
@ -640,8 +640,8 @@ export class DragNode extends Behavior {
|
||||
this.graph.stopHistoryBatch();
|
||||
}
|
||||
|
||||
public onDropCanvas(event: IG6GraphEvent) {
|
||||
const elements = this.graph.canvas.document.elementsFromPointSync(
|
||||
public async onDropCanvas(event: IG6GraphEvent) {
|
||||
const elements = await this.graph.canvas.document.elementsFromPoint(
|
||||
event.canvas.x,
|
||||
event.canvas.y,
|
||||
);
|
||||
|
@ -13,5 +13,6 @@ export * from './orbit-canvas-3d';
|
||||
export * from './rotate-canvas-3d';
|
||||
export * from './track-canvas-3d';
|
||||
export * from './zoom-canvas-3d';
|
||||
export * from './create-edge';
|
||||
export * from './shortcuts-call';
|
||||
export * from './scroll-canvas';
|
||||
|
@ -61,8 +61,10 @@ export class LassoSelect extends BrushSelect {
|
||||
const { graph, options } = this;
|
||||
const { brushStyle } = options;
|
||||
return graph.drawTransient('path', LASSO_SHAPE_ID, {
|
||||
style: brushStyle,
|
||||
capture: false,
|
||||
style: {
|
||||
pointerEvents: 'none',
|
||||
...brushStyle,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -82,7 +84,7 @@ export class LassoSelect extends BrushSelect {
|
||||
|
||||
getLassoPath = () => {
|
||||
const points: Point[] = this.points;
|
||||
const path = [];
|
||||
const path: any = [];
|
||||
if (points.length) {
|
||||
points.forEach((point, index) => {
|
||||
if (index === 0) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isBoolean, isObject } from '@antv/util';
|
||||
import { isBoolean, isNumber, isObject } from '@antv/util';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { ID, IG6GraphEvent } from '../../types';
|
||||
|
||||
@ -51,10 +51,6 @@ const DEFAULT_OPTIONS: ScrollCanvasOptions = {
|
||||
direction: 'both',
|
||||
enableOptimize: false,
|
||||
zoomKey: 'ctrl',
|
||||
// scroll-canvas 可滚动的扩展范围,默认为 0,即最多可以滚动一屏的位置
|
||||
// 当设置的值大于 0 时,即滚动可以超过一屏
|
||||
// 当设置的值小于 0 时,相当于缩小了可滚动范围
|
||||
// 具体实例可参考:https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*IFfoS67_HssAAAAAAAAAAAAAARQnAQ
|
||||
scalableRange: 0,
|
||||
allowDragOnItem: true,
|
||||
zoomRatio: 0.05,
|
||||
@ -83,8 +79,7 @@ export class ScrollCanvas extends Behavior {
|
||||
onWheel(ev: IG6GraphEvent & { deltaX?: number; deltaY?: number }) {
|
||||
if (!this.allowDrag(ev)) return;
|
||||
const graph = this.graph;
|
||||
const { zoomKey, zoomRatio, scalableRange, direction, enableOptimize } =
|
||||
this.options;
|
||||
const { zoomKey, zoomRatio, enableOptimize } = this.options;
|
||||
const zoomKeys = Array.isArray(zoomKey) ? [].concat(zoomKey) : [zoomKey];
|
||||
if (zoomKeys.includes('control')) zoomKeys.push('ctrl');
|
||||
const keyDown = zoomKeys.some((ele) => ev[`${ele}Key`]);
|
||||
@ -223,8 +218,14 @@ export class ScrollCanvas extends Behavior {
|
||||
|
||||
private hideShapes() {
|
||||
const { graph, options } = this;
|
||||
const { tileBehavior: graphBehaviorOptimize, tileBehaviorSize = 1000 } =
|
||||
graph.getSpecification().optimize || {};
|
||||
const { optimizeZoom } = options;
|
||||
if (this.options.enableOptimize) {
|
||||
const optimize = this.options.enableOptimize || graphBehaviorOptimize;
|
||||
const shouldOptimzie = isNumber(optimize)
|
||||
? graph.getAllNodesData().length > optimize
|
||||
: optimize;
|
||||
if (shouldOptimzie) {
|
||||
const currentZoom = graph.getZoom();
|
||||
const newHiddenEdgeIds = graph
|
||||
.getAllEdgesData()
|
||||
@ -242,14 +243,27 @@ export class ScrollCanvas extends Behavior {
|
||||
.getAllNodesData()
|
||||
.map((node) => node.id)
|
||||
.filter((id) => graph.getItemVisible(id));
|
||||
// draw node's keyShapes on transient, and then hidden the real nodes;
|
||||
newHiddenNodeIds.forEach((id) => {
|
||||
graph.drawTransient('node', id, {
|
||||
onlyDrawKeyShape: true,
|
||||
upsertAncestors: false,
|
||||
});
|
||||
});
|
||||
graph.hideItem(newHiddenNodeIds, true);
|
||||
|
||||
let requestId;
|
||||
const sectionNum = Math.ceil(newHiddenNodeIds.length / tileBehaviorSize);
|
||||
const sections = Array.from({ length: sectionNum }, (v, i) =>
|
||||
newHiddenNodeIds.slice(
|
||||
i * tileBehaviorSize,
|
||||
i * tileBehaviorSize + tileBehaviorSize,
|
||||
),
|
||||
);
|
||||
const update = () => {
|
||||
if (!sections.length) {
|
||||
cancelAnimationFrame(requestId);
|
||||
return;
|
||||
}
|
||||
const section = sections.shift();
|
||||
graph.startHistoryBatch();
|
||||
graph.hideItem(section, false, true);
|
||||
graph.stopHistoryBatch();
|
||||
requestId = requestAnimationFrame(update);
|
||||
};
|
||||
requestId = requestAnimationFrame(update);
|
||||
|
||||
if (currentZoom < optimizeZoom) {
|
||||
this.hiddenNodeIds.push(...newHiddenNodeIds);
|
||||
@ -269,20 +283,39 @@ export class ScrollCanvas extends Behavior {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hiddenEdgeIds = this.hiddenNodeIds = [];
|
||||
if (!this.options.enableOptimize) {
|
||||
const { tileBehavior: graphBehaviorOptimize, tileBehaviorSize = 1000 } =
|
||||
graph.getSpecification().optimize || {};
|
||||
const optimize = this.options.enableOptimize || graphBehaviorOptimize;
|
||||
const shouldOptimzie = isNumber(optimize)
|
||||
? graph.getAllNodesData().length > optimize
|
||||
: optimize;
|
||||
if (!shouldOptimzie) {
|
||||
this.hiddenEdgeIds = this.hiddenNodeIds = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (hiddenEdgeIds) {
|
||||
graph.showItem(hiddenEdgeIds, true);
|
||||
}
|
||||
if (hiddenNodeIds) {
|
||||
hiddenNodeIds.forEach((id) => {
|
||||
this.graph.drawTransient('node', id, { action: 'remove' });
|
||||
let requestId;
|
||||
const hiddenIds = [...hiddenNodeIds, ...hiddenEdgeIds];
|
||||
const sectionNum = Math.ceil(hiddenIds.length / tileBehaviorSize);
|
||||
const sections = Array.from({ length: sectionNum }, (v, i) =>
|
||||
hiddenIds.slice(
|
||||
i * tileBehaviorSize,
|
||||
i * tileBehaviorSize + tileBehaviorSize,
|
||||
),
|
||||
);
|
||||
const update = () => {
|
||||
if (!sections.length) {
|
||||
cancelAnimationFrame(requestId);
|
||||
return;
|
||||
}
|
||||
graph.executeWithNoStack(() => {
|
||||
graph.showItem(sections.shift(), false);
|
||||
});
|
||||
graph.showItem(hiddenNodeIds, true);
|
||||
}
|
||||
requestId = requestAnimationFrame(update);
|
||||
};
|
||||
requestId = requestAnimationFrame(update);
|
||||
|
||||
this.hiddenEdgeIds = this.hiddenNodeIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,7 @@ export class ZoomCanvas extends Behavior {
|
||||
private hiddenEdgeIds: ID[];
|
||||
private hiddenNodeIds: ID[];
|
||||
private zoomTimer: ReturnType<typeof setTimeout>;
|
||||
private tileRequestId?: number;
|
||||
|
||||
constructor(options: Partial<ZoomCanvasOptions>) {
|
||||
const finalOptions = Object.assign({}, DEFAULT_OPTIONS, options);
|
||||
@ -117,11 +118,14 @@ export class ZoomCanvas extends Behavior {
|
||||
const { graph } = this;
|
||||
const { tileBehavior: graphBehaviorOptimize, tileBehaviorSize = 1000 } =
|
||||
graph.getSpecification().optimize || {};
|
||||
const optimize = this.options.enableOptimize || graphBehaviorOptimize;
|
||||
const shouldOptimzie = isNumber(optimize)
|
||||
const optimize =
|
||||
this.options.enableOptimize !== undefined
|
||||
? this.options.enableOptimize
|
||||
: graphBehaviorOptimize;
|
||||
const shouldOptimze = isNumber(optimize)
|
||||
? graph.getAllNodesData().length > optimize
|
||||
: optimize;
|
||||
if (shouldOptimzie) {
|
||||
if (shouldOptimze) {
|
||||
this.hiddenEdgeIds = graph
|
||||
.getAllEdgesData()
|
||||
.map((edge) => edge.id)
|
||||
@ -131,7 +135,6 @@ export class ZoomCanvas extends Behavior {
|
||||
.map((node) => node.id)
|
||||
.filter((id) => graph.getItemVisible(id) === true);
|
||||
|
||||
let requestId;
|
||||
const hiddenIds = [...this.hiddenNodeIds];
|
||||
const sectionNum = Math.ceil(hiddenIds.length / tileBehaviorSize);
|
||||
const sections = Array.from({ length: sectionNum }, (v, i) =>
|
||||
@ -141,17 +144,18 @@ export class ZoomCanvas extends Behavior {
|
||||
),
|
||||
);
|
||||
const update = () => {
|
||||
if (!sections.length) {
|
||||
cancelAnimationFrame(requestId);
|
||||
if (!sections.length && this.tileRequestId) {
|
||||
cancelAnimationFrame(this.tileRequestId);
|
||||
this.tileRequestId = undefined;
|
||||
return;
|
||||
}
|
||||
const section = sections.shift();
|
||||
graph.startHistoryBatch();
|
||||
graph.hideItem(section, false, true);
|
||||
requestId = requestAnimationFrame(update);
|
||||
graph.stopHistoryBatch();
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
};
|
||||
graph.startHistoryBatch();
|
||||
requestId = requestAnimationFrame(update);
|
||||
graph.stopHistoryBatch();
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,14 +163,20 @@ export class ZoomCanvas extends Behavior {
|
||||
const { graph, hiddenEdgeIds = [], hiddenNodeIds } = this;
|
||||
const { tileBehavior: graphBehaviorOptimize, tileBehaviorSize = 1000 } =
|
||||
graph.getSpecification().optimize || {};
|
||||
const optimize = this.options.enableOptimize || graphBehaviorOptimize;
|
||||
const shouldOptimzie = isNumber(optimize)
|
||||
const optimize =
|
||||
this.options.enableOptimize !== undefined
|
||||
? this.options.enableOptimize
|
||||
: graphBehaviorOptimize;
|
||||
const shouldOptimze = isNumber(optimize)
|
||||
? graph.getAllNodesData().length > optimize
|
||||
: optimize;
|
||||
this.zooming = false;
|
||||
if (shouldOptimzie) {
|
||||
if (shouldOptimze) {
|
||||
if (this.tileRequestId) {
|
||||
cancelAnimationFrame(this.tileRequestId);
|
||||
this.tileRequestId = undefined;
|
||||
}
|
||||
if (hiddenNodeIds) {
|
||||
let requestId;
|
||||
const hiddenIds = [...hiddenNodeIds, ...hiddenEdgeIds];
|
||||
const sectionNum = Math.ceil(hiddenIds.length / tileBehaviorSize);
|
||||
const sections = Array.from({ length: sectionNum }, (v, i) =>
|
||||
@ -176,16 +186,17 @@ export class ZoomCanvas extends Behavior {
|
||||
),
|
||||
);
|
||||
const update = () => {
|
||||
if (!sections.length) {
|
||||
cancelAnimationFrame(requestId);
|
||||
if (!sections.length && this.tileRequestId) {
|
||||
cancelAnimationFrame(this.tileRequestId);
|
||||
this.tileRequestId = undefined;
|
||||
return;
|
||||
}
|
||||
graph.showItem(sections.shift(), false);
|
||||
requestId = requestAnimationFrame(update);
|
||||
graph.executeWithNoStack(() => {
|
||||
graph.showItem(sections.shift(), false);
|
||||
});
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
};
|
||||
graph.executeWithNoStack(() => {
|
||||
requestId = requestAnimationFrame(update);
|
||||
});
|
||||
this.tileRequestId = requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
this.hiddenEdgeIds = [];
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
/**
|
||||
* Validate and format the graph data.
|
||||
* @param data input user data.
|
||||
* @param userGraphCore the graph core stores the previous data.
|
||||
* @param graphCore the graph core stores the previous data.
|
||||
* @returns formatted data.
|
||||
*/
|
||||
export const MapNodeSize = (
|
||||
@ -18,7 +18,7 @@ export const MapNodeSize = (
|
||||
field?: string;
|
||||
range?: [number, number];
|
||||
} = {},
|
||||
userGraphCore?: GraphCore,
|
||||
graphCore?: GraphCore,
|
||||
): GraphData => {
|
||||
const { field, range = [8, 40] } = options;
|
||||
if (!field) return data;
|
||||
|
@ -10,13 +10,13 @@ import {
|
||||
/**
|
||||
* Validate and format the graph data.
|
||||
* @param data input user data.
|
||||
* @param userGraphCore the graph core stores the previous data.
|
||||
* @param graphCore the graph core stores the previous data.
|
||||
* @returns formatted data.
|
||||
*/
|
||||
export const TransformV4Data = (
|
||||
data: GraphData,
|
||||
options = {},
|
||||
userGraphCore?: GraphCore,
|
||||
graphCore?: GraphCore,
|
||||
): GraphData => {
|
||||
const { nodes = [], edges = [], combos = [] } = data;
|
||||
const formattedNodes = nodes.map((node: any) => {
|
||||
|
@ -10,13 +10,13 @@ import {
|
||||
/**
|
||||
* Validate and format the graph data.
|
||||
* @param data input user data.
|
||||
* @param userGraphCore the graph core stores the previous data.
|
||||
* @param graphCore the graph core stores the previous data.
|
||||
* @returns formatted data.
|
||||
*/
|
||||
export const ValidateData = (
|
||||
data: GraphData,
|
||||
options = {},
|
||||
userGraphCore?: GraphCore,
|
||||
graphCore?: GraphCore,
|
||||
): GraphData => {
|
||||
const { nodes, edges, combos } = data;
|
||||
const idMap = new Map();
|
||||
@ -64,7 +64,7 @@ export const ValidateData = (
|
||||
if (
|
||||
parentId !== undefined &&
|
||||
!comboIdMap.has(parentId) &&
|
||||
(!userGraphCore || !userGraphCore.hasNode(parentId))
|
||||
(!graphCore || !graphCore.hasNode(parentId))
|
||||
) {
|
||||
console.error(
|
||||
`The parentId of combo with id ${combo.id} will be removed since it is not exist in combos.`,
|
||||
@ -80,9 +80,9 @@ export const ValidateData = (
|
||||
if (
|
||||
parentId !== undefined &&
|
||||
!comboIdMap.has(parentId) &&
|
||||
(!userGraphCore || !userGraphCore.hasNode(parentId))
|
||||
(!graphCore || !graphCore.hasNode(parentId))
|
||||
) {
|
||||
// TODO: parentId is a node in userGraphCore
|
||||
// TODO: parentId is a node in graphCore
|
||||
console.error(
|
||||
`The parentId of node with id ${node.id} will be removed since it is not exist in combos.`,
|
||||
);
|
||||
@ -100,8 +100,8 @@ export const ValidateData = (
|
||||
let { source, target } = edge;
|
||||
if (!idAndDataCheck(edge, 'edge', true)) return false;
|
||||
|
||||
if (userGraphCore?.hasEdge(id)) {
|
||||
const existEdge = userGraphCore?.getEdge(id);
|
||||
if (graphCore?.hasEdge(id)) {
|
||||
const existEdge = graphCore?.getEdge(id);
|
||||
if (source === undefined) source = existEdge.source;
|
||||
if (target === undefined) target = existEdge.target;
|
||||
}
|
||||
@ -121,7 +121,7 @@ export const ValidateData = (
|
||||
if (
|
||||
!nodeIdMap.has(source) &&
|
||||
!comboIdMap.has(source) &&
|
||||
(!userGraphCore || !userGraphCore.hasNode(source))
|
||||
(!graphCore || !graphCore.hasNode(source))
|
||||
) {
|
||||
console.error(
|
||||
`The edge with id ${id} will be ignored since its source ${source} is not existed in nodes and combos.`,
|
||||
@ -131,7 +131,7 @@ export const ValidateData = (
|
||||
if (
|
||||
!nodeIdMap.has(target) &&
|
||||
!comboIdMap.has(target) &&
|
||||
(!userGraphCore || !userGraphCore.hasNode(target))
|
||||
(!graphCore || !graphCore.hasNode(target))
|
||||
) {
|
||||
console.error(
|
||||
`The edge with id ${id} will be ignored since its target ${target} is not existed in nodes and combos.`,
|
||||
|
@ -32,6 +32,7 @@ const {
|
||||
EllipseNode,
|
||||
ModelRectNode,
|
||||
ImageNode,
|
||||
CubeNode,
|
||||
} = Nodes;
|
||||
|
||||
const {
|
||||
@ -61,6 +62,7 @@ const {
|
||||
DragNode,
|
||||
DragCombo,
|
||||
ClickSelect,
|
||||
CreateEdge,
|
||||
ShortcutsCall,
|
||||
ScrollCanvas,
|
||||
} = Behaviors;
|
||||
@ -75,6 +77,7 @@ const {
|
||||
Toolbar,
|
||||
Timebar,
|
||||
Snapline,
|
||||
EdgeFilterLens,
|
||||
} = Plugins;
|
||||
|
||||
const {
|
||||
@ -95,6 +98,7 @@ const {
|
||||
import lassoSelector from './selector/lasso';
|
||||
import rectSelector from './selector/rect';
|
||||
import Hull from './plugin/hull';
|
||||
import { WaterMarker } from './plugin/watermaker';
|
||||
|
||||
const stdLib = {
|
||||
transforms: {
|
||||
@ -122,10 +126,10 @@ const stdLib = {
|
||||
'zoom-canvas': ZoomCanvas,
|
||||
'drag-node': DragNode,
|
||||
'drag-combo': DragCombo,
|
||||
'create-edge': CreateEdge,
|
||||
'collapse-expand-combo': CollapseExpandCombo,
|
||||
'collapse-expand-tree': CollapseExpandTree,
|
||||
'click-select': ClickSelect,
|
||||
'scroll-canvas': ScrollCanvas,
|
||||
},
|
||||
plugins: {
|
||||
history: History,
|
||||
@ -137,6 +141,7 @@ const stdLib = {
|
||||
},
|
||||
edges: {
|
||||
'line-edge': LineEdge,
|
||||
'loop-edge': LoopEdge,
|
||||
},
|
||||
combos: {
|
||||
'circle-combo': CircleCombo,
|
||||
@ -244,6 +249,7 @@ const Extensions = {
|
||||
TriangleNode,
|
||||
EllipseNode,
|
||||
ModelRectNode,
|
||||
CubeNode,
|
||||
// edges
|
||||
LineEdge,
|
||||
CubicEdge,
|
||||
@ -271,7 +277,9 @@ const Extensions = {
|
||||
CollapseExpandCombo,
|
||||
DragNode,
|
||||
DragCombo,
|
||||
CreateEdge,
|
||||
ShortcutsCall,
|
||||
ScrollCanvas,
|
||||
// plugins
|
||||
BasePlugin,
|
||||
History,
|
||||
@ -285,6 +293,8 @@ const Extensions = {
|
||||
Timebar,
|
||||
Hull,
|
||||
Snapline,
|
||||
EdgeFilterLens,
|
||||
WaterMarker
|
||||
};
|
||||
|
||||
export default registery;
|
||||
|
@ -289,9 +289,10 @@ export abstract class BaseEdge {
|
||||
const pointOffset = (keyShape as Line | Polyline).getPoint(
|
||||
positionPreset.pointRatio[1],
|
||||
);
|
||||
const angle = Math.atan(
|
||||
let angle = Math.atan(
|
||||
(point.y - pointOffset.y) / (point.x - pointOffset.x),
|
||||
); // TODO: NaN
|
||||
);
|
||||
if (isNaN(angle)) angle = 0;
|
||||
|
||||
// revert
|
||||
isRevert = pointOffset.x < point.x;
|
||||
@ -331,6 +332,7 @@ export abstract class BaseEdge {
|
||||
maxWidth,
|
||||
this.zoomCache.zoom,
|
||||
);
|
||||
this.zoomCache.wordWrapWidth = wordWrapWidth;
|
||||
const style = {
|
||||
...this.defaultStyles.labelShape,
|
||||
textAlign: positionPreset.textAlign,
|
||||
@ -339,6 +341,7 @@ export abstract class BaseEdge {
|
||||
...positionStyle,
|
||||
...otherStyle,
|
||||
};
|
||||
this.boundsCache.labelShapeTransform = style.transform;
|
||||
return this.upsertShape('text', 'labelShape', style, shapeMap, model);
|
||||
}
|
||||
|
||||
@ -686,12 +689,9 @@ export abstract class BaseEdge {
|
||||
resultStyle[`${markerField}Offset`] = 0;
|
||||
return;
|
||||
}
|
||||
let arrowStyle = {} as ArrowStyle;
|
||||
if (isBoolean(arrowConfig)) {
|
||||
arrowStyle = { ...DEFAULT_ARROW_CONFIG } as ArrowStyle;
|
||||
} else {
|
||||
arrowStyle = arrowConfig;
|
||||
}
|
||||
const arrowStyle = isBoolean(arrowConfig)
|
||||
? ({ ...DEFAULT_ARROW_CONFIG } as ArrowStyle)
|
||||
: arrowConfig;
|
||||
const {
|
||||
type = 'triangle',
|
||||
width = 10,
|
||||
@ -700,14 +700,13 @@ export abstract class BaseEdge {
|
||||
offset = 0,
|
||||
...others
|
||||
} = arrowStyle;
|
||||
const path = propPath ? propPath : getArrowPath(type, width, height);
|
||||
resultStyle[markerField] = this.upsertShape(
|
||||
'path',
|
||||
`${markerField}Shape`,
|
||||
{
|
||||
...bodyStyle,
|
||||
fill: type === 'simple' ? '' : bodyStyle.stroke,
|
||||
path,
|
||||
path: propPath || getArrowPath(type, width, height),
|
||||
anchor: '0.5 0.5',
|
||||
transformOrigin: 'center',
|
||||
...others,
|
||||
|
@ -21,6 +21,7 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
themeStyles: NodeShapeStyles;
|
||||
mergedStyles: NodeShapeStyles;
|
||||
device: any; // for 3d renderer
|
||||
dimensions: number = 3;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.device = props.device;
|
||||
|
102
packages/g6/src/stdlib/item/node/cube.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { NodeDisplayModel } from '../../../types';
|
||||
import { State } from '../../../types/item';
|
||||
import {
|
||||
NodeModelData,
|
||||
NodeShapeMap,
|
||||
NodeShapeStyles,
|
||||
} from '../../../types/node';
|
||||
import { BaseNode3D } from './base3d';
|
||||
|
||||
export class CubeNode extends BaseNode3D {
|
||||
override defaultStyles = {
|
||||
keyShape: {
|
||||
latitudeBands: 32,
|
||||
longitudeBands: 32,
|
||||
width: 10,
|
||||
height: 10,
|
||||
depth: 10,
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
};
|
||||
mergedStyles: NodeShapeStyles;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
public draw(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): NodeShapeMap {
|
||||
const { data = {} } = model;
|
||||
|
||||
let shapes: NodeShapeMap = { keyShape: undefined };
|
||||
|
||||
// keyShape
|
||||
shapes.keyShape = this.drawKeyShape(model, shapeMap, diffData);
|
||||
|
||||
// haloShape
|
||||
if (data.haloShape && this.drawHaloShape) {
|
||||
shapes.haloShape = this.drawHaloShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// labelShape
|
||||
if (data.labelShape) {
|
||||
shapes.labelShape = this.drawLabelShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// labelBackgroundShape
|
||||
if (data.labelBackgroundShape) {
|
||||
shapes.labelBackgroundShape = this.drawLabelBackgroundShape(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
);
|
||||
}
|
||||
|
||||
// iconShape
|
||||
if (data.iconShape) {
|
||||
shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
|
||||
}
|
||||
|
||||
// badgeShape
|
||||
if (data.badgeShapes) {
|
||||
const badgeShapes = this.drawBadgeShapes(
|
||||
model,
|
||||
shapeMap,
|
||||
diffData,
|
||||
diffState,
|
||||
);
|
||||
shapes = {
|
||||
...shapes,
|
||||
...badgeShapes,
|
||||
};
|
||||
}
|
||||
|
||||
// otherShapes
|
||||
if (data.otherShapes && this.drawOtherShapes) {
|
||||
shapes = {
|
||||
...shapes,
|
||||
...this.drawOtherShapes(model, shapeMap, diffData),
|
||||
};
|
||||
}
|
||||
return shapes;
|
||||
}
|
||||
|
||||
public drawKeyShape(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
return this.upsertShape(
|
||||
'cube',
|
||||
'keyShape',
|
||||
this.mergedStyles.keyShape,
|
||||
shapeMap,
|
||||
);
|
||||
}
|
||||
}
|
@ -9,3 +9,4 @@ export * from './donut';
|
||||
export * from './diamond';
|
||||
export * from './modelRect';
|
||||
export * from './image';
|
||||
export * from './cube';
|
||||
|
380
packages/g6/src/stdlib/plugin/edgeFilterLens/index.ts
Normal file
@ -0,0 +1,380 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { clone } from '@antv/util';
|
||||
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
|
||||
import { IGraph } from '../../../types';
|
||||
import { IG6GraphEvent } from '../../../types/event';
|
||||
import { ShapeStyle } from '../../../types/item';
|
||||
import { distance } from '../../../util/point';
|
||||
|
||||
const DELTA = 0.01;
|
||||
interface EdgeFilterLensConfig extends IPluginBaseConfig {
|
||||
trigger?: 'mousemove' | 'click' | 'drag';
|
||||
r?: number;
|
||||
delegateStyle?: ShapeStyle;
|
||||
showLabel?: 'node' | 'edge' | 'both' | undefined;
|
||||
scaleRBy?: 'wheel' | undefined;
|
||||
maxR?: number;
|
||||
minR?: number;
|
||||
showType?: 'one' | 'both' | 'only-source' | 'only-target'; // 更名,原名与plugin的type冲突
|
||||
shouldShow?: (d?: unknown) => boolean;
|
||||
}
|
||||
|
||||
const lensDelegateStyle = {
|
||||
stroke: '#000',
|
||||
strokeOpacity: 0.8,
|
||||
lineWidth: 2,
|
||||
fillOpacity: 0.1,
|
||||
fill: '#fff',
|
||||
};
|
||||
|
||||
export class EdgeFilterLens extends Base {
|
||||
private showNodeLabel: boolean;
|
||||
private showEdgeLabel: boolean;
|
||||
private delegate: DisplayObject;
|
||||
private cachedTransientNodes: Set<string | number>;
|
||||
private cachedTransientEdges: Set<string | number>;
|
||||
private dragging: boolean;
|
||||
private delegateCenterDiff: { x: number; y: number };
|
||||
|
||||
constructor(config?: EdgeFilterLensConfig) {
|
||||
super(config);
|
||||
this.cachedTransientNodes = new Set();
|
||||
this.cachedTransientEdges = new Set();
|
||||
}
|
||||
|
||||
public getDefaultCfgs(): EdgeFilterLensConfig {
|
||||
return {
|
||||
showType: 'both',
|
||||
trigger: 'mousemove',
|
||||
r: 60,
|
||||
delegateStyle: clone(lensDelegateStyle),
|
||||
showLabel: 'edge',
|
||||
scaleRBy: 'wheel',
|
||||
};
|
||||
}
|
||||
|
||||
public getEvents() {
|
||||
let events = {
|
||||
pointerdown: this.onPointerDown,
|
||||
pointerup: this.onPointerUp,
|
||||
wheel: this.onWheel,
|
||||
} as {
|
||||
[key: string]: any;
|
||||
};
|
||||
switch (this.options.trigger) {
|
||||
case 'click':
|
||||
events = {
|
||||
...events,
|
||||
click: this.filter,
|
||||
};
|
||||
break;
|
||||
case 'drag':
|
||||
events = {
|
||||
...events,
|
||||
pointermove: this.onPointerMove,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
events = {
|
||||
...events,
|
||||
pointermove: this.filter,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
public init(graph: IGraph) {
|
||||
super.init(graph);
|
||||
const showLabel = this.options.showLabel;
|
||||
const showNodeLabel = showLabel === 'node' || showLabel === 'both';
|
||||
const showEdgeLabel = showLabel === 'edge' || showLabel === 'both';
|
||||
this.showNodeLabel = showNodeLabel;
|
||||
this.showEdgeLabel = showEdgeLabel;
|
||||
const shouldShow = this.options.shouldShow;
|
||||
if (!shouldShow) this.options.shouldShow = () => true;
|
||||
}
|
||||
|
||||
protected onPointerUp(e: IG6GraphEvent) {
|
||||
this.dragging = false;
|
||||
}
|
||||
|
||||
protected onPointerMove(e: IG6GraphEvent) {
|
||||
if (!this.dragging) return;
|
||||
this.moveDelegate(e);
|
||||
}
|
||||
|
||||
protected onPointerDown(e: IG6GraphEvent) {
|
||||
const { delegate: lensDelegate } = this;
|
||||
let cacheCenter;
|
||||
if (!lensDelegate || lensDelegate.destroyed) {
|
||||
cacheCenter = { x: e.canvas.x, y: e.canvas.y };
|
||||
this.filter(e);
|
||||
} else {
|
||||
cacheCenter = {
|
||||
x: lensDelegate.style.cx,
|
||||
y: lensDelegate.style.cy,
|
||||
};
|
||||
}
|
||||
this.delegateCenterDiff = {
|
||||
x: e.canvas.x - cacheCenter.x,
|
||||
y: e.canvas.y - cacheCenter.y,
|
||||
};
|
||||
this.dragging = true;
|
||||
}
|
||||
|
||||
// Determine whether it is dragged in the delegate
|
||||
protected isInLensDelegate(lensDelegate, pointer): boolean {
|
||||
const { cx: lensX, cy: lensY, r: lensR } = lensDelegate.style;
|
||||
if (
|
||||
pointer.x >= lensX - lensR &&
|
||||
pointer.x <= lensX + lensR &&
|
||||
pointer.y >= lensY - lensR &&
|
||||
pointer.y <= lensY + lensR
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected moveDelegate(e) {
|
||||
if (
|
||||
this.isInLensDelegate(this.delegate, { x: e.canvas.x, y: e.canvas.y })
|
||||
) {
|
||||
const center = {
|
||||
x: e.canvas.x - this.delegateCenterDiff.x,
|
||||
y: e.canvas.y - this.delegateCenterDiff.y,
|
||||
};
|
||||
this.filter(e, center);
|
||||
}
|
||||
}
|
||||
|
||||
protected onWheel(e: IG6GraphEvent) {
|
||||
const { delegate: lensDelegate, options } = this;
|
||||
const { scaleRBy } = options;
|
||||
if (!lensDelegate || lensDelegate.destroyed) return;
|
||||
if (scaleRBy !== 'wheel') return;
|
||||
if (this.isInLensDelegate(lensDelegate, { x: e.canvas.x, y: e.canvas.y })) {
|
||||
if (scaleRBy === 'wheel') {
|
||||
this.scaleRByWheel(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale the range by wheel
|
||||
* @param e mouse wheel event
|
||||
*/
|
||||
protected scaleRByWheel(e: IG6GraphEvent) {
|
||||
if (!e || !e.originalEvent) return;
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
const { graph, options } = this;
|
||||
const graphCanvasEl = graph.canvas.context.config.canvas;
|
||||
const graphHeight = graphCanvasEl?.height || 500;
|
||||
const maxR = options.maxR
|
||||
? Math.min(options.maxR, graphHeight)
|
||||
: graphHeight;
|
||||
const minR = options.minR
|
||||
? Math.max(options.minR, graphHeight * DELTA)
|
||||
: graphHeight * DELTA;
|
||||
|
||||
const scale = 1 + (e.originalEvent as any).deltaY * -1 * DELTA;
|
||||
let r = options.r * scale;
|
||||
r = Math.min(r, maxR);
|
||||
r = Math.max(r, minR);
|
||||
options.r = r;
|
||||
this.delegate.style.r = r;
|
||||
this.filter(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Response function for mousemove, click, or drag to filter out the edges
|
||||
* @param e mouse event
|
||||
*/
|
||||
protected filter(e: IG6GraphEvent, mousePos?) {
|
||||
const {
|
||||
graph,
|
||||
options,
|
||||
showNodeLabel,
|
||||
showEdgeLabel,
|
||||
cachedTransientNodes,
|
||||
cachedTransientEdges,
|
||||
} = this;
|
||||
const r = options.r;
|
||||
const showType = options.showType;
|
||||
const shouldShow = options.shouldShow;
|
||||
const fCenter = mousePos || { x: e.canvas.x, y: e.canvas.y };
|
||||
this.updateDelegate(fCenter, r);
|
||||
|
||||
const nodes = graph.getAllNodesData();
|
||||
const hitNodesMap = {};
|
||||
nodes.forEach((node) => {
|
||||
const { data, id } = node;
|
||||
if (distance({ x: data.x, y: data.y }, fCenter) < r) {
|
||||
hitNodesMap[id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
const edges = graph.getAllEdgesData();
|
||||
const hitEdges = [];
|
||||
edges.forEach((edge) => {
|
||||
const sourceId = edge.source;
|
||||
const targetId = edge.target;
|
||||
if (shouldShow(edge)) {
|
||||
if (showType === 'only-source' || showType === 'one') {
|
||||
if (hitNodesMap[sourceId] && !hitNodesMap[targetId])
|
||||
hitEdges.push(edge);
|
||||
} else if (showType === 'only-target' || showType === 'one') {
|
||||
if (hitNodesMap[targetId] && !hitNodesMap[sourceId])
|
||||
hitEdges.push(edge);
|
||||
} else if (
|
||||
showType === 'both' &&
|
||||
hitNodesMap[sourceId] &&
|
||||
hitNodesMap[targetId]
|
||||
) {
|
||||
hitEdges.push(edge);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentTransientNodes = new Set<string | number>();
|
||||
const currentTransientEdges = new Set<string | number>();
|
||||
|
||||
if (showNodeLabel) {
|
||||
Object.keys(hitNodesMap).forEach((key) => {
|
||||
const node = hitNodesMap[key];
|
||||
currentTransientNodes.add(node.id);
|
||||
|
||||
if (cachedTransientNodes.has(node.id)) {
|
||||
cachedTransientNodes.delete(node.id);
|
||||
} else {
|
||||
graph.drawTransient('node', node.id, { shapeIds: ['labelShape'] });
|
||||
}
|
||||
});
|
||||
|
||||
cachedTransientNodes.forEach((id) => {
|
||||
graph.drawTransient('node', id, { action: 'remove' });
|
||||
});
|
||||
}
|
||||
|
||||
if (showEdgeLabel) {
|
||||
hitEdges.forEach((edge) => {
|
||||
currentTransientEdges.add(edge.id);
|
||||
|
||||
if (cachedTransientEdges.has(edge.id)) {
|
||||
cachedTransientEdges.delete(edge.id);
|
||||
} else {
|
||||
graph.drawTransient('edge', edge.id, {
|
||||
shapeIds: ['labelShape'],
|
||||
drawSource: false,
|
||||
drawTarget: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cachedTransientEdges.forEach((id) => {
|
||||
graph.drawTransient('edge', id, { action: 'remove' });
|
||||
});
|
||||
}
|
||||
|
||||
this.cachedTransientNodes = currentTransientNodes;
|
||||
this.cachedTransientEdges = currentTransientEdges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust part of the parameters, including trigger, showType, r, maxR, minR, shouldShow, showLabel, and scaleRBy
|
||||
* @param {EdgeFilterLensConfig} cfg
|
||||
*/
|
||||
public updateParams(cfg: EdgeFilterLensConfig) {
|
||||
const self = this;
|
||||
const { r, trigger, minR, maxR, scaleRBy, showLabel, shouldShow } = cfg;
|
||||
if (!isNaN(cfg.r)) {
|
||||
self.options.r = r;
|
||||
}
|
||||
if (!isNaN(maxR)) {
|
||||
self.options.maxR = maxR;
|
||||
}
|
||||
if (!isNaN(minR)) {
|
||||
self.options.minR = minR;
|
||||
}
|
||||
if (trigger === 'mousemove' || trigger === 'click' || trigger === 'drag') {
|
||||
self.options.trigger = trigger;
|
||||
}
|
||||
if (scaleRBy === 'wheel' || scaleRBy === 'unset') {
|
||||
self.options.scaleRBy = scaleRBy;
|
||||
self.delegate.remove();
|
||||
self.delegate.destroy();
|
||||
}
|
||||
if (showLabel === 'node' || showLabel === 'both') {
|
||||
self.showNodeLabel = true;
|
||||
}
|
||||
if (showLabel === 'edge' || showLabel === 'both') {
|
||||
self.showEdgeLabel = true;
|
||||
}
|
||||
if (shouldShow) {
|
||||
self.options.shouldShow = shouldShow;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the delegate shape of the lens
|
||||
* @param {Point} mCenter the center of the shape
|
||||
* @param {number} r the radius of the shape
|
||||
*/
|
||||
private updateDelegate(mCenter, r) {
|
||||
const { graph, options, delegate } = this;
|
||||
let lensDelegate = delegate;
|
||||
if (!lensDelegate || lensDelegate.destroyed) {
|
||||
// 拖动多个
|
||||
const attrs = options.delegateStyle || lensDelegateStyle;
|
||||
|
||||
// model上的x, y是相对于图形中心的,delegateShape是g实例,x,y是绝对坐标
|
||||
lensDelegate = graph.drawTransient('circle', 'lens-shape', {
|
||||
style: {
|
||||
r,
|
||||
cx: mCenter.x,
|
||||
cy: mCenter.y,
|
||||
...attrs,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
lensDelegate.style.cx = mCenter.x;
|
||||
lensDelegate.style.cy = mCenter.y;
|
||||
lensDelegate.style.r = mCenter.r;
|
||||
}
|
||||
|
||||
this.delegate = lensDelegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the filtering
|
||||
*/
|
||||
public clear() {
|
||||
const {
|
||||
graph,
|
||||
delegate: lensDelegate,
|
||||
cachedTransientNodes,
|
||||
cachedTransientEdges,
|
||||
} = this;
|
||||
|
||||
if (lensDelegate && !lensDelegate.destroyed) {
|
||||
graph.drawTransient('circle', 'lens-shape', { action: 'remove' });
|
||||
}
|
||||
cachedTransientNodes.forEach((id) => {
|
||||
graph.drawTransient('node', id, { action: 'remove' });
|
||||
});
|
||||
cachedTransientEdges.forEach((id) => {
|
||||
graph.drawTransient('edge', id, { action: 'remove' });
|
||||
});
|
||||
|
||||
cachedTransientNodes.clear();
|
||||
cachedTransientEdges.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the component
|
||||
*/
|
||||
public destroy() {
|
||||
this.clear();
|
||||
}
|
||||
}
|
@ -43,27 +43,45 @@ export class ItemDataCommand implements Command {
|
||||
operationType: StackType,
|
||||
onlyMove = false,
|
||||
) {
|
||||
let models;
|
||||
const modelMap = new Map();
|
||||
if (this.type === 'combo' && !onlyMove) {
|
||||
models = this.changes.map((data) => ({
|
||||
id: data.nodeId,
|
||||
data: {
|
||||
parentId: STACK_TYPE.undo ? data.oldParentId : data.newParentId,
|
||||
},
|
||||
}));
|
||||
this.changes.forEach((data) => {
|
||||
const model = modelMap.get(data.nodeId) || {};
|
||||
modelMap.set(data.nodeId, {
|
||||
id: data.nodeId,
|
||||
data: {
|
||||
...model.data,
|
||||
parentId: STACK_TYPE.undo ? data.oldParentId : data.newParentId,
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
models = this.changes.map((data) => {
|
||||
return {
|
||||
this.changes.forEach((data) => {
|
||||
const value =
|
||||
operationType === STACK_TYPE.undo ? data.oldValue : data.newValue;
|
||||
if (
|
||||
(typeof value === 'number' && isNaN(value)) ||
|
||||
(['x', 'y'].includes(data.propertyName) && value === undefined)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = modelMap.get(data.nodeId) || {};
|
||||
modelMap.set(data.id, {
|
||||
id: data.id,
|
||||
data:
|
||||
operationType === STACK_TYPE.undo ? data.oldValue : data.newValue,
|
||||
};
|
||||
data: {
|
||||
...model.data,
|
||||
[data.propertyName]: value,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
graph.pauseStack();
|
||||
const models = Array.from(modelMap.values());
|
||||
if (onlyMove) {
|
||||
graph.updatePosition(this.type, models, this.upsertAncestors);
|
||||
// No matter it is node or combo, update the nodes' positions
|
||||
graph.updateNodePosition(models, this.upsertAncestors);
|
||||
} else {
|
||||
graph.updateData(this.type, models);
|
||||
}
|
||||
|
@ -51,7 +51,8 @@ interface HullComponentFullOptions extends HullComponentOptions {
|
||||
export default class Hull {
|
||||
graph: IGraph;
|
||||
|
||||
path: any[][];
|
||||
// TODO: PathArray is not exported by @antv/util 2.x but by 3.x. Correct the type String | PathArray after upgrading @antv/util
|
||||
path: any;
|
||||
|
||||
members: (NodeModel | ComboModel)[];
|
||||
|
||||
@ -172,9 +173,9 @@ export default class Hull {
|
||||
const shape = this.graph.drawTransient('path', this.options.id, {
|
||||
style: {
|
||||
path: this.path,
|
||||
pointerEvents: 'none',
|
||||
...this.options.style,
|
||||
},
|
||||
capture: false,
|
||||
});
|
||||
const { labelShape } = this.options;
|
||||
if (labelShape?.text) {
|
||||
@ -183,10 +184,10 @@ export default class Hull {
|
||||
const formattedLabelStyle = this.getLabelStyle(labelShape, shapeBounds);
|
||||
this.graph.drawTransient('text', `${this.options.id}-label`, {
|
||||
style: {
|
||||
pointerEvents: 'none',
|
||||
...labelShapeStyle,
|
||||
...formattedLabelStyle,
|
||||
},
|
||||
capture: false,
|
||||
});
|
||||
}
|
||||
shape.toBack();
|
||||
@ -426,8 +427,10 @@ export default class Hull {
|
||||
|
||||
public updateStyle(style: ShapeStyle) {
|
||||
this.graph.drawTransient('path', this.options.id, {
|
||||
style,
|
||||
capture: false,
|
||||
style: {
|
||||
...style,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -8,3 +8,4 @@ export * from './toolbar';
|
||||
export * from './tooltip';
|
||||
export * from './timebar';
|
||||
export * from './snapline';
|
||||
export * from './edgeFilterLens';
|
||||
|
808
packages/g6/src/stdlib/plugin/map/index.ts
Normal file
@ -0,0 +1,808 @@
|
||||
// TODO: update type define.
|
||||
// @ts-nocheck
|
||||
import { createDom, modifyCSS } from '@antv/dom-util';
|
||||
import { Canvas, DisplayObject, Group, Rect } from '@antv/g';
|
||||
import { debounce, each, isNil, isString, uniqueId } from '@antv/util';
|
||||
import { IGraph } from '../../../types';
|
||||
import { IG6GraphEvent } from '../../../types/event';
|
||||
import { ShapeStyle } from '../../../types/item';
|
||||
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
|
||||
import { createCanvas } from '../../../util/canvas';
|
||||
|
||||
const DEFAULT_MODE = 'default';
|
||||
const KEYSHAPE_MODE = 'keyShape';
|
||||
const DELEGATE_MODE = 'delegate';
|
||||
const SVG = 'svg';
|
||||
|
||||
export interface MiniMapConfig extends IPluginBaseConfig {
|
||||
/** Class name of viewport */
|
||||
viewportClassName?: string;
|
||||
/** Class name of minimap */
|
||||
className?: string;
|
||||
/** Mode of minimap */
|
||||
mode?: 'default' | 'keyShape' | 'delegate';
|
||||
/** Size of minimap */
|
||||
size?: number[];
|
||||
/** Style of delegate shape */
|
||||
delegateStyle?: ShapeStyle;
|
||||
/** Whether to refresh minimap */
|
||||
refresh?: boolean;
|
||||
/** Padding of minimap */
|
||||
padding?: number;
|
||||
/** Whether to hide edges on minimap to enhance performance */
|
||||
hideEdge?: boolean;
|
||||
/** Container for minimap */
|
||||
container?: HTMLDivElement | null;
|
||||
}
|
||||
|
||||
export class Minimap extends Base {
|
||||
private canvas: Canvas;
|
||||
/** The viewport DOM on the minimap. */
|
||||
private viewport: HTMLElement | undefined;
|
||||
/** Cache the mapping of graphics of nodes/edges/combos on main graph and minimap graph. */
|
||||
private itemMap: Map<
|
||||
ID,
|
||||
{
|
||||
minimapItem: DisplayObject;
|
||||
graphItem: DisplayObject;
|
||||
}
|
||||
> = new Map();
|
||||
private container: HTMLDivElement;
|
||||
/** Ratio of (minimap graph size / main graph size). */
|
||||
private ratio: number;
|
||||
/** Distance from top of minimap graph to the top of minimap container. */
|
||||
private dx: number;
|
||||
/** Distance from left of minimap graph to the left of minimap container. */
|
||||
private dy: number;
|
||||
/** Cache the visibility while items' visibility changed. And apply them onto the minimap with debounce. */
|
||||
private visibleCache: { [id: string]: boolean } = {};
|
||||
|
||||
constructor(options?: MiniMapConfig) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public getDefaultCfgs(): MiniMapConfig {
|
||||
return {
|
||||
key: `minimap-${uniqueId()}`,
|
||||
container: null,
|
||||
className: 'g6-minimap',
|
||||
viewportClassName: 'g6-minimap-viewport',
|
||||
// Minimap 中默认展示和主图一样的内容,KeyShape 只展示节点和边的 key shape 部分,delegate表示展示自定义的rect,用户可自定义样式
|
||||
mode: 'default',
|
||||
padding: 8,
|
||||
size: [200, 120],
|
||||
delegateStyle: {
|
||||
fill: '#40a9ff',
|
||||
stroke: '#096dd9',
|
||||
},
|
||||
refresh: true,
|
||||
hideEdge: false,
|
||||
};
|
||||
}
|
||||
|
||||
public getEvents() {
|
||||
return {
|
||||
afteritemstatechange: this.handleUpdateCanvas,
|
||||
afterlayout: this.handleUpdateCanvas,
|
||||
viewportchange: this.handleUpdateCanvas,
|
||||
afteritemchange: this.handleUpdateCanvas,
|
||||
afteritemvisibilitychange: this.handleVisibilityChange,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If it is animating, disable refresh.
|
||||
*/
|
||||
protected disableRefresh() {
|
||||
this.options.refresh = false;
|
||||
}
|
||||
|
||||
protected enableRefresh() {
|
||||
this.options.refresh = true;
|
||||
this.updateCanvas();
|
||||
}
|
||||
|
||||
private initViewport() {
|
||||
const { canvas, options, destroyed, graph } = this;
|
||||
const { size, viewportClassName } = options;
|
||||
|
||||
if (destroyed) return;
|
||||
|
||||
const containerDOM = canvas.context.config.container as HTMLElement;
|
||||
const isFireFox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
const isSafari = navigator.userAgent.toLowerCase().indexOf('safari') > -1;
|
||||
const viewport = createDom(`
|
||||
<div
|
||||
class=${viewportClassName}
|
||||
style='position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
border: 2px solid #1980ff;
|
||||
box-sizing:border-box;
|
||||
background: rgba(0, 0, 255, 0.1);
|
||||
cursor:move'
|
||||
draggable=${isSafari || isFireFox ? false : true}
|
||||
/>`);
|
||||
|
||||
// Last mouse x position
|
||||
let x = 0;
|
||||
// Last mouse y position
|
||||
let y = 0;
|
||||
// Whether in dragging status
|
||||
let dragging = false;
|
||||
let resizing = false;
|
||||
|
||||
const dragstartevent = isSafari || isFireFox ? 'mousedown' : 'dragstart';
|
||||
this.container.addEventListener('mousemove', (e) => {
|
||||
const moveAtBorder = getMoveAtBorder(viewport, e);
|
||||
if (moveAtBorder) {
|
||||
this.container.style.cursor = cursorMap[moveAtBorder];
|
||||
viewport.style.cursor = cursorMap[moveAtBorder];
|
||||
} else {
|
||||
this.container.style.cursor = 'unset';
|
||||
viewport.style.cursor = 'move';
|
||||
}
|
||||
});
|
||||
viewport.addEventListener(
|
||||
dragstartevent,
|
||||
((e: IG6GraphEvent) => {
|
||||
resizing = getMoveAtBorder(viewport, e);
|
||||
if (resizing) return;
|
||||
|
||||
if ((e as any).dataTransfer) {
|
||||
const img = new Image();
|
||||
img.src =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath /%3E%3C/svg%3E";
|
||||
(e as any).dataTransfer.setDragImage?.(img, 0, 0);
|
||||
try {
|
||||
(e as any).dataTransfer.setData('text/html', 'view-port-minimap');
|
||||
} catch {
|
||||
// support IE
|
||||
(e as any).dataTransfer.setData('text', 'view-port-minimap');
|
||||
}
|
||||
}
|
||||
|
||||
this.options.refresh = false;
|
||||
if (e.target !== viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragging = true;
|
||||
x = e.clientX;
|
||||
y = e.clientY;
|
||||
}).bind(this),
|
||||
false,
|
||||
);
|
||||
|
||||
const dragListener = (e: IG6GraphEvent) => {
|
||||
const { style } = viewport;
|
||||
const left = parseInt(style.left, 10);
|
||||
const top = parseInt(style.top, 10);
|
||||
const width = parseInt(style.width, 10);
|
||||
const height = parseInt(style.height, 10);
|
||||
|
||||
if (resizing) {
|
||||
const { clientX, clientY } = e;
|
||||
const afterResize = { left, top, width, height };
|
||||
if (resizing.includes('left')) {
|
||||
afterResize.left = `${clientX}px`;
|
||||
afterResize.width = `${left + width - clientX}px`;
|
||||
} else if (resizing.includes('right')) {
|
||||
afterResize.width = `${clientX - left}px`;
|
||||
}
|
||||
if (resizing.includes('top')) {
|
||||
afterResize.top = `${clientY}`;
|
||||
afterResize.height = `${top + height - clientY}`;
|
||||
} else if (resizing.includes('bottom')) {
|
||||
afterResize.height = `${clientY - top}`;
|
||||
}
|
||||
modifyCSS(viewport, afterResize);
|
||||
return;
|
||||
}
|
||||
if (!dragging || isNil(e.clientX) || isNil(e.clientY)) {
|
||||
return;
|
||||
}
|
||||
const { ratio } = this;
|
||||
const zoom = graph!.getZoom();
|
||||
|
||||
let dx = x - e.clientX;
|
||||
let dy = y - e.clientY;
|
||||
|
||||
// If the viewport is already on the left or right, stop moving x.
|
||||
if (left - dx < 0 || left - dx + width >= size[0]) {
|
||||
dx = 0;
|
||||
}
|
||||
// If the viewport is already on the top or bottom, stop moving y.
|
||||
if (top - dy < 0 || top - dy + height >= size[1]) {
|
||||
dy = 0;
|
||||
}
|
||||
|
||||
// Translate tht graph and update minimap viewport.
|
||||
graph!
|
||||
.translate({
|
||||
dx: (dx * zoom) / ratio,
|
||||
dy: (dy * zoom) / ratio,
|
||||
})
|
||||
.then(() => {
|
||||
this.updateViewport();
|
||||
});
|
||||
x = e.clientX;
|
||||
y = e.clientY;
|
||||
};
|
||||
if (!isSafari && !isFireFox) {
|
||||
viewport.addEventListener('drag', dragListener.bind(this), false);
|
||||
}
|
||||
|
||||
const dragendListener = () => {
|
||||
dragging = false;
|
||||
resizing = false;
|
||||
this.options.refresh = true;
|
||||
};
|
||||
const dragendevent = isSafari || isFireFox ? 'mouseup' : 'dragend';
|
||||
viewport.addEventListener(dragendevent, dragendListener.bind(this), false);
|
||||
|
||||
const zoomListener = (evt) => {
|
||||
// TODO: zoom the graph and update viewport
|
||||
};
|
||||
viewport.addEventListener('wheel', zoomListener, false);
|
||||
|
||||
containerDOM.addEventListener('mouseleave', dragendListener.bind(this));
|
||||
containerDOM.addEventListener('mouseup', dragendListener.bind(this));
|
||||
|
||||
if (isSafari || isFireFox) {
|
||||
containerDOM.addEventListener(
|
||||
'mousemove',
|
||||
dragListener.bind(this),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
this.viewport = viewport;
|
||||
containerDOM.appendChild(viewport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the viewport DOM.
|
||||
*/
|
||||
private updateViewport() {
|
||||
if (this.destroyed) return;
|
||||
if (!this.viewport) {
|
||||
this.initViewport();
|
||||
}
|
||||
const { options, graph, dx, dy, ratio, viewport } = this;
|
||||
|
||||
const { size } = options;
|
||||
const graphCanvasEl = graph.canvas.context.config.canvas;
|
||||
const [
|
||||
graphWidth = graphCanvasEl?.scrollWidth || 500,
|
||||
graphHeight = graphCanvasEl?.scrollHeight || 500,
|
||||
] = graph.getSize();
|
||||
|
||||
const graphZoom = graph.getZoom();
|
||||
const graphBBox = graph.canvas.getRoot().getRenderBounds();
|
||||
|
||||
const graphTopLeftViewport = graph.getViewportByCanvas({
|
||||
x: graphBBox.min[0],
|
||||
y: graphBBox.min[1],
|
||||
});
|
||||
const graphBottomRightViewport = graph.getViewportByCanvas({
|
||||
x: graphBBox.max[0],
|
||||
y: graphBBox.max[1],
|
||||
});
|
||||
|
||||
// Width and height of the viewport DOM
|
||||
let width = (graphWidth * ratio) / graphZoom;
|
||||
let height = (graphHeight * ratio) / graphZoom;
|
||||
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
if (graphTopLeftViewport.x < 0) {
|
||||
left = (-graphTopLeftViewport.x / graphWidth) * width + dx;
|
||||
if (graphBottomRightViewport.x < graphWidth) {
|
||||
width = size[0] - left;
|
||||
}
|
||||
} else {
|
||||
width -= (graphTopLeftViewport.x / graphWidth) * width - dx;
|
||||
}
|
||||
if (graphTopLeftViewport.y < 0) {
|
||||
top = (-graphTopLeftViewport.y / graphHeight) * height + dy;
|
||||
if (graphBottomRightViewport.y < graphHeight) {
|
||||
height = size[1] - top;
|
||||
}
|
||||
} else {
|
||||
height -= (graphTopLeftViewport.y / graphHeight) * height - dy;
|
||||
}
|
||||
|
||||
const right = width + left;
|
||||
if (right > size[0]) {
|
||||
width -= right - size[0];
|
||||
}
|
||||
const bottom = height + top;
|
||||
if (bottom > size[1]) {
|
||||
height -= bottom - size[1];
|
||||
}
|
||||
|
||||
modifyCSS(viewport, {
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone all the graphic from main graph to the minimap graph.
|
||||
*/
|
||||
private updateGraphShapes() {
|
||||
const { graph, options, canvas } = this;
|
||||
const graphGroup = graph.canvas.getRoot();
|
||||
if (graphGroup.destroyed) return;
|
||||
canvas.removeChildren();
|
||||
let clonedGroup;
|
||||
const { hideEdge } = options;
|
||||
if (hideEdge) {
|
||||
clonedGroup = new Group();
|
||||
canvas.appendChild(clonedGroup);
|
||||
graphGroup.children.forEach((group) => {
|
||||
if (group.id === 'edge-group') return;
|
||||
clonedGroup.appendChild(group.cloneNode(true));
|
||||
});
|
||||
} else {
|
||||
clonedGroup = graphGroup.cloneNode(true);
|
||||
canvas.appendChild(clonedGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only draw keyShapes on the minimap.
|
||||
*/
|
||||
private updateKeyShapes() {
|
||||
const { graph, options, canvas } = this;
|
||||
const { hideEdge } = options;
|
||||
|
||||
const group = canvas.getRoot();
|
||||
|
||||
if (!hideEdge) {
|
||||
each(graph!.getAllEdgesData(), (edge) => {
|
||||
this.updateOneEdgeKeyShape(edge, group);
|
||||
});
|
||||
}
|
||||
each(graph!.getAllNodesData(), (node) => {
|
||||
this.updateOneNodeKeyShape(node, group);
|
||||
});
|
||||
const combos = graph!.getAllCombosData();
|
||||
if (combos && combos.length) {
|
||||
let comboGroup = group.find((e) => e.id === 'combo-group');
|
||||
if (!comboGroup) {
|
||||
comboGroup = new Group({ id: 'combo-group' });
|
||||
group.appendChild(comboGroup);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (this.destroyed) return;
|
||||
each(combos, (combo) => {
|
||||
// this.updateOneComboKeyShape(combo, comboGroup);
|
||||
this.updateOneNodeKeyShape(combo, comboGroup);
|
||||
});
|
||||
comboGroup?.sort();
|
||||
comboGroup?.toBack();
|
||||
this.updateCanvas();
|
||||
}, 250);
|
||||
}
|
||||
this.clearDestroyedShapes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update keyShape of one node.
|
||||
* @param nodeModel node data model
|
||||
* @param group container graphics group on minimap
|
||||
*/
|
||||
private updateOneNodeKeyShape(nodeModel, group) {
|
||||
const { itemMap = new Map(), graph } = this;
|
||||
const graphNodeGroup = graph.canvas
|
||||
.getRoot()
|
||||
.find((ele) => ele.id === 'node-group');
|
||||
if (!graphNodeGroup) return;
|
||||
|
||||
let { minimapItem, graphItem } = itemMap.get(nodeModel.id) || {};
|
||||
if (!minimapItem || minimapItem.destroyed) {
|
||||
graphItem = graphNodeGroup
|
||||
.find((ele) => ele.getAttribute('data-item-id') === nodeModel.id)
|
||||
?.find((ele) => ele.id === 'keyShape');
|
||||
minimapItem = graphItem.cloneNode();
|
||||
minimapItem.id = `minimap-keyShape-${nodeModel.id}`;
|
||||
group.appendChild(minimapItem);
|
||||
itemMap.set(nodeModel.id, { graphItem, minimapItem });
|
||||
}
|
||||
const bbox = graphItem.getRenderBounds();
|
||||
if (!bbox) return;
|
||||
const keyShapeStyle = graphItem.attributes;
|
||||
const attrs: any = {
|
||||
...keyShapeStyle,
|
||||
cx: bbox.center[0],
|
||||
cy: bbox.center[1],
|
||||
};
|
||||
minimapItem.toFront();
|
||||
|
||||
const shapeType = minimapItem.get('type');
|
||||
if (shapeType === 'rect' || shapeType === 'image' || shapeType === 'text') {
|
||||
attrs.x = bbox.min[0];
|
||||
attrs.y = bbox.min[1];
|
||||
}
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
minimapItem.style[key] = attrs[key];
|
||||
});
|
||||
|
||||
if (!graph.getItemVisible(nodeModel.id)) minimapItem.hide();
|
||||
else minimapItem.show();
|
||||
const zIndex = nodeModel.data.depth;
|
||||
if (!isNaN(zIndex)) minimapItem.set('zIndex', zIndex);
|
||||
this.itemMap = itemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the delegate rects for nodes and line edges on minimap.
|
||||
*/
|
||||
private updateDelegateShapes() {
|
||||
const { graph, options, canvas } = this;
|
||||
const { hideEdge } = options;
|
||||
|
||||
const group = canvas.getRoot();
|
||||
|
||||
// If hideEdge is true, do not render the edges on minimap to enhance the performance
|
||||
if (!hideEdge) {
|
||||
each(graph!.getAllEdgesData(), (edge) => {
|
||||
this.updateOneEdgeKeyShape(edge, group);
|
||||
});
|
||||
}
|
||||
each(graph!.getAllNodesData(), (node) => {
|
||||
this.updateOneNodeDelegateShape(node, group);
|
||||
});
|
||||
const combos = graph!.getAllCombosData();
|
||||
if (combos && combos.length) {
|
||||
const comboGroup =
|
||||
group.find((e) => e.get('name') === 'comboGroup') ||
|
||||
group.addGroup({
|
||||
name: 'comboGroup',
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (this.destroyed) return;
|
||||
each(combos, (combo) => {
|
||||
// this.updateOneComboKeyShape(combo, comboGroup);
|
||||
this.updateOneNodeKeyShape(combo, comboGroup);
|
||||
});
|
||||
comboGroup?.sort();
|
||||
comboGroup?.toBack();
|
||||
this.updateCanvas();
|
||||
}, 250);
|
||||
}
|
||||
this.clearDestroyedShapes();
|
||||
}
|
||||
|
||||
private clearDestroyedShapes() {
|
||||
const { itemMap = new Map() } = this;
|
||||
itemMap.forEach((val, key) => {
|
||||
const { minimapItem, graphItem } = val || {};
|
||||
if (graphItem.destroyed && minimapItem) {
|
||||
minimapItem.remove();
|
||||
itemMap.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update keyShape of one edge.
|
||||
* @param edgeModel edge data model
|
||||
* @param group container graphics group on minimap
|
||||
*/
|
||||
private updateOneEdgeKeyShape(edgeModel, group) {
|
||||
const { itemMap = new Map(), graph } = this;
|
||||
const graphEdgeGroup = graph.canvas
|
||||
.getRoot()
|
||||
.find((ele) => ele.id === 'edge-group');
|
||||
if (!graphEdgeGroup) return;
|
||||
let { minimapItem, graphItem } = itemMap.get(edgeModel.id) || {};
|
||||
if (minimapItem && !minimapItem.destroyed) {
|
||||
const { path, x1, x2, y1, y2 } = graphItem.style;
|
||||
minimapItem.style.path = path;
|
||||
minimapItem.style.x1 = x1;
|
||||
minimapItem.style.x2 = x2;
|
||||
minimapItem.style.y1 = y1;
|
||||
minimapItem.style.y2 = y2;
|
||||
} else {
|
||||
graphItem = graphEdgeGroup
|
||||
.find((ele) => ele.getAttribute('data-item-id') === edgeModel.id)
|
||||
?.find((ele) => ele.id === 'keyShape');
|
||||
minimapItem = graphItem.cloneNode();
|
||||
minimapItem.id = `minimap-keyShape-${edgeModel.id}`;
|
||||
group.appendChild(minimapItem);
|
||||
}
|
||||
if (!graph.getItemVisible(edgeModel.id)) minimapItem.hide();
|
||||
else minimapItem.show();
|
||||
itemMap.set(edgeModel.id, { graphItem, minimapItem });
|
||||
this.itemMap = itemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update delegate rect of one node.
|
||||
* @param nodeModel node data model
|
||||
* @param group container graphics group on minimap
|
||||
*/
|
||||
private updateOneNodeDelegateShape(nodeModel, group) {
|
||||
const { itemMap = new Map(), options, graph } = this;
|
||||
const { delegateStyle } = options;
|
||||
|
||||
const graphNodeGroup = graph.canvas
|
||||
.getRoot()
|
||||
.find((ele) => ele.id === 'node-group');
|
||||
if (!graphNodeGroup) return;
|
||||
|
||||
// 差量更新 minimap 上的一个节点,对应主图的 item
|
||||
let { minimapItem, graphItem } = itemMap.get(nodeModel.id) || {};
|
||||
if (!graphItem) {
|
||||
graphItem = graphNodeGroup
|
||||
.find((ele) => ele.getAttribute('data-item-id') === nodeModel.id)
|
||||
?.find((ele) => ele.id === 'keyShape');
|
||||
}
|
||||
|
||||
const bbox = graphItem.getRenderBounds();
|
||||
const attrs = {
|
||||
x: bbox.min[0],
|
||||
y: bbox.min[1],
|
||||
width: bbox.max[0] - bbox.min[0],
|
||||
height: bbox.max[1] - bbox.min[1],
|
||||
...delegateStyle,
|
||||
};
|
||||
if (!minimapItem || minimapItem.destroyed) {
|
||||
minimapItem = new Rect({
|
||||
style: {
|
||||
...graphItem.attributes,
|
||||
...attrs,
|
||||
...delegateStyle,
|
||||
},
|
||||
id: `minimap-delegate-${nodeModel.id}`,
|
||||
});
|
||||
group.appendChild(minimapItem);
|
||||
} else {
|
||||
Object.keys(attrs).forEach(
|
||||
(key) => (minimapItem.style[key] = attrs[key]),
|
||||
);
|
||||
}
|
||||
minimapItem.toFront();
|
||||
|
||||
if (!graph.getItemVisible(nodeModel.id)) minimapItem.hide();
|
||||
else minimapItem.show();
|
||||
itemMap.set(nodeModel.id, { graphItem, minimapItem });
|
||||
this.itemMap = itemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for main graph updating, update the viewport DOM.
|
||||
*/
|
||||
private handleUpdateCanvas = debounce(
|
||||
(event) => {
|
||||
const self = this;
|
||||
if (self.destroyed) return;
|
||||
self.updateCanvas();
|
||||
},
|
||||
100,
|
||||
false,
|
||||
);
|
||||
|
||||
private handleVisibilityChange = (params) => {
|
||||
const { ids, value } = params;
|
||||
ids.forEach((id) => {
|
||||
this.visibleCache[id] = value;
|
||||
});
|
||||
this.debounceCloneVisibility();
|
||||
};
|
||||
|
||||
private debounceCloneVisibility = debounce(
|
||||
() => {
|
||||
const nodeGroup = this.canvas.getRoot().getElementById('node-group');
|
||||
const edgeGroup = this.canvas.getRoot().getElementById('edge-group');
|
||||
(nodeGroup?.childNodes || [])
|
||||
.concat(edgeGroup?.childNodes || [])
|
||||
.forEach((child) => {
|
||||
const id = child.getAttribute?.('data-item-id');
|
||||
if (this.visibleCache.hasOwnProperty(id)) {
|
||||
if (this.visibleCache[id]) {
|
||||
child.childNodes.forEach((shape) => shape.show());
|
||||
} else if (this.visibleCache[id] === false) {
|
||||
child.childNodes.forEach((shape) => shape.hide());
|
||||
}
|
||||
}
|
||||
});
|
||||
this.visibleCache = {};
|
||||
},
|
||||
50,
|
||||
false,
|
||||
);
|
||||
|
||||
public init(graph: IGraph) {
|
||||
super.init(graph);
|
||||
const promise = this.initContainer();
|
||||
promise.then(() => this.updateCanvas());
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the DOM container for minimap.
|
||||
*/
|
||||
public initContainer() {
|
||||
const { graph, options } = this;
|
||||
const { size, className } = options;
|
||||
let parentNode = options.container;
|
||||
const container: HTMLDivElement = createDom(
|
||||
`<div class='${className}' style='width: ${size[0]}px; height: ${size[1]}px; overflow: hidden;'></div>`,
|
||||
);
|
||||
|
||||
if (isString(parentNode)) {
|
||||
parentNode = document.getElementById(parentNode) as HTMLDivElement;
|
||||
}
|
||||
|
||||
if (parentNode) {
|
||||
parentNode.appendChild(container);
|
||||
} else {
|
||||
graph.container.appendChild(container);
|
||||
}
|
||||
|
||||
if (this.container) {
|
||||
this.container.remove();
|
||||
this.viewport?.remove();
|
||||
this.viewport = undefined;
|
||||
this.canvas?.destroy();
|
||||
}
|
||||
this.container = container;
|
||||
|
||||
const containerDOM = createDom(
|
||||
'<div class="g6-minimap-container" style="position: relative;"></div>',
|
||||
);
|
||||
container.appendChild(containerDOM);
|
||||
containerDOM.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
containerDOM.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// TODO: graph.rendererType
|
||||
const graphCanvas = graph.canvas;
|
||||
this.canvas = createCanvas(
|
||||
'canvas',
|
||||
containerDOM,
|
||||
size[0],
|
||||
size[1],
|
||||
graphCanvas.devicePixelRatio,
|
||||
);
|
||||
|
||||
return this.canvas.ready;
|
||||
}
|
||||
|
||||
public updateCanvas() {
|
||||
if (this.destroyed) return;
|
||||
const { graph, canvas, options } = this;
|
||||
const { refresh, size, padding, mode } = options;
|
||||
// Controlled by the animation of graph. Animating, and then disable refreshing
|
||||
if (!refresh || graph.destroyed || canvas.destroyed) return;
|
||||
|
||||
switch (mode) {
|
||||
case DEFAULT_MODE:
|
||||
this.updateGraphShapes();
|
||||
break;
|
||||
case KEYSHAPE_MODE:
|
||||
this.updateKeyShapes();
|
||||
break;
|
||||
case DELEGATE_MODE:
|
||||
this.updateDelegateShapes();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const group = canvas.getRoot();
|
||||
if (!group) return;
|
||||
|
||||
const minimapBBox = group.getRenderBounds();
|
||||
const graphBBox = graph.canvas.getRoot().getRenderBounds();
|
||||
const width = graphBBox.max[0] - graphBBox.min[0];
|
||||
const height = graphBBox.max[1] - graphBBox.min[1];
|
||||
|
||||
// Scale the graph to fit the size - padding of the minimap container
|
||||
const zoomRatio = Math.min(
|
||||
(size[0] - 2 * padding) / width,
|
||||
(size[1] - 2 * padding) / height,
|
||||
);
|
||||
const zoomCenter = canvas.viewport2Canvas({ x: 0, y: 0 });
|
||||
canvas.getCamera().setFocalPoint(zoomCenter.x, zoomCenter.y);
|
||||
canvas.getCamera().setPosition(zoomCenter.x, zoomCenter.y);
|
||||
canvas.getCamera().setZoom(zoomRatio);
|
||||
canvas
|
||||
.getCamera()
|
||||
.setPosition(minimapBBox.center[0], minimapBBox.center[1]);
|
||||
canvas
|
||||
.getCamera()
|
||||
.setFocalPoint(minimapBBox.center[0], minimapBBox.center[1]);
|
||||
|
||||
const { x: dx, y: dy } = canvas.canvas2Viewport({
|
||||
x: minimapBBox.min[0],
|
||||
y: minimapBBox.min[1],
|
||||
});
|
||||
|
||||
// Update the viewport DOM
|
||||
this.ratio = zoomRatio;
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this.updateViewport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canvas of the minimap.
|
||||
* @return {Canvas} G Canvas
|
||||
*/
|
||||
public getCanvas(): Canvas {
|
||||
return this.canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the viewport DOM of the minimap.
|
||||
* @return {HTMLElement} viewport DOM
|
||||
*/
|
||||
public getViewport(): HTMLElement {
|
||||
return this.viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the container DOM of the minimap.
|
||||
* @return {HTMLElement} container DOM
|
||||
*/
|
||||
public getContainer(): HTMLElement {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.canvas?.destroy();
|
||||
const container = this.container;
|
||||
if (container?.parentNode) container.parentNode.removeChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
const getMoveAtBorder = (dom, evt) => {
|
||||
const bounds = dom.getBoundingClientRect();
|
||||
const { clientX, clientY } = evt;
|
||||
if (Math.abs(clientX - bounds.x) < 4 && Math.abs(clientY - bounds.y) < 4) {
|
||||
return 'left-top';
|
||||
} else if (
|
||||
Math.abs(clientX - bounds.x) < 4 &&
|
||||
Math.abs(clientY - bounds.y - bounds.height) < 4
|
||||
) {
|
||||
return 'left-bottom';
|
||||
} else if (
|
||||
Math.abs(clientX - bounds.x - bounds.width) < 4 &&
|
||||
Math.abs(clientY - bounds.y) < 4
|
||||
) {
|
||||
return 'right-top';
|
||||
} else if (
|
||||
Math.abs(clientX - bounds.x - bounds.width) < 4 &&
|
||||
Math.abs(clientY - bounds.y - bounds.height) < 4
|
||||
) {
|
||||
return 'right-bottom';
|
||||
} else if (Math.abs(clientX - bounds.x) < 4) {
|
||||
return 'left';
|
||||
} else if (Math.abs(clientY - bounds.y) < 4) {
|
||||
return 'top';
|
||||
} else if (Math.abs(clientY - bounds.y - bounds.height) < 4) {
|
||||
return 'bottom';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const cursorMap = {
|
||||
'left-top': 'nwse-resize',
|
||||
'right-bottom': 'nwse-resize',
|
||||
'right-top': 'nesw-resize',
|
||||
'left-bottom': 'nesw-resize',
|
||||
left: 'ew-resize',
|
||||
right: 'ew-resize',
|
||||
top: 'ns-resize',
|
||||
bottom: 'ns-resize',
|
||||
};
|
@ -54,6 +54,8 @@ export class Minimap extends Base {
|
||||
private dx: number;
|
||||
/** Distance from left of minimap graph to the left of minimap container. */
|
||||
private dy: number;
|
||||
/** Cache the visibility while items' visibility changed. And apply them onto the minimap with debounce. */
|
||||
private visibleCache: { [id: string]: boolean } = {};
|
||||
|
||||
constructor(options?: MiniMapConfig) {
|
||||
super(options);
|
||||
@ -80,11 +82,11 @@ export class Minimap extends Base {
|
||||
|
||||
public getEvents() {
|
||||
return {
|
||||
afterupdateitem: this.handleUpdateCanvas,
|
||||
afteritemstatechange: this.handleUpdateCanvas,
|
||||
afterlayout: this.handleUpdateCanvas,
|
||||
viewportchange: this.handleUpdateCanvas,
|
||||
afteritemchange: this.handleUpdateCanvas,
|
||||
afteritemvisibilitychange: this.handleVisibilityChange,
|
||||
};
|
||||
}
|
||||
|
||||
@ -115,12 +117,12 @@ export class Minimap extends Base {
|
||||
style='position:absolute;
|
||||
left:0;
|
||||
top:0;
|
||||
box-sizing:border-box;
|
||||
border: 2px solid #1980ff;
|
||||
box-sizing:border-box;
|
||||
background: rgba(0, 0, 255, 0.1);
|
||||
cursor:move'
|
||||
draggable=${isSafari || isFireFox ? false : true}
|
||||
</div>`);
|
||||
/>`);
|
||||
|
||||
// Last mouse x position
|
||||
let x = 0;
|
||||
@ -128,11 +130,25 @@ export class Minimap extends Base {
|
||||
let y = 0;
|
||||
// Whether in dragging status
|
||||
let dragging = false;
|
||||
let resizing = false;
|
||||
|
||||
const dragstartevent = isSafari || isFireFox ? 'mousedown' : 'dragstart';
|
||||
this.container.addEventListener('mousemove', (e) => {
|
||||
const moveAtBorder = getMoveAtBorder(viewport, e);
|
||||
if (moveAtBorder) {
|
||||
this.container.style.cursor = cursorMap[moveAtBorder];
|
||||
viewport.style.cursor = cursorMap[moveAtBorder];
|
||||
} else {
|
||||
this.container.style.cursor = 'unset';
|
||||
viewport.style.cursor = 'move';
|
||||
}
|
||||
});
|
||||
viewport.addEventListener(
|
||||
dragstartevent,
|
||||
((e: IG6GraphEvent) => {
|
||||
resizing = getMoveAtBorder(viewport, e);
|
||||
if (resizing) return;
|
||||
|
||||
if ((e as any).dataTransfer) {
|
||||
const img = new Image();
|
||||
img.src =
|
||||
@ -159,6 +175,30 @@ export class Minimap extends Base {
|
||||
);
|
||||
|
||||
const dragListener = (e: IG6GraphEvent) => {
|
||||
const { style } = viewport;
|
||||
const left = parseInt(style.left, 10);
|
||||
const top = parseInt(style.top, 10);
|
||||
const width = parseInt(style.width, 10);
|
||||
const height = parseInt(style.height, 10);
|
||||
|
||||
if (resizing) {
|
||||
const { clientX, clientY } = e;
|
||||
const afterResize = { left, top, width, height };
|
||||
if (resizing.includes('left')) {
|
||||
afterResize.left = `${clientX}px`;
|
||||
afterResize.width = `${left + width - clientX}px`;
|
||||
} else if (resizing.includes('right')) {
|
||||
afterResize.width = `${clientX - left}px`;
|
||||
}
|
||||
if (resizing.includes('top')) {
|
||||
afterResize.top = `${clientY}`;
|
||||
afterResize.height = `${top + height - clientY}`;
|
||||
} else if (resizing.includes('bottom')) {
|
||||
afterResize.height = `${clientY - top}`;
|
||||
}
|
||||
modifyCSS(viewport, afterResize);
|
||||
return;
|
||||
}
|
||||
if (!dragging || isNil(e.clientX) || isNil(e.clientY)) {
|
||||
return;
|
||||
}
|
||||
@ -168,12 +208,6 @@ export class Minimap extends Base {
|
||||
let dx = x - e.clientX;
|
||||
let dy = y - e.clientY;
|
||||
|
||||
const { style } = viewport;
|
||||
const left = parseInt(style.left, 10);
|
||||
const top = parseInt(style.top, 10);
|
||||
const width = parseInt(style.width, 10);
|
||||
const height = parseInt(style.height, 10);
|
||||
|
||||
// If the viewport is already on the left or right, stop moving x.
|
||||
if (left - dx < 0 || left - dx + width >= size[0]) {
|
||||
dx = 0;
|
||||
@ -201,6 +235,7 @@ export class Minimap extends Base {
|
||||
|
||||
const dragendListener = () => {
|
||||
dragging = false;
|
||||
resizing = false;
|
||||
this.options.refresh = true;
|
||||
};
|
||||
const dragendevent = isSafari || isFireFox ? 'mouseup' : 'dragend';
|
||||
@ -467,8 +502,12 @@ export class Minimap extends Base {
|
||||
if (!graphEdgeGroup) return;
|
||||
let { minimapItem, graphItem } = itemMap.get(edgeModel.id) || {};
|
||||
if (minimapItem && !minimapItem.destroyed) {
|
||||
const path = graphItem.style.path;
|
||||
const { path, x1, x2, y1, y2 } = graphItem.style;
|
||||
minimapItem.style.path = path;
|
||||
minimapItem.style.x1 = x1;
|
||||
minimapItem.style.x2 = x2;
|
||||
minimapItem.style.y1 = y1;
|
||||
minimapItem.style.y2 = y2;
|
||||
} else {
|
||||
graphItem = graphEdgeGroup
|
||||
.find((ele) => ele.getAttribute('data-item-id') === edgeModel.id)
|
||||
@ -549,6 +588,36 @@ export class Minimap extends Base {
|
||||
false,
|
||||
);
|
||||
|
||||
private handleVisibilityChange = (params) => {
|
||||
const { ids, value } = params;
|
||||
ids.forEach((id) => {
|
||||
this.visibleCache[id] = value;
|
||||
});
|
||||
this.debounceCloneVisibility();
|
||||
};
|
||||
|
||||
private debounceCloneVisibility = debounce(
|
||||
() => {
|
||||
const nodeGroup = this.canvas.getRoot().getElementById('node-group');
|
||||
const edgeGroup = this.canvas.getRoot().getElementById('edge-group');
|
||||
(nodeGroup?.childNodes || [])
|
||||
.concat(edgeGroup?.childNodes || [])
|
||||
.forEach((child) => {
|
||||
const id = child.getAttribute?.('data-item-id');
|
||||
if (this.visibleCache.hasOwnProperty(id)) {
|
||||
if (this.visibleCache[id]) {
|
||||
child.childNodes.forEach((shape) => shape.show());
|
||||
} else if (this.visibleCache[id] === false) {
|
||||
child.childNodes.forEach((shape) => shape.hide());
|
||||
}
|
||||
}
|
||||
});
|
||||
this.visibleCache = {};
|
||||
},
|
||||
50,
|
||||
false,
|
||||
);
|
||||
|
||||
public init(graph: IGraph) {
|
||||
super.init(graph);
|
||||
const promise = this.initContainer();
|
||||
@ -696,3 +765,44 @@ export class Minimap extends Base {
|
||||
if (container?.parentNode) container.parentNode.removeChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
const getMoveAtBorder = (dom, evt) => {
|
||||
const bounds = dom.getBoundingClientRect();
|
||||
const { clientX, clientY } = evt;
|
||||
if (Math.abs(clientX - bounds.x) < 4 && Math.abs(clientY - bounds.y) < 4) {
|
||||
return 'left-top';
|
||||
} else if (
|
||||
Math.abs(clientX - bounds.x) < 4 &&
|
||||
Math.abs(clientY - bounds.y - bounds.height) < 4
|
||||
) {
|
||||
return 'left-bottom';
|
||||
} else if (
|
||||
Math.abs(clientX - bounds.x - bounds.width) < 4 &&
|
||||
Math.abs(clientY - bounds.y) < 4
|
||||
) {
|
||||
return 'right-top';
|
||||
} else if (
|
||||
Math.abs(clientX - bounds.x - bounds.width) < 4 &&
|
||||
Math.abs(clientY - bounds.y - bounds.height) < 4
|
||||
) {
|
||||
return 'right-bottom';
|
||||
} else if (Math.abs(clientX - bounds.x) < 4) {
|
||||
return 'left';
|
||||
} else if (Math.abs(clientY - bounds.y) < 4) {
|
||||
return 'top';
|
||||
} else if (Math.abs(clientY - bounds.y - bounds.height) < 4) {
|
||||
return 'bottom';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const cursorMap = {
|
||||
'left-top': 'nwse-resize',
|
||||
'right-bottom': 'nwse-resize',
|
||||
'right-top': 'nesw-resize',
|
||||
'left-bottom': 'nesw-resize',
|
||||
left: 'ew-resize',
|
||||
right: 'ew-resize',
|
||||
top: 'ns-resize',
|
||||
bottom: 'ns-resize',
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import { ID } from '@antv/graphlib';
|
||||
import { AABB, DisplayObject } from '@antv/g';
|
||||
import { throttle } from '@antv/util';
|
||||
import { ITEM_TYPE, ShapeStyle } from '../../../types/item';
|
||||
import { IG6GraphEvent, IGraph } from '../../../types';
|
||||
import { IG6GraphEvent, IGraph, NodeModel } from '../../../types';
|
||||
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
|
||||
import { Point } from '../../../types/common';
|
||||
|
||||
@ -46,6 +46,11 @@ export class Snapline extends Base {
|
||||
offsetY: number | undefined,
|
||||
] = [undefined, undefined];
|
||||
|
||||
/**
|
||||
* Cache the nodes' positions to be throttly updated.
|
||||
*/
|
||||
private updateCache: Map<ID, NodeModel> = new Map();
|
||||
|
||||
/**
|
||||
* Gets the current cursor node and center offset and then with initialOffset difference
|
||||
*/
|
||||
@ -70,14 +75,9 @@ export class Snapline extends Base {
|
||||
];
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
private dragging = false;
|
||||
private draggingBBox: AABB = undefined;
|
||||
|
||||
// the offset(cursor and draggingbox) when start to drag
|
||||
private nonAbosorbOffset: Point = undefined;
|
||||
|
||||
// Gets the offset between the cursor and draggingItem
|
||||
private getPointerOffsetWithItem = (pointer: {
|
||||
x: number;
|
||||
@ -118,6 +118,7 @@ export class Snapline extends Base {
|
||||
* @param dl drawLine
|
||||
*/
|
||||
getDrawLineIdByDrawLine(dl: DrawLine): ID {
|
||||
if (!dl) return;
|
||||
return `${dl.line[0].x}-${dl.line[0].y}-${dl.line[1].x}-${dl.line[1].y}`;
|
||||
}
|
||||
/**
|
||||
@ -777,11 +778,6 @@ export class Snapline extends Base {
|
||||
event.canvas.x - this.draggingBBox.center[0],
|
||||
event.canvas.y - this.draggingBBox.center[1],
|
||||
];
|
||||
|
||||
this.nonAbosorbOffset = this.getPointerOffsetWithItem({
|
||||
x: event.canvas.x,
|
||||
y: event.canvas.y,
|
||||
});
|
||||
}
|
||||
|
||||
onDrag = throttle(
|
||||
@ -790,7 +786,7 @@ export class Snapline extends Base {
|
||||
|
||||
this.initAlignLinesForChoose();
|
||||
|
||||
// control layer:Check to delete existing line & drawed line & adsorption
|
||||
// control layer: Check to delete existing line & drawed line & adsorption
|
||||
// set historyPoints
|
||||
|
||||
this.historyPoints[0] = this.historyPoints[1];
|
||||
@ -936,10 +932,10 @@ export class Snapline extends Base {
|
||||
}
|
||||
}
|
||||
}).bind(this),
|
||||
10,
|
||||
16,
|
||||
{
|
||||
leading: true,
|
||||
trailing: false,
|
||||
leading: false,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
@ -1272,6 +1268,30 @@ export class Snapline extends Base {
|
||||
}
|
||||
|
||||
doUpdatePosition(data) {
|
||||
this.graph.updateNodePosition({ id: this.dragItemId, data }, false, true);
|
||||
const cache = this.updateCache.get(this.dragItemId);
|
||||
this.updateCache.set(this.dragItemId, {
|
||||
id: this.dragItemId,
|
||||
data: {
|
||||
...cache?.data,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
this.throttleUpdate();
|
||||
}
|
||||
|
||||
throttleUpdate = throttle(
|
||||
() => {
|
||||
this.graph.updateNodePosition(
|
||||
Array.from(this.updateCache.values()),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
this.updateCache.clear();
|
||||
},
|
||||
16,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -8,3 +8,4 @@ export type { ToolbarConfig } from './toolbar';
|
||||
export type { TooltipConfig } from './tooltip';
|
||||
export type { TimebarConfig } from './timebar';
|
||||
export type { SnapLineConfig } from './snapline';
|
||||
export type { EdgeFilterLens } from './edgeFilterLens';
|
||||
|
309
packages/g6/src/stdlib/plugin/watermaker/index.ts
Normal file
@ -0,0 +1,309 @@
|
||||
import { uniqueId, deepMix, isString } from '@antv/util';
|
||||
import { createDom, modifyCSS } from '@antv/dom-util';
|
||||
import { Canvas, CanvasLike } from '@antv/g';
|
||||
import { Plugin as Base, IPluginBaseConfig } from '../../../types/plugin';
|
||||
import { createCanvas } from '../../../util/canvas';
|
||||
import { IGraph } from 'types';
|
||||
|
||||
/** Define configuration types for image and text watermarks */
|
||||
type ImageWaterMarkerConfig = {
|
||||
/** URL of the image used as the watermark. */
|
||||
imgURL: string;
|
||||
/** Width of the image watermark in pixels. */
|
||||
width: number;
|
||||
/** Height of the image watermark in pixels. */
|
||||
height: number;
|
||||
/** Horizontal position of the image watermark on the canvas. */
|
||||
x: number;
|
||||
/** Vertical position of the image watermark on the canvas. */
|
||||
y: number;
|
||||
/** Rotation angle of the image watermark in degrees (0 to 360). */
|
||||
rotate: number;
|
||||
};
|
||||
type TextWaterMarkerConfig = {
|
||||
/** Text or an array of texts to be used as the watermark content. */
|
||||
texts: string | string[];
|
||||
/** Horizontal position of the text watermark on the canvas. */
|
||||
x: number;
|
||||
/** Vertical position of the text watermark on the canvas. */
|
||||
y: number;
|
||||
/** Line height between multiple lines of text in pixels. */
|
||||
lineHeight: number;
|
||||
/** Rotation angle of the text watermark in degrees (0 to 360). */
|
||||
rotate: number;
|
||||
/** Font size of the text in pixels. */
|
||||
fontSize: number;
|
||||
/** Font family used for the text (e.g., "Microsoft YaHei"). */
|
||||
fontFamily: string;
|
||||
/** Text fill color (e.g., "rgba(0, 0, 0, 0.1)"). */
|
||||
fill: string;
|
||||
/** Text baseline alignment (e.g., "Middle"). */
|
||||
baseline: string;
|
||||
};
|
||||
|
||||
/** Define configuration types for watermarks */
|
||||
export interface WaterMarkerConfig extends IPluginBaseConfig {
|
||||
/** (Optional) The CSS class name applied to the watermark container. */
|
||||
className?: string;
|
||||
/** (Optional) The width of the watermark canvas in pixels. */
|
||||
width?: number;
|
||||
/** (Optional) The height of the watermark canvas in pixels. */
|
||||
height?: number;
|
||||
/** (Optional) The mode of the watermark, either 'image' or 'text'. */
|
||||
mode?: 'image' | 'text';
|
||||
/** (Optional) The position of the watermark, default: under the main canvas*/
|
||||
position?: 'top' | 'mid' | 'bottom';
|
||||
/** (Optional) Configuration for an image watermark (See ImageWaterMarkerConfig). */
|
||||
image?: ImageWaterMarkerConfig;
|
||||
/** (Optional) Configuration for a text watermark (See TextWaterMarkerConfig). */
|
||||
text?: TextWaterMarkerConfig;
|
||||
}
|
||||
|
||||
export class WaterMarker extends Base {
|
||||
private container: HTMLDivElement;
|
||||
private canvas: Canvas;
|
||||
|
||||
constructor(options?: WaterMarkerConfig) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public getDefaultCfgs(): WaterMarkerConfig {
|
||||
return {
|
||||
key: `watermarker-${uniqueId()}`,
|
||||
container: null,
|
||||
className: 'g6-watermarker',
|
||||
mode: 'image',
|
||||
width: 150,
|
||||
height: 100,
|
||||
position: 'bottom',
|
||||
image: {
|
||||
imgURL:
|
||||
'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 94,
|
||||
height: 28,
|
||||
rotate: 0,
|
||||
},
|
||||
text: {
|
||||
texts: 'AntV',
|
||||
x: 0,
|
||||
y: 60,
|
||||
lineHeight: 20,
|
||||
rotate: 20,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Microsoft YaHei',
|
||||
fill: 'rgba(0, 0, 0, 0.1)',
|
||||
baseline: 'Middle',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the WaterMarker plugin.
|
||||
*/
|
||||
public init(graph: IGraph) {
|
||||
super.init(graph);
|
||||
const promise = this.initCanvas();
|
||||
promise.then(() => {
|
||||
this.updateWaterMarker();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the canvas for watermark rendering.
|
||||
*/
|
||||
public initCanvas() {
|
||||
const { graph, options } = this;
|
||||
const { width, height } = options;
|
||||
let parentNode = options.container;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
if (isString(parentNode)) {
|
||||
parentNode = document.getElementById(parentNode) as HTMLDivElement;
|
||||
}
|
||||
if (parentNode) {
|
||||
container = parentNode;
|
||||
} else {
|
||||
container = graph.container as HTMLDivElement;
|
||||
}
|
||||
if (!container.style.position) {
|
||||
container.style.position = 'relative';
|
||||
}
|
||||
|
||||
this.canvas = createCanvas('canvas', container, width, height);
|
||||
this.container = container;
|
||||
return this.canvas.ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the watermark based on the chosen mode (image or text).
|
||||
*/
|
||||
public updateWaterMarker() {
|
||||
switch (this.options.mode) {
|
||||
case 'image':
|
||||
this.setImageWaterMarker();
|
||||
break;
|
||||
case 'text':
|
||||
this.setTextWaterMarker();
|
||||
break;
|
||||
default:
|
||||
this.setImageWaterMarker();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an image watermark on the canvas.
|
||||
*/
|
||||
public setImageWaterMarker() {
|
||||
const { container, canvas, options } = this;
|
||||
const { className, image, position } = options;
|
||||
const { imgURL, x, y, width, height, rotate } = image;
|
||||
|
||||
if (canvas) canvas.destroy();
|
||||
|
||||
const canvasContextService = canvas.getContextService();
|
||||
(canvasContextService.getDomElement() as any).style.display = 'none';
|
||||
const ctx = canvasContextService.getContext() as any;
|
||||
ctx.rotate((-rotate * Math.PI) / 180);
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.src = imgURL;
|
||||
img.onload = async () => {
|
||||
ctx.drawImage(img, x, y, width, height);
|
||||
ctx.rotate((rotate * Math.PI) / 180);
|
||||
let dataURL = '';
|
||||
await canvasContextService
|
||||
.toDataURL({ type: 'image/png' })
|
||||
.then((url) => {
|
||||
dataURL = url;
|
||||
});
|
||||
let box = document.querySelector(`.${className}`) as HTMLElement;
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.className = className;
|
||||
}
|
||||
if (canvas) {
|
||||
box.style.cssText = `background-image: url(${dataURL});background-repeat:repeat;position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;`;
|
||||
let canvas = this.graph.canvas.getContextService().getDomElement() as any;
|
||||
let transientCanvas = this.graph.transientCanvas
|
||||
.getContextService()
|
||||
.getDomElement() as any;
|
||||
container.removeChild(canvas);
|
||||
container.removeChild(transientCanvas);
|
||||
switch (position) {
|
||||
case 'top':
|
||||
container.appendChild(canvas);
|
||||
container.appendChild(transientCanvas);
|
||||
container.appendChild(box);
|
||||
break;
|
||||
case 'mid':
|
||||
container.appendChild(canvas);
|
||||
container.appendChild(box);
|
||||
container.appendChild(transientCanvas);
|
||||
break;
|
||||
case 'bottom':
|
||||
default:
|
||||
container.appendChild(box);
|
||||
container.appendChild(canvas);
|
||||
container.appendChild(transientCanvas);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a text watermark on the canvas.
|
||||
*/
|
||||
public async setTextWaterMarker() {
|
||||
const { container, canvas, options } = this;
|
||||
const { text, className, position } = options;
|
||||
const {
|
||||
rotate,
|
||||
fill,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
baseline,
|
||||
x,
|
||||
y,
|
||||
lineHeight,
|
||||
texts,
|
||||
} = text;
|
||||
|
||||
if (canvas) canvas.destroy();
|
||||
|
||||
const canvasContextService = canvas.getContextService();
|
||||
(canvasContextService.getDomElement() as any).style.display = 'none';
|
||||
const ctx = canvasContextService.getContext() as any;
|
||||
ctx.rotate((-rotate * Math.PI) / 180);
|
||||
ctx.font = `${fontSize}px ${fontFamily}`;
|
||||
ctx.fillStyle = fill;
|
||||
ctx.textBaseline = baseline;
|
||||
let displayTexts = isString(texts) ? [texts] : texts;
|
||||
for (let i = displayTexts.length - 1; i >= 0; i--) {
|
||||
ctx.fillText(displayTexts[i], x, y + i * lineHeight);
|
||||
}
|
||||
ctx.rotate((rotate * Math.PI) / 180);
|
||||
let dataURL = '';
|
||||
await canvasContextService.toDataURL({ type: 'image/png' }).then((url) => {
|
||||
dataURL = url;
|
||||
});
|
||||
|
||||
let box = document.querySelector(`.${className}`) as HTMLElement;
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.className = className;
|
||||
}
|
||||
if(canvas){
|
||||
box.style.cssText = `background-image: url(${dataURL});background-repeat:repeat;position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;`;
|
||||
let canvas = this.graph.canvas.getContextService().getDomElement() as any;
|
||||
let transientCanvas = this.graph.transientCanvas
|
||||
.getContextService()
|
||||
.getDomElement() as any;
|
||||
container.removeChild(canvas);
|
||||
container.removeChild(transientCanvas);
|
||||
switch (position) {
|
||||
case 'top':
|
||||
container.appendChild(canvas);
|
||||
container.appendChild(transientCanvas);
|
||||
container.appendChild(box);
|
||||
break;
|
||||
case 'mid':
|
||||
container.appendChild(canvas);
|
||||
container.appendChild(box);
|
||||
container.appendChild(transientCanvas);
|
||||
break;
|
||||
case 'bottom':
|
||||
default:
|
||||
container.appendChild(box);
|
||||
container.appendChild(canvas);
|
||||
container.appendChild(transientCanvas);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the WaterMarker and remove it from the container.
|
||||
*/
|
||||
public destroy() {
|
||||
const { canvas, options } = this;
|
||||
const { className } = options;
|
||||
|
||||
let parentNode = options.container;
|
||||
let container: HTMLDivElement;
|
||||
|
||||
if (!parentNode) {
|
||||
container = this.graph.container as HTMLDivElement;
|
||||
}
|
||||
if (isString(container)) {
|
||||
container = document.getElementById(container) as HTMLDivElement;
|
||||
}
|
||||
|
||||
const box = document.querySelector(`.${className}`) as HTMLElement;
|
||||
container.removeChild(box);
|
||||
canvas.destroy();
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import EventEmitter from '@antv/event-emitter';
|
||||
import { AABB, Canvas, DisplayObject, PointLike } from '@antv/g';
|
||||
import { AABB, Canvas, Cursor, DisplayObject, PointLike } from '@antv/g';
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { Command } from '../stdlib/plugin/history/command';
|
||||
import { Hooks } from '../types/hook';
|
||||
@ -10,11 +10,11 @@ import { Padding, Point } from './common';
|
||||
import { GraphData } from './data';
|
||||
import { EdgeModel, EdgeUserModel } from './edge';
|
||||
import type { StackType } from './history';
|
||||
import { ITEM_TYPE, SHAPE_TYPE } from './item';
|
||||
import { ITEM_TYPE, SHAPE_TYPE, ShapeStyle } from './item';
|
||||
import { LayoutOptions } from './layout';
|
||||
import { NodeModel, NodeUserModel } from './node';
|
||||
import { RendererName } from './render';
|
||||
import { Specification } from './spec';
|
||||
import { ComboMapper, EdgeMapper, NodeMapper, Specification } from './spec';
|
||||
import { ThemeOptionsOf, ThemeRegistry } from './theme';
|
||||
import { FitViewRules, GraphTransformOptions } from './view';
|
||||
|
||||
@ -36,7 +36,7 @@ export interface IGraph<
|
||||
* @returns
|
||||
* @group Graph Instance
|
||||
*/
|
||||
destroy: (callback?: Function) => void;
|
||||
destroy: (callback?: () => void) => void;
|
||||
/**
|
||||
* Update the specs (configurations).
|
||||
*/
|
||||
@ -45,6 +45,12 @@ export interface IGraph<
|
||||
* Update the theme specs (configurations).
|
||||
*/
|
||||
updateTheme: (theme: ThemeOptionsOf<T>) => void;
|
||||
/**
|
||||
* Update the item display mapper for a specific item type.
|
||||
* @param {ITEM_TYPE} type - The type of item (node, edge, or combo).
|
||||
* @param {NodeMapper | EdgeMapper | ComboMapper} mapper - The mapper to be updated.
|
||||
* */
|
||||
updateMapper(type: ITEM_TYPE, mapper: NodeMapper | EdgeMapper | ComboMapper);
|
||||
/**
|
||||
* Get the copy of specs(configurations).
|
||||
* @returns graph specs
|
||||
@ -389,8 +395,8 @@ export interface IGraph<
|
||||
*/
|
||||
fitView: (
|
||||
options?: {
|
||||
padding: Padding;
|
||||
rules: FitViewRules;
|
||||
padding?: Padding;
|
||||
rules?: FitViewRules;
|
||||
},
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) => Promise<void>;
|
||||
@ -400,7 +406,10 @@ export interface IGraph<
|
||||
* @returns
|
||||
* @group View
|
||||
*/
|
||||
fitCenter: (effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
fitCenter: (
|
||||
boundsType?: 'render' | 'layout',
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Move the graph to make the item align the view center.
|
||||
* @param item node/edge/combo item or its id
|
||||
@ -595,6 +604,11 @@ export interface IGraph<
|
||||
* @group Interaction
|
||||
*/
|
||||
getMode: () => string;
|
||||
/**
|
||||
* Set the cursor. But the cursor in item's style has higher priority.
|
||||
* @param cursor
|
||||
*/
|
||||
setCursor: (cursor: Cursor) => void;
|
||||
/**
|
||||
* Add behavior(s) to mode(s).
|
||||
* @param behaviors behavior names or configs
|
||||
@ -632,7 +646,22 @@ export interface IGraph<
|
||||
drawTransient: (
|
||||
type: ITEM_TYPE | SHAPE_TYPE,
|
||||
id: ID,
|
||||
config: any,
|
||||
config: {
|
||||
action?: 'remove' | 'add' | 'update' | undefined;
|
||||
/** Data to be merged into the transient item. */
|
||||
data?: Record<string, any>;
|
||||
/** Style to be merged into the transient shape. */
|
||||
style?: ShapeStyle;
|
||||
/** For type: 'edge' */
|
||||
drawSource?: boolean;
|
||||
/** For type: 'edge' */
|
||||
drawTarget?: boolean;
|
||||
/** Only shape with id in shapeIds will be cloned while type is ITEM_TYPE. If shapeIds is not assigned, the whole item will be cloned. */
|
||||
shapeIds?: string[];
|
||||
/** Whether show the shapes in shapeIds. True by default. */
|
||||
visible?: boolean;
|
||||
upsertAncestors?: boolean;
|
||||
},
|
||||
canvas?: Canvas,
|
||||
) => DisplayObject;
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { ThemeSpecification } from './theme';
|
||||
import { GraphTransformOptions } from './view';
|
||||
import { ComboModel } from './combo';
|
||||
import { Plugin as PluginBase } from './plugin';
|
||||
import { ComboMapper, EdgeMapper, NodeMapper } from './spec';
|
||||
|
||||
export interface IHook<T> {
|
||||
name: string;
|
||||
@ -101,8 +102,17 @@ export interface Hooks {
|
||||
id: ID;
|
||||
canvas: Canvas;
|
||||
config: {
|
||||
style: ShapeStyle;
|
||||
action: 'remove' | 'add' | 'update' | undefined;
|
||||
style?: ShapeStyle;
|
||||
action?: 'remove' | 'add' | 'update';
|
||||
/** For type: 'edge' */
|
||||
drawSource?: boolean;
|
||||
/** For type: 'edge' */
|
||||
drawTarget?: boolean;
|
||||
/** Only shape with id in shapeIds will be cloned while type is ITEM_TYPE. If shapeIds is not assigned, the whole item will be cloned. */
|
||||
shapeIds?: string[];
|
||||
/** Whether show the shapes in shapeIds. True by default. */
|
||||
visible?: boolean;
|
||||
upsertAncestors?: boolean;
|
||||
};
|
||||
graphCore: GraphCore;
|
||||
}>;
|
||||
@ -124,6 +134,10 @@ export interface Hooks {
|
||||
transient: Canvas;
|
||||
};
|
||||
}>;
|
||||
mapperchange: IHook<{
|
||||
type: ITEM_TYPE;
|
||||
mapper: NodeMapper | EdgeMapper | ComboMapper;
|
||||
}>;
|
||||
treecollapseexpand: IHook<{
|
||||
ids: ID[];
|
||||
action: 'collapse' | 'expand';
|
||||
|
@ -29,6 +29,12 @@ import { RendererName } from './render';
|
||||
import { StackCfg } from './history';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
export type NodeMapper = ((data: NodeModel) => NodeDisplayModel) | NodeEncode;
|
||||
export type EdgeMapper = ((data: EdgeModel) => EdgeDisplayModel) | EdgeEncode;
|
||||
export type ComboMapper =
|
||||
| ((data: ComboModel) => ComboDisplayModel)
|
||||
| ComboEncode;
|
||||
|
||||
export interface Specification<
|
||||
B extends BehaviorRegistry,
|
||||
T extends ThemeRegistry,
|
||||
@ -92,9 +98,9 @@ export interface Specification<
|
||||
| TransformerFn[];
|
||||
|
||||
/** item */
|
||||
node?: ((data: NodeModel) => NodeDisplayModel) | NodeEncode;
|
||||
edge?: ((data: EdgeModel) => EdgeDisplayModel) | EdgeEncode;
|
||||
combo?: ((data: ComboModel) => ComboDisplayModel) | ComboEncode;
|
||||
node?: NodeMapper;
|
||||
edge?: EdgeMapper;
|
||||
combo?: ComboMapper;
|
||||
|
||||
/** item state styles */
|
||||
nodeState?: {
|
||||
|
@ -1,7 +1,12 @@
|
||||
export interface FitViewRules {
|
||||
onlyOutOfViewPort?: boolean; // Whehter fit it only when the graph is out of the view.
|
||||
direction?: 'x' | 'y' | 'both'; // Axis to fit.
|
||||
ratioRule?: 'max' | 'min'; // Ratio rule to fit.
|
||||
/** Whehter fit it only when the graph is out of the view. */
|
||||
onlyOutOfViewPort?: boolean;
|
||||
/** Axis to fit. */
|
||||
direction?: 'x' | 'y' | 'both';
|
||||
/** Ratio rule to fit. */
|
||||
ratioRule?: 'max' | 'min';
|
||||
/** Bounds type. 'render' means calculate the bounds by get rendering bounds. 'layout' for the situation that layout algorithm is done but the positions are not animated rendered. */
|
||||
boundsType?: 'render' | 'layout';
|
||||
}
|
||||
|
||||
export type GraphAlignment =
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
NodeDataUpdated,
|
||||
NodeRemoved,
|
||||
TreeStructureChanged,
|
||||
TreeStructureAttached,
|
||||
TreeStructureDetached,
|
||||
} from '@antv/graphlib';
|
||||
import { IG6GraphEvent, IGraph, NodeModelData } from '../types';
|
||||
import { GraphCore } from '../types/data';
|
||||
@ -84,6 +86,8 @@ export type GroupedChanges = {
|
||||
EdgeDataUpdated: EdgeDataUpdated<EdgeModelData>[];
|
||||
TreeStructureChanged: TreeStructureChanged[];
|
||||
ComboStructureChanged: TreeStructureChanged[];
|
||||
TreeStructureAttached: TreeStructureAttached[];
|
||||
TreeStructureDetached: TreeStructureDetached[];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -106,6 +110,8 @@ export const getGroupedChanges = (
|
||||
EdgeDataUpdated: [],
|
||||
TreeStructureChanged: [],
|
||||
ComboStructureChanged: [],
|
||||
TreeStructureAttached: [],
|
||||
TreeStructureDetached: [],
|
||||
};
|
||||
changes.forEach((change) => {
|
||||
const { type: changeType } = change;
|
||||
@ -126,7 +132,14 @@ export const getGroupedChanges = (
|
||||
else if (change.treeKey === 'tree')
|
||||
groupedChanges.TreeStructureChanged.push(change);
|
||||
return;
|
||||
} else if (['NodeRemoved', 'EdgeRemoved'].includes(changeType)) {
|
||||
} else if (
|
||||
[
|
||||
'NodeRemoved',
|
||||
'EdgeRemoved',
|
||||
'TreeStructureAttached',
|
||||
'TreeStructureDetached',
|
||||
].includes(changeType)
|
||||
) {
|
||||
groupedChanges[changeType].push(change);
|
||||
} else {
|
||||
const { id: oid } = change.value;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { Group } from '@antv/g';
|
||||
import { uniqueId } from '@antv/util';
|
||||
import { IGraph } from '../types';
|
||||
import Combo from '../item/combo';
|
||||
import Edge from '../item/edge';
|
||||
@ -34,43 +35,62 @@ export const upsertTransientItem = (
|
||||
transientItemMap: Map<ID, Node | Edge | Combo | Group>,
|
||||
itemMap: Map<ID, Node | Edge | Combo>,
|
||||
graphCore?: GraphCore,
|
||||
onlyDrawKeyShape?: boolean,
|
||||
drawOptions: {
|
||||
shapeIds?: string[];
|
||||
/** For transient edge */
|
||||
drawSource?: boolean;
|
||||
/** For transient edge */
|
||||
drawTarget?: boolean;
|
||||
visible?: boolean;
|
||||
} = {},
|
||||
upsertAncestors = true,
|
||||
) => {
|
||||
let transientItem = transientItemMap.get(item.model.id);
|
||||
if (transientItem) {
|
||||
return transientItem;
|
||||
}
|
||||
const {
|
||||
shapeIds,
|
||||
drawSource = true,
|
||||
drawTarget = true,
|
||||
visible = true,
|
||||
} = drawOptions;
|
||||
if (item.type === 'node') {
|
||||
transientItem = item.clone(nodeGroup, onlyDrawKeyShape, true) as Node;
|
||||
transientItem = item.clone(nodeGroup, shapeIds, true);
|
||||
} else if (item.type === 'edge') {
|
||||
const source = upsertTransientItem(
|
||||
item.sourceItem,
|
||||
nodeGroup,
|
||||
edgeGroup,
|
||||
comboGroup,
|
||||
transientItemMap,
|
||||
itemMap,
|
||||
graphCore,
|
||||
onlyDrawKeyShape,
|
||||
false,
|
||||
) as Node | Combo;
|
||||
const target = upsertTransientItem(
|
||||
item.targetItem,
|
||||
nodeGroup,
|
||||
edgeGroup,
|
||||
comboGroup,
|
||||
transientItemMap,
|
||||
itemMap,
|
||||
graphCore,
|
||||
onlyDrawKeyShape,
|
||||
false,
|
||||
) as Node | Combo;
|
||||
let source;
|
||||
let target;
|
||||
if (drawSource) {
|
||||
source = upsertTransientItem(
|
||||
item.sourceItem,
|
||||
nodeGroup,
|
||||
edgeGroup,
|
||||
comboGroup,
|
||||
transientItemMap,
|
||||
itemMap,
|
||||
graphCore,
|
||||
drawOptions,
|
||||
false,
|
||||
) as Node | Combo;
|
||||
}
|
||||
if (drawTarget) {
|
||||
target = upsertTransientItem(
|
||||
item.targetItem,
|
||||
nodeGroup,
|
||||
edgeGroup,
|
||||
comboGroup,
|
||||
transientItemMap,
|
||||
itemMap,
|
||||
graphCore,
|
||||
drawOptions,
|
||||
false,
|
||||
) as Node | Combo;
|
||||
}
|
||||
transientItem = item.clone(
|
||||
edgeGroup,
|
||||
source,
|
||||
target,
|
||||
undefined,
|
||||
shapeIds,
|
||||
true,
|
||||
) as Edge;
|
||||
} else {
|
||||
@ -89,7 +109,7 @@ export const upsertTransientItem = (
|
||||
transientItemMap,
|
||||
itemMap,
|
||||
graphCore,
|
||||
onlyDrawKeyShape,
|
||||
drawOptions,
|
||||
false,
|
||||
),
|
||||
);
|
||||
@ -97,7 +117,7 @@ export const upsertTransientItem = (
|
||||
});
|
||||
transientItem = (item as Combo).clone(
|
||||
comboGroup,
|
||||
onlyDrawKeyShape,
|
||||
shapeIds,
|
||||
true,
|
||||
() => getCombinedBoundsByItem(childItems), // getCombinedBounds
|
||||
() => childItems,
|
||||
@ -105,6 +125,7 @@ export const upsertTransientItem = (
|
||||
transientItem.toBack();
|
||||
}
|
||||
transientItemMap.set(item.model.id, transientItem);
|
||||
// @ts-ignore
|
||||
transientItem.transient = true;
|
||||
|
||||
if (
|
||||
@ -125,12 +146,28 @@ export const upsertTransientItem = (
|
||||
transientItemMap,
|
||||
itemMap,
|
||||
graphCore,
|
||||
onlyDrawKeyShape,
|
||||
drawOptions,
|
||||
);
|
||||
transientAncestor.toBack();
|
||||
}
|
||||
currentAncestor = graphCore.getParent(currentAncestor.id, 'combo');
|
||||
}
|
||||
}
|
||||
|
||||
if (shapeIds?.length && visible) {
|
||||
// @ts-ignore
|
||||
(transientItem as Group).childNodes.forEach((shape) => shape.show());
|
||||
}
|
||||
return transientItem;
|
||||
};
|
||||
|
||||
/**
|
||||
* generate unique edge id
|
||||
* @todo consider edges with same source and target
|
||||
* @param source
|
||||
* @param target
|
||||
* @returns
|
||||
*/
|
||||
export function generateEdgeID(source: ID, target: ID) {
|
||||
return [source, uniqueId(), target].join('->');
|
||||
}
|
||||
|
@ -164,3 +164,48 @@ export const radialLayout = (
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the layout (nodes' positions) bounds of a graph.
|
||||
* @param {Graph} graph - The graph object.
|
||||
* @returns {Object} - The layout bounds object containing the minimum, maximum, center, and halfExtents values.
|
||||
*/
|
||||
export const getLayoutBounds = (graph) => {
|
||||
const min = [Infinity, Infinity];
|
||||
const max = [-Infinity, -Infinity];
|
||||
const borderIds = { min: [], max: [] };
|
||||
graph.getAllNodesData().forEach((model) => {
|
||||
const { x, y } = model.data;
|
||||
if (isNaN(x) || isNaN(y)) return;
|
||||
if (min[0] > x) {
|
||||
min[0] = x;
|
||||
borderIds.min[0] = model.id;
|
||||
}
|
||||
if (min[1] > y) {
|
||||
min[1] = y;
|
||||
borderIds.min[1] = model.id;
|
||||
}
|
||||
if (max[0] < x) {
|
||||
max[0] = x;
|
||||
borderIds.max[0] = model.id;
|
||||
}
|
||||
if (max[1] < y) {
|
||||
max[1] = y;
|
||||
borderIds.max[1] = model.id;
|
||||
}
|
||||
});
|
||||
borderIds.min.forEach((id, i) => {
|
||||
const { halfExtents: nodeHalfSize } = graph.getRenderBBox(id);
|
||||
min[i] -= nodeHalfSize[i];
|
||||
});
|
||||
borderIds.max.forEach((id, i) => {
|
||||
const { halfExtents: nodeHalfSize } = graph.getRenderBBox(id);
|
||||
max[i] += nodeHalfSize[i];
|
||||
});
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
center: min.map((val, i) => (max[i] + val) / 2),
|
||||
halfExtents: min.map((val, i) => (max[i] - val) / 2),
|
||||
};
|
||||
};
|
||||
|
@ -52,7 +52,10 @@ export const distanceVec = (p1: vec2, p2: vec2): number => {
|
||||
export const distance = (p1: Point, p2: Point): number => {
|
||||
const vx = p1.x - p2.x;
|
||||
const vy = p1.y - p2.y;
|
||||
return Math.sqrt(vx * vx + vy * vy);
|
||||
const vz = p1.z - p2.z;
|
||||
return isNaN(vz)
|
||||
? Math.sqrt(vx * vx + vy * vy)
|
||||
: Math.sqrt(vx * vx + vy * vy + vz * vz);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -61,8 +64,10 @@ export const distance = (p1: Point, p2: Point): number => {
|
||||
* @param p2
|
||||
* @returns
|
||||
*/
|
||||
export const isSamePoint = (p1: Point, p2: Point): boolean =>
|
||||
p1.x === p2.x && p1.y === p2.y && p1.z === p2.z;
|
||||
export const isSamePoint = (p1: Point, p2: Point): boolean => {
|
||||
if (!p1 || !p2) return false;
|
||||
return p1.x === p2.x && p1.y === p2.y && p1.z === p2.z;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get point and circle intersect point.
|
||||
|
@ -269,7 +269,7 @@ export const formatPadding = (
|
||||
* @returns
|
||||
*/
|
||||
export const mergeStyles = (styleMaps: ItemShapeStyles[]) => {
|
||||
let currentResult = styleMaps[0];
|
||||
let currentResult = clone(styleMaps[0]);
|
||||
styleMaps.forEach((styleMap, i) => {
|
||||
if (i > 0) currentResult = merge2Styles(currentResult, styleMap);
|
||||
});
|
||||
@ -288,15 +288,14 @@ const merge2Styles = (
|
||||
) => {
|
||||
if (!styleMap1) return { ...styleMap2 };
|
||||
else if (!styleMap2) return { ...styleMap1 };
|
||||
const mergedStyle = clone(styleMap1);
|
||||
const mergedStyle = styleMap1;
|
||||
Object.keys(styleMap2).forEach((shapeId) => {
|
||||
const style = styleMap2[shapeId];
|
||||
mergedStyle[shapeId] = mergedStyle[shapeId] || {};
|
||||
if (!style) return;
|
||||
Object.keys(style).forEach((styleName) => {
|
||||
const value = style[styleName];
|
||||
mergedStyle[shapeId][styleName] = value;
|
||||
});
|
||||
mergedStyle[shapeId] = {
|
||||
...mergedStyle[shapeId],
|
||||
...style,
|
||||
};
|
||||
});
|
||||
return mergedStyle;
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
|
||||
const GeometryTagMap = {
|
||||
sphere: SphereGeometry,
|
||||
cubic: CubeGeometry,
|
||||
cube: CubeGeometry,
|
||||
plane: PlaneGeometry,
|
||||
};
|
||||
|
||||
|
20
packages/g6/src/util/warn.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* optionName: 选项名
|
||||
* shouldBe: 合法的输入参数
|
||||
* now: 当前用户输入值
|
||||
* scope: 选项所属的模块
|
||||
*/
|
||||
type WarnOption = {
|
||||
optionName: string;
|
||||
shouldBe: any;
|
||||
now: string | number;
|
||||
scope: string;
|
||||
};
|
||||
|
||||
export function warn({ optionName, shouldBe, now, scope }: WarnOption) {
|
||||
console.warn(
|
||||
`G6 [${scope}]: Invalid option, ${optionName} must be one of ${shouldBe.join(
|
||||
', ',
|
||||
)}, but got ${now}`,
|
||||
);
|
||||
}
|
@ -26,7 +26,7 @@ export default (context: TestCaseContext) => {
|
||||
edges: [{ id: 'edge1', source: 'node1', target: 'node2', data: {} }],
|
||||
},
|
||||
modes: {
|
||||
default: ['brush-select'],
|
||||
default: ['brush-select', 'drag-node'],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
81
packages/g6/tests/demo/behaviors/create-edge.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { extend, Graph, Extensions } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext, options) => {
|
||||
const createEdgeOptions = options || {
|
||||
trigger: 'click',
|
||||
edgeConfig: { keyShape: { stroke: '#f00' } },
|
||||
createVirtualEventName: 'begincreate',
|
||||
createActualEventName: 'aftercreate',
|
||||
cancelCreateEventName: 'cancelcreate',
|
||||
};
|
||||
const ExtGraph = extend(Graph, {
|
||||
behaviors: {
|
||||
'create-edge': Extensions.CreateEdge,
|
||||
'brush-select': Extensions.BrushSelect,
|
||||
},
|
||||
});
|
||||
const graph = new ExtGraph({
|
||||
...context,
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: {} },
|
||||
{ id: 'node2', data: {} },
|
||||
{ id: 'node3', data: {} },
|
||||
{ id: 'node4', data: {} },
|
||||
{ id: 'node5', data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'node1', target: 'node2', data: {} },
|
||||
{ id: 'edge2', source: 'node1', target: 'node3', data: {} },
|
||||
{ id: 'edge3', source: 'node1', target: 'node4', data: {} },
|
||||
{ id: 'edge4', source: 'node2', target: 'node3', data: {} },
|
||||
{ id: 'edge5', source: 'node3', target: 'node4', data: {} },
|
||||
{ id: 'edge6', source: 'node4', target: 'node5', data: {} },
|
||||
],
|
||||
},
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'brush-select',
|
||||
eventName: 'afterbrush',
|
||||
},
|
||||
{
|
||||
type: 'create-edge',
|
||||
...createEdgeOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
graph.on('begincreate', (e) => {
|
||||
graph.setCursor('crosshair');
|
||||
});
|
||||
graph.on('cancelcreate', (e) => {
|
||||
graph.setCursor('default');
|
||||
});
|
||||
graph.on('aftercreate', (e) => {
|
||||
const { edge } = e;
|
||||
debugger;
|
||||
graph.updateData('edge', {
|
||||
id: edge.id,
|
||||
data: {
|
||||
keyShape: {
|
||||
stroke: '#0f0',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return graph;
|
||||
};
|
@ -1,8 +1,13 @@
|
||||
import G6 from '../../../src/index';
|
||||
import { extend, Extensions, Graph } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
return new G6.Graph({
|
||||
const ExtGraph = extend(Graph, {
|
||||
behaviors: {
|
||||
'scroll-canvas': Extensions.ScrollCanvas,
|
||||
},
|
||||
});
|
||||
return new ExtGraph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
|
@ -132,5 +132,14 @@ export default (
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.on('canvas:click', (e) => {
|
||||
// graph.removeData('combo', 'combo1');
|
||||
graph.updateData('node', {
|
||||
id: 'node5',
|
||||
data: {
|
||||
parentId: 'combo3',
|
||||
},
|
||||
});
|
||||
});
|
||||
return graph;
|
||||
};
|
||||
|
1422
packages/g6/tests/demo/data/graphCore.ts
Normal file
@ -9,6 +9,7 @@ import behaviors_scrollCanvas from './behaviors/scroll-canvas';
|
||||
import behaviors_brush_select from './behaviors/brush-select';
|
||||
import behaviors_click_select from './behaviors/click-select';
|
||||
import behaviors_collapse_expand_tree from './behaviors/collapse-expand-tree';
|
||||
import behaviors_create_edge from './behaviors/create-edge';
|
||||
import circularUpdate from './layouts/circular-update';
|
||||
import comboBasic from './combo/combo-basic';
|
||||
import comboRect from './combo/combo-rect';
|
||||
@ -61,11 +62,20 @@ import layouts_combocombined from './layouts/combo-combined';
|
||||
import hull from './plugins/hull';
|
||||
import legend from './plugins/legend';
|
||||
import snapline from './plugins/snapline';
|
||||
import mapper from './visual/mapper';
|
||||
import minimap from './plugins/minimap';
|
||||
import edgeFilterLens from './plugins/edgeFilterLens';
|
||||
import watermarker from './plugins/watermarker';
|
||||
import cube from './item/node/cube';
|
||||
import graphCore from './data/graphCore';
|
||||
import dagreUpdate from './layouts/dagre-update';
|
||||
|
||||
export { default as timebar_time } from './plugins/timebar-time';
|
||||
export { default as timebar_chart } from './plugins/timebar-chart';
|
||||
|
||||
export {
|
||||
minimap,
|
||||
mapper,
|
||||
anchor,
|
||||
animations_node_build_in,
|
||||
arrow,
|
||||
@ -76,6 +86,7 @@ export {
|
||||
behaviors_brush_select,
|
||||
behaviors_click_select,
|
||||
behaviors_collapse_expand_tree,
|
||||
behaviors_create_edge,
|
||||
circularUpdate,
|
||||
comboBasic,
|
||||
comboRect,
|
||||
@ -128,4 +139,9 @@ export {
|
||||
hull,
|
||||
legend,
|
||||
snapline,
|
||||
edgeFilterLens,
|
||||
watermarker,
|
||||
cube,
|
||||
graphCore,
|
||||
dagreUpdate,
|
||||
};
|
||||
|
@ -24,7 +24,12 @@ const defaultData = {
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: {},
|
||||
data: {
|
||||
keyShape: {
|
||||
stroke: '#f00',
|
||||
lineDash: [2, 2],
|
||||
},
|
||||
},
|
||||
edgeState: {
|
||||
selected: {
|
||||
keyShape: {
|
||||
@ -225,7 +230,7 @@ export default (context: TestCaseContext) => {
|
||||
},
|
||||
});
|
||||
// 2.create graph
|
||||
graph = new ExtGraph({
|
||||
graph = new Graph({
|
||||
...context,
|
||||
data: defaultData,
|
||||
modes: {
|
||||
|
76
packages/g6/tests/demo/item/node/cube.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Graph, Extensions, extend } from '../../../../src/index';
|
||||
import { data } from '../../../datasets/dataset1';
|
||||
import { TestCaseContext } from '../../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const ExtGraph = extend(Graph, {
|
||||
nodes: {
|
||||
'cube-node': Extensions.CubeNode,
|
||||
},
|
||||
});
|
||||
const graph = new ExtGraph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
renderer: 'webgl-3d',
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } },
|
||||
{ id: 'node2', data: { x: 200, y: 250, nodeType: 'b' } },
|
||||
{ id: 'node3', data: { x: 200, y: 350, nodeType: 'b' } },
|
||||
{ id: 'node4', data: { x: 300, y: 250, nodeType: 'c' } },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: { edgeType: 'e1' },
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
data: { edgeType: 'e2' },
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'node3',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
{
|
||||
id: 'edge4',
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
],
|
||||
},
|
||||
node: {
|
||||
type: 'cube-node',
|
||||
keyShape: {
|
||||
opacity: 0.7,
|
||||
r: 10,
|
||||
width: 100,
|
||||
height: 200,
|
||||
depth: 200,
|
||||
},
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
fontSize: 4,
|
||||
wordWrapWidth: 200,
|
||||
isSizeAttenuation: true,
|
||||
},
|
||||
// iconShape: {
|
||||
// img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
// z: 100
|
||||
// },
|
||||
// labelBackgroundShape: [],
|
||||
|
||||
},
|
||||
});
|
||||
return graph;
|
||||
};
|
@ -4,7 +4,7 @@ import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const { width, height } = context;
|
||||
return new G6.Graph({
|
||||
const graph = new G6.Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
@ -13,5 +13,9 @@ export default (context: TestCaseContext) => {
|
||||
center: [width! / 2, height! / 2],
|
||||
radius: 200,
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-canvas', 'zoom-canvas'],
|
||||
},
|
||||
});
|
||||
return graph;
|
||||
};
|
||||
|
149
packages/g6/tests/demo/layouts/dagre-update.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { extend, Graph, Extensions } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const ExtGraph = extend(Graph, {
|
||||
layouts: {
|
||||
dagre: Extensions.DagreLayout,
|
||||
},
|
||||
edges: {
|
||||
'cubic-edge': Extensions.CubicEdge,
|
||||
'quadratic-edge': Extensions.QuadraticEdge,
|
||||
'polyline-edge': Extensions.PolylineEdge,
|
||||
},
|
||||
});
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
data: {
|
||||
label: '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
data: {
|
||||
label: '1',
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-395',
|
||||
source: '0',
|
||||
target: '1',
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const layoutConfigs = {
|
||||
Default: {
|
||||
type: 'dagre',
|
||||
nodesep: 100,
|
||||
ranksep: 70,
|
||||
controlPoints: true,
|
||||
},
|
||||
// TODO: 换个数据
|
||||
LR: {
|
||||
type: 'dagre',
|
||||
rankdir: 'LR',
|
||||
align: 'DL',
|
||||
nodesep: 50,
|
||||
ranksep: 70,
|
||||
controlPoints: true,
|
||||
},
|
||||
'LR&UL': {
|
||||
type: 'dagre',
|
||||
rankdir: 'LR',
|
||||
align: 'UL',
|
||||
controlPoints: true,
|
||||
nodesep: 50,
|
||||
ranksep: 70,
|
||||
},
|
||||
};
|
||||
|
||||
const container = context.container;
|
||||
const graph = new ExtGraph({
|
||||
...context,
|
||||
layout: layoutConfigs.Default,
|
||||
node: (node) => {
|
||||
return {
|
||||
id: node.id,
|
||||
data: {
|
||||
...node.data,
|
||||
type: 'rect-node',
|
||||
lodStrategy: {},
|
||||
keyShape: {
|
||||
width: 60,
|
||||
height: 30,
|
||||
radius: 8,
|
||||
},
|
||||
labelShape: {
|
||||
position: 'center',
|
||||
maxWidth: '80%',
|
||||
text: `node-${node.data.label}`,
|
||||
},
|
||||
// animates: {
|
||||
// update: [
|
||||
// {
|
||||
// fields: ['x', 'y'],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
},
|
||||
};
|
||||
},
|
||||
edge: {
|
||||
type: 'polyline-edge',
|
||||
keyShape: {
|
||||
// radius: 20,
|
||||
// offset: 45,
|
||||
// endArrow: true,
|
||||
// lineWidth: 2,
|
||||
// stroke: '#C2C8D5',
|
||||
// routeCfg: {
|
||||
// obstacleAvoidance: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node', 'drag-canvas', 'zoom-canvas', 'click-select'],
|
||||
},
|
||||
autoFit: 'view',
|
||||
data,
|
||||
});
|
||||
|
||||
graph.on('edge:click', (e) => {
|
||||
console.log('clickedge', e.itemId, graph.canvas, graph);
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined')
|
||||
window.onresize = () => {
|
||||
if (!graph || graph.destroyed) return;
|
||||
if (!container || !container.scrollWidth || !container.scrollHeight)
|
||||
return;
|
||||
graph.setSize([container.scrollWidth, container.scrollHeight]);
|
||||
};
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.style.position = 'absolute';
|
||||
container.appendChild(btnContainer);
|
||||
const tip = document.createElement('span');
|
||||
tip.innerHTML = '👉 Change configs:';
|
||||
btnContainer.appendChild(tip);
|
||||
|
||||
Object.keys(layoutConfigs).forEach((name, i) => {
|
||||
const btn = document.createElement('a');
|
||||
btn.innerHTML = name;
|
||||
btn.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
|
||||
btn.style.padding = '4px';
|
||||
btn.style.marginLeft = i > 0 ? '24px' : '8px';
|
||||
btnContainer.appendChild(btn);
|
||||
btn.addEventListener('click', () => {
|
||||
graph.layout(layoutConfigs[name]);
|
||||
});
|
||||
});
|
||||
return graph;
|
||||
};
|
@ -512,10 +512,8 @@ const data = {
|
||||
],
|
||||
};
|
||||
export default (context: TestCaseContext) => {
|
||||
const { width, height } = context;
|
||||
const graph = new G6.Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
data: JSON.parse(JSON.stringify(data)),
|
||||
node: {
|
||||
lodStrategy: {},
|
||||
|
@ -1759,11 +1759,6 @@ const 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;
|
||||
});
|
||||
|
||||
@ -2116,7 +2111,7 @@ const createGraph = async () => {
|
||||
],
|
||||
show: [
|
||||
{
|
||||
fields: ['size'],
|
||||
fields: ['transform'],
|
||||
duration: 200,
|
||||
},
|
||||
{
|
||||
@ -2238,5 +2233,6 @@ export default async () => {
|
||||
graph.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
195
packages/g6/tests/demo/plugins/edgeFilterLens.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { Graph, Extensions, extend } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
import data from '../../datasets/force-data.json';
|
||||
import { clone } from '@antv/util';
|
||||
|
||||
export default (context: TestCaseContext, options = {}) => {
|
||||
const trigger = 'mousemove';
|
||||
let filterLens = {
|
||||
type: 'filterLens',
|
||||
key: 'filterLens1',
|
||||
trigger,
|
||||
showLabel: 'edge',
|
||||
r: 140,
|
||||
...options,
|
||||
};
|
||||
|
||||
// ================= The DOMs for configurations =============== //
|
||||
const graphDiv = document.getElementById('container');
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.display = 'inline-block';
|
||||
buttonContainer.style.height = '35px';
|
||||
buttonContainer.style.width = '100%';
|
||||
buttonContainer.style.textAlign = 'center';
|
||||
|
||||
// tip
|
||||
const tip = document.createElement('span');
|
||||
tip.innerHTML =
|
||||
'点击画布任意位置开始探索。过滤镜中显示两端节点均在过滤镜中的边。';
|
||||
buttonContainer.appendChild(tip);
|
||||
|
||||
buttonContainer.appendChild(document.createElement('br'));
|
||||
|
||||
// tip english
|
||||
const tipEn = document.createElement('span');
|
||||
tipEn.innerHTML =
|
||||
'Click the canvas to begin. Show the edge whose both end nodes are inside the lens.';
|
||||
buttonContainer.appendChild(tipEn);
|
||||
|
||||
buttonContainer.appendChild(document.createElement('br'));
|
||||
|
||||
// enable/disable the fisheye lens button
|
||||
const swithButton = document.createElement('input');
|
||||
swithButton.type = 'button';
|
||||
swithButton.value = 'Disable';
|
||||
swithButton.style.height = '25px';
|
||||
swithButton.style.width = '60px';
|
||||
swithButton.style.marginLeft = '16px';
|
||||
buttonContainer.appendChild(swithButton);
|
||||
|
||||
// list for changing trigger
|
||||
const triggerTag = document.createElement('span');
|
||||
triggerTag.innerHTML = 'Trigger:';
|
||||
triggerTag.style.marginLeft = '16px';
|
||||
buttonContainer.appendChild(triggerTag);
|
||||
const configTrigger = document.createElement('select');
|
||||
configTrigger.value = 'mousemove';
|
||||
configTrigger.style.height = '25px';
|
||||
configTrigger.style.width = '100px';
|
||||
configTrigger.style.marginLeft = '8px';
|
||||
const mousemoveTrigger = document.createElement('option');
|
||||
mousemoveTrigger.value = 'mousemove';
|
||||
mousemoveTrigger.innerHTML = 'mousemove';
|
||||
configTrigger.appendChild(mousemoveTrigger);
|
||||
const dragTrigger = document.createElement('option');
|
||||
dragTrigger.value = 'drag';
|
||||
dragTrigger.innerHTML = 'drag';
|
||||
configTrigger.appendChild(dragTrigger);
|
||||
const clickTrigger = document.createElement('option');
|
||||
clickTrigger.value = 'click';
|
||||
clickTrigger.innerHTML = 'click';
|
||||
configTrigger.appendChild(clickTrigger);
|
||||
buttonContainer.appendChild(configTrigger);
|
||||
|
||||
// list for changing scaleRBy
|
||||
const scaleR = document.createElement('span');
|
||||
scaleR.innerHTML = 'Scale r by:';
|
||||
scaleR.style.marginLeft = '16px';
|
||||
buttonContainer.appendChild(scaleR);
|
||||
const configScaleRBy = document.createElement('select');
|
||||
configScaleRBy.value = 'wheel';
|
||||
configScaleRBy.style.height = '25px';
|
||||
configScaleRBy.style.width = '100px';
|
||||
configScaleRBy.style.marginLeft = '8px';
|
||||
const scaleRByWheel = document.createElement('option');
|
||||
scaleRByWheel.value = 'wheel';
|
||||
scaleRByWheel.innerHTML = 'wheel';
|
||||
configScaleRBy.appendChild(scaleRByWheel);
|
||||
const scaleRByUnset = document.createElement('option');
|
||||
scaleRByUnset.value = 'unset';
|
||||
scaleRByUnset.innerHTML = 'unset';
|
||||
configScaleRBy.appendChild(scaleRByUnset);
|
||||
buttonContainer.appendChild(configScaleRBy);
|
||||
|
||||
graphDiv.parentNode.appendChild(buttonContainer);
|
||||
|
||||
// ========================================================= //
|
||||
|
||||
const ExtGraph = extend(Graph, {
|
||||
plugins: {
|
||||
filterLens: Extensions.EdgeFilterLens,
|
||||
},
|
||||
});
|
||||
const graph = new ExtGraph({
|
||||
...context,
|
||||
layout: {
|
||||
type: 'grid',
|
||||
begin: [0, 0],
|
||||
},
|
||||
plugins: [filterLens],
|
||||
node: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ zoomRange: [0, 0.9] }, // -1
|
||||
{ zoomRange: [0.9, 1], primary: true }, // 0
|
||||
{ zoomRange: [1, 1.2] }, // 1
|
||||
{ zoomRange: [1.2, 1.5] }, // 2
|
||||
{ zoomRange: [1.5, Infinity] }, // 3
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 500,
|
||||
},
|
||||
},
|
||||
labelShape: {
|
||||
text: innerModel.data.label,
|
||||
lod: 1, // 图的缩放大于 levels 第一层定义的 zoomRange[0] 时展示,小于时隐藏
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
edge: (innerModel) => {
|
||||
return {
|
||||
...innerModel,
|
||||
data: {
|
||||
...innerModel.data,
|
||||
lodStrategy: {
|
||||
levels: [
|
||||
{ zoomRange: [0, 0.9] }, // -1
|
||||
{ zoomRange: [0.9, 1], primary: true }, // 0
|
||||
{ zoomRange: [1, 1.2] }, // 1
|
||||
{ zoomRange: [1.2, 1.5] }, // 2
|
||||
{ zoomRange: [1.5, Infinity] }, // 3
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 500,
|
||||
},
|
||||
},
|
||||
labelShape: {
|
||||
text: innerModel.data.label,
|
||||
maxWidth: '100%',
|
||||
lod: 1, // 图的缩放大于 levels 第一层定义的 zoomRange[0] 时展示,小于时隐藏
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
modes: {
|
||||
// default: ['drag-canvas',],
|
||||
},
|
||||
});
|
||||
|
||||
swithButton.addEventListener('click', (e) => {
|
||||
if (swithButton.value === 'Disable') {
|
||||
swithButton.value = 'Enable';
|
||||
graph.removePlugins(['filterLens1']);
|
||||
} else {
|
||||
swithButton.value = 'Disable';
|
||||
graph.addPlugins([filterLens]);
|
||||
}
|
||||
});
|
||||
configScaleRBy.addEventListener('change', (e) => {
|
||||
filterLens = {
|
||||
...filterLens,
|
||||
scaleRBy: e.target.value,
|
||||
};
|
||||
graph.updatePlugin(filterLens);
|
||||
});
|
||||
configTrigger.addEventListener('change', (e) => {
|
||||
filterLens = {
|
||||
...filterLens,
|
||||
trigger: e.target.value,
|
||||
};
|
||||
graph.updatePlugin(filterLens);
|
||||
});
|
||||
|
||||
const cloneData = clone(data);
|
||||
cloneData.edges.forEach((edge) => (edge.data = { label: edge.id }));
|
||||
graph.read(cloneData);
|
||||
graph.zoom(0.6);
|
||||
|
||||
return graph;
|
||||
};
|
58
packages/g6/tests/demo/plugins/minimap.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Graph, Extensions, extend } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const ExtGraph = extend(Graph, {
|
||||
plugins: {
|
||||
minimap: Extensions.Minimap,
|
||||
},
|
||||
});
|
||||
|
||||
return new ExtGraph({
|
||||
...context,
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } },
|
||||
{ id: 'node2', data: { x: 200, y: 250, nodeType: 'b' } },
|
||||
{ id: 'node3', data: { x: 200, y: 350, nodeType: 'b' } },
|
||||
{ id: 'node4', data: { x: 300, y: 250, nodeType: 'c' } },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: { edgeType: 'e1' },
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
data: { edgeType: 'e2' },
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'node3',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
{
|
||||
id: 'edge4',
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['minimap'],
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-node',
|
||||
},
|
||||
'zoom-canvas',
|
||||
'drag-canvas',
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
62
packages/g6/tests/demo/plugins/watermarker.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Graph, Extensions, extend } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
export default (context: TestCaseContext) => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{ id: 'node1', data: { x: 100, y: 200, nodeType: 'a' } },
|
||||
{ id: 'node2', data: { x: 200, y: 250, nodeType: 'b' } },
|
||||
{ id: 'node3', data: { x: 200, y: 350, nodeType: 'b' } },
|
||||
{ id: 'node4', data: { x: 300, y: 250, nodeType: 'c' } },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: { edgeType: 'e1' },
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
data: { edgeType: 'e2' },
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'node3',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
{
|
||||
id: 'edge4',
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ExtGraph = extend(Graph, {
|
||||
plugins: {
|
||||
watermarker: Extensions.WaterMarker,
|
||||
},
|
||||
});
|
||||
const graph = new Graph({
|
||||
...context,
|
||||
type: 'graph',
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: ['watermarker'],
|
||||
data,
|
||||
});
|
||||
return graph;
|
||||
};
|
@ -32,6 +32,9 @@ export default (
|
||||
layout: {
|
||||
type: layoutType,
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node', 'collapse-expand-tree'],
|
||||
},
|
||||
node: (innerModel) => {
|
||||
const { x, y, labelShape } = innerModel.data;
|
||||
return {
|
||||
@ -190,5 +193,8 @@ export default (
|
||||
|
||||
graph.translateTo({ x: 100, y: 100 });
|
||||
|
||||
graph.on('edge:click', (e) => {
|
||||
console.log('clickedge', e.itemId);
|
||||
});
|
||||
return graph;
|
||||
};
|
||||
|
103
packages/g6/tests/demo/visual/mapper.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Graph } from '../../../src/index';
|
||||
import { TestCaseContext } from '../interface';
|
||||
|
||||
export default (context: TestCaseContext) => {
|
||||
const graph = new Graph({
|
||||
...context,
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: '1', data: {} },
|
||||
{ id: '2', data: {} },
|
||||
{ id: '3', data: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'edge1', source: '1', target: '2', data: {} },
|
||||
{ id: 'edge2', source: '1', target: '3', data: {} },
|
||||
{ id: 'edge4', source: '2', target: '3', data: {} },
|
||||
],
|
||||
},
|
||||
layout: {
|
||||
type: 'grid',
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = context;
|
||||
const jsonNodeMapperBtn = document.createElement('button');
|
||||
container.parentNode?.appendChild(jsonNodeMapperBtn);
|
||||
jsonNodeMapperBtn.innerHTML = '更改节点JSON映射';
|
||||
jsonNodeMapperBtn.id = 'change-node-json-mapper';
|
||||
jsonNodeMapperBtn.style.zIndex = 10;
|
||||
jsonNodeMapperBtn.addEventListener('click', (e) => {
|
||||
graph.updateMapper('node', {
|
||||
labelShape: {
|
||||
text: 'xxx',
|
||||
fontWeight: 800,
|
||||
fill: '#f00',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const funcNodeMapperBtn = document.createElement('button');
|
||||
container.parentNode?.appendChild(funcNodeMapperBtn);
|
||||
funcNodeMapperBtn.innerHTML = '更改节点函数映射';
|
||||
funcNodeMapperBtn.id = 'change-node-func-mapper';
|
||||
funcNodeMapperBtn.style.zIndex = 10;
|
||||
funcNodeMapperBtn.addEventListener('click', (e) => {
|
||||
graph.updateMapper('node', (model) => ({
|
||||
id: model.id,
|
||||
data: {
|
||||
labelShape: {
|
||||
text: `new-${model.id}`,
|
||||
fontWeight: 800,
|
||||
fill: '#0f0',
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const jsonEdgeMapperBtn = document.createElement('button');
|
||||
container.parentNode?.appendChild(jsonEdgeMapperBtn);
|
||||
jsonEdgeMapperBtn.innerHTML = '更改边JSON映射';
|
||||
jsonEdgeMapperBtn.id = 'change-edge-json-mapper';
|
||||
jsonEdgeMapperBtn.style.zIndex = 10;
|
||||
jsonEdgeMapperBtn.addEventListener('click', (e) => {
|
||||
graph.updateMapper('edge', {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
fontWeight: 800,
|
||||
fill: '#f00',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const funcEdgeMapperBtn = document.createElement('button');
|
||||
container.parentNode?.appendChild(funcEdgeMapperBtn);
|
||||
funcEdgeMapperBtn.innerHTML = '更改边函数映射';
|
||||
funcEdgeMapperBtn.id = 'change-edge-func-mapper';
|
||||
funcEdgeMapperBtn.style.zIndex = 10;
|
||||
funcEdgeMapperBtn.addEventListener('click', (e) => {
|
||||
graph.updateMapper('edge', (model) => ({
|
||||
id: model.id,
|
||||
data: {
|
||||
labelShape: {
|
||||
text: `new-${model.id}`,
|
||||
fontWeight: 800,
|
||||
fill: '#0f0',
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
return graph;
|
||||
};
|
72
packages/g6/tests/integration/api-update-mapper.spec.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import mapper from '../demo/visual/mapper';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('updateMapper API', () => {
|
||||
beforeEach(() => {
|
||||
/**
|
||||
* SVG Snapshot testing will generate a unique id for each element.
|
||||
* Reset to 0 to keep snapshot consistent.
|
||||
*/
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('node and edge mapper update', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = mapper({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await expect(canvas).toMatchCanvasSnapshot(dir, 'api-update-mapper-init');
|
||||
|
||||
const $updateNodeJson = document.getElementById(
|
||||
'change-node-json-mapper',
|
||||
);
|
||||
$updateNodeJson?.click();
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'api-update-mapper-node-json',
|
||||
);
|
||||
|
||||
const $updateNodeFunc = document.getElementById(
|
||||
'change-node-func-mapper',
|
||||
);
|
||||
$updateNodeFunc?.click();
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'api-update-mapper-node-func',
|
||||
);
|
||||
|
||||
const $updateEdgeJson = document.getElementById(
|
||||
'change-edge-json-mapper',
|
||||
);
|
||||
$updateEdgeJson?.click();
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'api-update-mapper-edge-json',
|
||||
);
|
||||
|
||||
const $updateEdgeFunc = document.getElementById(
|
||||
'change-edge-func-mapper',
|
||||
);
|
||||
$updateEdgeFunc?.click();
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'api-update-mapper-edge-func',
|
||||
);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
192
packages/g6/tests/integration/behaviors-create-edge.spec.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import createEdge from '../demo/behaviors/create-edge';
|
||||
import { createContext } from './utils';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Create edge behavior', () => {
|
||||
beforeEach(() => {
|
||||
/**
|
||||
* SVG Snapshot testing will generate a unique id for each element.
|
||||
* Reset to 0 to keep snapshot consistent.
|
||||
*/
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('trigger click should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas/behaviors`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = createEdge(
|
||||
{
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
},
|
||||
{
|
||||
trigger: 'click',
|
||||
edgeConfig: { keyShape: { stroke: '#f00' } },
|
||||
createVirtualEventName: 'begincreate',
|
||||
cancelCreateEventName: 'cancelcreate',
|
||||
},
|
||||
);
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
graph.emit('node:click', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
graph.emit('pointermove', { canvas: { x: 100, y: 100 } });
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-click-begin',
|
||||
);
|
||||
|
||||
graph.emit('node:click', {
|
||||
itemId: 'node2',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-click-finish',
|
||||
);
|
||||
|
||||
graph.emit('node:click', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
graph.emit('node:click', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-click-loop',
|
||||
);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('trigger drag should be rendered correctly with Canvas2D', (done) => {
|
||||
const dir = `${__dirname}/snapshots/canvas/behaviors`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
|
||||
const graph = createEdge(
|
||||
{
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
},
|
||||
{
|
||||
trigger: 'drag',
|
||||
edgeConfig: { keyShape: { stroke: '#f00' } },
|
||||
},
|
||||
);
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
graph.emit('node:dragstart', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
graph.emit('drag', { canvas: { x: 100, y: 100 } });
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-drag-begin',
|
||||
);
|
||||
|
||||
const nodeModel = graph.getNodeData('node2');
|
||||
graph.emit('drop', {
|
||||
itemId: 'node2',
|
||||
itemType: 'node',
|
||||
canvas: { x: nodeModel?.data.x, y: nodeModel?.data.y },
|
||||
});
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-drag-finish',
|
||||
);
|
||||
|
||||
graph.emit('node:dragstart', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
graph.emit('drag', { canvas: { x: 100, y: 100 } });
|
||||
const node5Model = graph.getNodeData('node5');
|
||||
graph.emit('drop', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: node5Model?.data.x, y: node5Model?.data.y },
|
||||
});
|
||||
await expect(canvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-drag-loop',
|
||||
);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be rendered correctly with SVG', (done) => {
|
||||
const dir = `${__dirname}/snapshots/svg/behaviors`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('svg', 500, 500);
|
||||
|
||||
const graph = createEdge(
|
||||
{
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
},
|
||||
{
|
||||
trigger: 'click',
|
||||
edgeConfig: { keyShape: { stroke: '#f00' } },
|
||||
createVirtualEventName: 'begincreate',
|
||||
cancelCreateEventName: 'cancelcreate',
|
||||
},
|
||||
);
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
graph.emit('node:click', {
|
||||
itemId: 'node5',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
graph.emit('pointermove', { canvas: { x: 100, y: 100 } });
|
||||
await expect(canvas).toMatchSVGSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-click-begin',
|
||||
);
|
||||
|
||||
graph.emit('node:click', {
|
||||
itemId: 'node2',
|
||||
itemType: 'node',
|
||||
canvas: { x: 100, y: 100 },
|
||||
});
|
||||
await expect(canvas).toMatchSVGSnapshot(
|
||||
dir,
|
||||
'behaviors-create-edge-click-finish',
|
||||
);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
37
packages/g6/tests/integration/items-node-cube.spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import cube from '../demo/item/node/cube'
|
||||
import './utils/useSnapshotMatchers';
|
||||
import { createContext, sleep } from './utils';
|
||||
|
||||
describe('Items node cube', () => {
|
||||
beforeEach(() => {
|
||||
/**
|
||||
* SVG Snapshot testing will generate a unique id for each element.
|
||||
* Reset to 0 to keep snapshot consistent.
|
||||
*/
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('should be rendered correctly with WebGL', (done) => {
|
||||
const dir = `${__dirname}/snapshots/webgl/items/node`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('webgl', 500, 500);
|
||||
|
||||
const graph = cube({
|
||||
container,
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
renderer: 'webgl-3d',
|
||||
});
|
||||
|
||||
graph.on('afterlayout', async () => {
|
||||
await sleep(300);
|
||||
await expect(canvas).toMatchWebGLSnapshot(dir, 'items-node-cube');
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
42
packages/g6/tests/integration/plugins-edgeFilterLens.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { resetEntityCounter } from '@antv/g';
|
||||
import EdgeFilterLens from '../demo/plugins/edgeFilterLens';
|
||||
import { createContext, sleep } from './utils';
|
||||
import { triggerEvent } from './utils/event';
|
||||
import './utils/useSnapshotMatchers';
|
||||
|
||||
describe('Default EdgeFilterLens', () => {
|
||||
beforeEach(() => {
|
||||
resetEntityCounter();
|
||||
});
|
||||
|
||||
it('should be rendered correctly with fitler lens with mousemove', async () => {
|
||||
const dir = `${__dirname}/snapshots/canvas/plugins/edgeFilterLens`;
|
||||
const { backgroundCanvas, canvas, transientCanvas, container } =
|
||||
createContext('canvas', 500, 500);
|
||||
const graph = EdgeFilterLens({
|
||||
backgroundCanvas,
|
||||
canvas,
|
||||
transientCanvas,
|
||||
width: 500,
|
||||
height: 500,
|
||||
container,
|
||||
});
|
||||
console.log('graph', graph);
|
||||
|
||||
const process = new Promise((reslove) => {
|
||||
graph.on('afterlayout', () => {
|
||||
console.log('afterlayout');
|
||||
reslove();
|
||||
});
|
||||
});
|
||||
|
||||
await process;
|
||||
await sleep(300);
|
||||
triggerEvent(graph, 'mousedown', 200, 200);
|
||||
await expect(transientCanvas).toMatchCanvasSnapshot(
|
||||
dir,
|
||||
'plugins-edge-filter-lens-transients',
|
||||
);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
150
packages/g6/tests/integration/plugins-watermarker.spec.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { Graph } from '../../src/index';
|
||||
import { pxCompare } from '../unit/util';
|
||||
import { sleep } from './utils';
|
||||
|
||||
const container = document.createElement('div');
|
||||
//@ts-ignore
|
||||
document.querySelector('body').appendChild(container);
|
||||
|
||||
const createGraph = (plugins) => {
|
||||
return new Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
data: {
|
||||
nodes: [
|
||||
{ id: 'node1', data: { x: 100, y: 200 } },
|
||||
{ id: 'node2', data: { x: 200, y: 250 } },
|
||||
{ id: 'node3', data: { x: 200, y: 350 } },
|
||||
{ id: 'node4', data: { x: 300, y: 250 } },
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
data: { edgeType: 'e1' },
|
||||
},
|
||||
{
|
||||
id: 'edge2',
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
data: { edgeType: 'e2' },
|
||||
},
|
||||
{
|
||||
id: 'edge3',
|
||||
source: 'node3',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
{
|
||||
id: 'edge4',
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
data: { edgeType: 'e3' },
|
||||
},
|
||||
],
|
||||
},
|
||||
node: {
|
||||
labelShape: {
|
||||
text: {
|
||||
fields: ['id'],
|
||||
formatter: (model) => model.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
modes: {
|
||||
default: ['brush-select'],
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
};
|
||||
|
||||
describe('plugin', () => {
|
||||
it('watermarker with default config', (done) => {
|
||||
const graph = createGraph(['watermarker']);
|
||||
graph.on('afterlayout', (e) => {
|
||||
const watermakerDiv = document.getElementsByClassName(
|
||||
'g6-watermarker',
|
||||
)?.[0];
|
||||
expect(watermakerDiv).not.toBe(undefined);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('watermarker with general config', (done) => {
|
||||
const graph = createGraph([
|
||||
{
|
||||
key: 'watermarker-1',
|
||||
type: 'watermarker',
|
||||
width: 200,
|
||||
height: 200
|
||||
}
|
||||
]);
|
||||
graph.on('afterlayout', (e) => {
|
||||
const watermakerDiv = document.getElementsByClassName(
|
||||
'g6-watermarker',
|
||||
)?.[0];
|
||||
expect(watermakerDiv).not.toBe(undefined);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('watermarker with image config', (done) => {
|
||||
const graph = createGraph([
|
||||
{
|
||||
key: 'watermarker-1',
|
||||
type: 'watermarker',
|
||||
mode: 'image',
|
||||
image: {
|
||||
imgURL:
|
||||
'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg',
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 84,
|
||||
height: 38,
|
||||
rotate: 20,
|
||||
},
|
||||
}
|
||||
]);
|
||||
graph.on('afterlayout', (e) => {
|
||||
const watermakerDiv = document.getElementsByClassName(
|
||||
'g6-watermarker',
|
||||
)?.[0];
|
||||
expect(watermakerDiv).not.toBe(undefined);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('watermarker with text config', (done) => {
|
||||
const graph = createGraph([
|
||||
{
|
||||
key: 'watermarker-1',
|
||||
type: 'watermarker',
|
||||
mode: 'text',
|
||||
text: {
|
||||
texts: ['Test', 'AntV'],
|
||||
x: 10,
|
||||
y: 80,
|
||||
lineHeight: 20,
|
||||
rotate: 30,
|
||||
fontSize: 16,
|
||||
fontFamily: 'Microsoft YaHei',
|
||||
fill: 'rgba(255, 0, 0, 1)',
|
||||
baseline: 'Middle',
|
||||
},
|
||||
}
|
||||
]);
|
||||
graph.on('afterlayout', (e) => {
|
||||
const watermakerDiv = document.getElementsByClassName(
|
||||
'g6-watermarker',
|
||||
)?.[0];
|
||||
expect(watermakerDiv).not.toBe(undefined);
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 10 KiB |
@ -439,7 +439,7 @@ describe('viewport', () => {
|
||||
expect(py).toBeCloseTo(250, 1);
|
||||
});
|
||||
|
||||
await graph.fitCenter({
|
||||
await graph.fitCenter('render', {
|
||||
duration: 2000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: Graph
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [graph](../../modules/graph.en.md) / Graph
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [graph](../../modules/graph.en.md) / Graph
|
||||
|
||||
[graph](../../modules/graph.en.md).Graph
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: Graph
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [graph](../../modules/graph.zh.md) / Graph
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [graph](../../modules/graph.zh.md) / Graph
|
||||
|
||||
[graph](../../modules/graph.zh.md).Graph
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: CircleNode
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CircleNode
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CircleNode
|
||||
|
||||
[item](../../modules/item.en.md).CircleNode
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: CircleNode
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CircleNode
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CircleNode
|
||||
|
||||
[item](../../modules/item.zh.md).CircleNode
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: CustomEdge
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CustomEdge
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CustomEdge
|
||||
|
||||
[item](../../modules/item.en.md).CustomEdge
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: CustomEdge
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CustomEdge
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CustomEdge
|
||||
|
||||
[item](../../modules/item.zh.md).CustomEdge
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: CustomNode
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CustomNode
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CustomNode
|
||||
|
||||
[item](../../modules/item.en.md).CustomNode
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: CustomNode
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CustomNode
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CustomNode
|
||||
|
||||
[item](../../modules/item.zh.md).CustomNode
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: CustomNode3D
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CustomNode3D
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / CustomNode3D
|
||||
|
||||
[item](../../modules/item.en.md).CustomNode3D
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: CustomNode3D
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CustomNode3D
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / CustomNode3D
|
||||
|
||||
[item](../../modules/item.zh.md).CustomNode3D
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: DiamondNode
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / DiamondNode
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / DiamondNode
|
||||
|
||||
[item](../../modules/item.en.md).DiamondNode
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: DiamondNode
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / DiamondNode
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / DiamondNode
|
||||
|
||||
[item](../../modules/item.zh.md).DiamondNode
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: DonutNode
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / DonutNode
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / DonutNode
|
||||
|
||||
[item](../../modules/item.en.md).DonutNode
|
||||
|
||||
|
@ -4,7 +4,7 @@ title: DonutNode
|
||||
|
||||
> 📋 中文文档还在翻译中... 欢迎 PR
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / DonutNode
|
||||
[Overview - v5.0.0-beta.11](../../README.zh.md) / [Modules](../../modules.zh.md) / [item](../../modules/item.zh.md) / DonutNode
|
||||
|
||||
[item](../../modules/item.zh.md).DonutNode
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
title: EllipseNode
|
||||
---
|
||||
|
||||
[Overview - v5.0.0-beta.4](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / EllipseNode
|
||||
[Overview - v5.0.0-beta.11](../../README.en.md) / [Modules](../../modules.en.md) / [item](../../modules/item.en.md) / EllipseNode
|
||||
|
||||
[item](../../modules/item.en.md).EllipseNode
|
||||
|
||||
|