chore: merge v5

This commit is contained in:
hustcc 2023-09-27 15:31:56 +08:00
commit a9a5a30b14
254 changed files with 6449 additions and 929 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

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

View File

@ -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') &&

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

@ -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.`,

View File

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

View File

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

View File

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

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

View File

@ -9,3 +9,4 @@ export * from './donut';
export * from './diamond';
export * from './modelRect';
export * from './image';
export * from './cube';

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

View File

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

View File

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

View File

@ -8,3 +8,4 @@ export * from './toolbar';
export * from './tooltip';
export * from './timebar';
export * from './snapline';
export * from './edgeFilterLens';

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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?: {

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -19,7 +19,7 @@ import {
const GeometryTagMap = {
sphere: SphereGeometry,
cubic: CubeGeometry,
cube: CubeGeometry,
plane: PlaneGeometry,
};

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

View File

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

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

View File

@ -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: {

View File

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

File diff suppressed because it is too large Load Diff

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

View File

@ -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: {

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

View File

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

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

View File

@ -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: {},

View File

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

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

View 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',
],
},
});
};

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

View File

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

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -439,7 +439,7 @@ describe('viewport', () => {
expect(py).toBeCloseTo(250, 1);
});
await graph.fitCenter({
await graph.fitCenter('render', {
duration: 2000,
easing: 'ease-in',
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More