mirror of
https://gitee.com/antv/g6.git
synced 2024-11-30 02:38:20 +08:00
feat: animations (#4439)
* feat: animations * feat: zoom level rendering and size keeping; feat: animate at buildIn, show, hide, update, buildOut * chore: refine * chore: refine
This commit is contained in:
parent
05948c5e7d
commit
2f749bc084
@ -48,8 +48,8 @@
|
||||
"fix": "eslint ./src ./tests --fix && prettier ./src ./tests --write ",
|
||||
"test": "jest",
|
||||
"size": "limit-size",
|
||||
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/behaviors/zoom-canvas-spec.ts",
|
||||
"test-behavior": "DEBUG_MODE=1 jest --watch ./tests/unit/click-select-spec.ts"
|
||||
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/show-animate-spec.ts",
|
||||
"test-behavior": "DEBUG_MODE=1 jest --watch ./tests/unit/item-3d-spec.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
|
@ -16,7 +16,7 @@ export const DEFAULT_SHAPE_STYLE = {
|
||||
fillOpacity: 1,
|
||||
shadowColor: undefined,
|
||||
shadowBlur: 0,
|
||||
lineDash: undefined,
|
||||
lineDash: ['100%', 0],
|
||||
zIndex: 0,
|
||||
};
|
||||
/** Default text style to avoid shape value missing */
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { Group } from '@antv/g';
|
||||
import { clone } from '@antv/util';
|
||||
import { clone, throttle } from '@antv/util';
|
||||
import { EdgeDisplayModel, EdgeModel, NodeModelData } from '../types';
|
||||
import { EdgeModelData } from '../types/edge';
|
||||
import { DisplayMapper, State } from '../types/item';
|
||||
import { DisplayMapper, State, ZoomStrategyObj } from '../types/item';
|
||||
import { updateShapes } from '../util/shape';
|
||||
import Item from './item';
|
||||
import Node from './node';
|
||||
import { EdgeStyleSet } from 'types/theme';
|
||||
import { animateShapes } from '../util/animate';
|
||||
import { EdgeStyleSet } from '../types/theme';
|
||||
|
||||
interface IProps {
|
||||
model: EdgeModel;
|
||||
@ -18,7 +19,12 @@ interface IProps {
|
||||
};
|
||||
sourceItem: Node;
|
||||
targetItem: Node;
|
||||
themeStyles: EdgeStyleSet;
|
||||
zoom?: number;
|
||||
theme: {
|
||||
styles: EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
};
|
||||
onframe?: Function;
|
||||
}
|
||||
|
||||
export default class Edge extends Item {
|
||||
@ -45,6 +51,7 @@ export default class Edge extends Item {
|
||||
displayModel: EdgeDisplayModel,
|
||||
diffData?: { previous: EdgeModelData; current: EdgeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
// get the end points
|
||||
const { x: sx, y: sy, z: sz } = this.sourceItem.model.data as NodeModelData;
|
||||
@ -52,6 +59,9 @@ export default class Edge extends Item {
|
||||
const sourcePoint = this.sourceItem.getAnchorPoint({ x: tx, y: ty, z: tz });
|
||||
const targetPoint = this.targetItem.getAnchorPoint({ x: sx, y: sy, z: sz });
|
||||
this.renderExt.mergeStyles(displayModel);
|
||||
const firstRendering = !this.shapeMap?.keyShape;
|
||||
this.renderExt.setSourcePoint(sourcePoint);
|
||||
this.renderExt.setTargetPoint(targetPoint);
|
||||
const shapeMap = this.renderExt.draw(
|
||||
displayModel,
|
||||
sourcePoint,
|
||||
@ -63,20 +73,55 @@ export default class Edge extends Item {
|
||||
|
||||
// add shapes to group, and update shapeMap
|
||||
this.shapeMap = updateShapes(this.shapeMap, shapeMap, this.group);
|
||||
const { haloShape, labelShape, labelBackgroundShape } = this.shapeMap;
|
||||
|
||||
// handle shape's and group's animate
|
||||
const { animates, disableAnimate } = displayModel.data;
|
||||
const usingAnimates = { ...animates };
|
||||
let targetStyles = this.renderExt.mergedStyles;
|
||||
const { haloShape, labelShape } = this.shapeMap;
|
||||
haloShape?.toBack();
|
||||
labelShape?.toFront();
|
||||
|
||||
super.draw(displayModel, diffData, diffState);
|
||||
this.renderExt.updateCache(this.shapeMap);
|
||||
|
||||
if (firstRendering) {
|
||||
// update the transform
|
||||
this.renderExt.onZoom(this.shapeMap, this.zoom);
|
||||
}
|
||||
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
const timing = firstRendering ? 'buildIn' : 'update';
|
||||
// handle shape's animate
|
||||
if (!disableAnimate && usingAnimates[timing]?.length) {
|
||||
this.animations = animateShapes(
|
||||
usingAnimates,
|
||||
targetStyles, // targetStylesMap
|
||||
this.shapeMap, // shapeMap
|
||||
this.group,
|
||||
firstRendering ? 'buildIn' : 'update',
|
||||
this.changedStates,
|
||||
this.animateFrameListener,
|
||||
() => onfinish(displayModel.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sometimes no changes on edge data, but need to re-draw it
|
||||
* e.g. source and target nodes' position changed
|
||||
*/
|
||||
public forceUpdate() {
|
||||
this.draw(this.displayModel);
|
||||
}
|
||||
public forceUpdate = throttle(
|
||||
() => {
|
||||
if (!this.destroyed) this.draw(this.displayModel);
|
||||
},
|
||||
16,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Update end item for item and re-draw the edge
|
||||
@ -98,6 +143,7 @@ export default class Edge extends Item {
|
||||
sourceItem: Node,
|
||||
targetItem: Node,
|
||||
onlyKeyShape?: boolean,
|
||||
disableAnimate?: boolean,
|
||||
) {
|
||||
if (onlyKeyShape) {
|
||||
const clonedKeyShape = this.shapeMap.keyShape.cloneNode();
|
||||
@ -106,15 +152,21 @@ export default class Edge extends Item {
|
||||
containerGroup.appendChild(clonedGroup);
|
||||
return clonedGroup;
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
return new Edge({
|
||||
model: clone(this.model),
|
||||
model: clonedModel,
|
||||
renderExtensions: this.renderExtensions,
|
||||
sourceItem,
|
||||
targetItem,
|
||||
containerGroup,
|
||||
mapper: this.mapper,
|
||||
stateMapper: this.stateMapper,
|
||||
themeStyles: clone(this.themeStyles),
|
||||
zoom: this.zoom,
|
||||
theme: {
|
||||
styles: clone(this.themeStyles),
|
||||
zoomStrategy: this.zoomStrategy,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Group, DisplayObject, AABB } from '@antv/g';
|
||||
import { clone, isFunction } from '@antv/util';
|
||||
import { Group, DisplayObject, AABB, IAnimation } from '@antv/g';
|
||||
import { clone, isFunction, throttle } from '@antv/util';
|
||||
import { OTHER_SHAPES_FIELD_NAME, RESERVED_SHAPE_IDS } from '../constant';
|
||||
import { EdgeShapeMap } from '../types/edge';
|
||||
import {
|
||||
@ -11,6 +11,7 @@ import {
|
||||
ItemShapeStyles,
|
||||
ITEM_TYPE,
|
||||
State,
|
||||
ZoomStrategyObj,
|
||||
} from '../types/item';
|
||||
import { NodeShapeMap } from '../types/node';
|
||||
import { EdgeStyleSet, NodeStyleSet } from '../types/theme';
|
||||
@ -18,48 +19,75 @@ import { isArrayOverlap } from '../util/array';
|
||||
import { mergeStyles, updateShapes } from '../util/shape';
|
||||
import { isEncode } from '../util/type';
|
||||
import { DEFAULT_MAPPER } from '../util/mapper';
|
||||
import {
|
||||
getShapeAnimateBeginStyles,
|
||||
animateShapes,
|
||||
GROUP_ANIMATE_STYLES,
|
||||
} from '../util/animate';
|
||||
import { AnimateTiming, IAnimates } from '../types/animate';
|
||||
|
||||
export default abstract class Item implements IItem {
|
||||
public destroyed = false;
|
||||
// inner data model
|
||||
/** Inner model. */
|
||||
public model: ItemModel;
|
||||
// display data model
|
||||
/** Display model, user will not touch it. */
|
||||
public displayModel: ItemDisplayModel;
|
||||
/** The mapper configured at graph with field name 'node' / 'edge' / 'combo'. */
|
||||
public mapper: DisplayMapper;
|
||||
/** The state sstyle mapper configured at traph with field name 'nodeState' / 'edgeState' / 'comboState'. */
|
||||
public stateMapper: {
|
||||
[stateName: string]: DisplayMapper;
|
||||
};
|
||||
/** The graphic group for item drawing. */
|
||||
public group: Group;
|
||||
/** The keyShape of the item. */
|
||||
public keyShape: DisplayObject;
|
||||
// render extension for this item
|
||||
/** render extension for this item. */
|
||||
public renderExt;
|
||||
/** Visibility. */
|
||||
public visible = true;
|
||||
/** The states on the item. */
|
||||
public states: {
|
||||
name: string;
|
||||
value: string | boolean;
|
||||
}[] = [];
|
||||
/** The map caches the shapes of the item. The key is the shape id, the value is the g shape. */
|
||||
public shapeMap: NodeShapeMap | EdgeShapeMap = {
|
||||
keyShape: undefined,
|
||||
};
|
||||
/** Set to different value in implements */
|
||||
public afterDrawShapeMap = {};
|
||||
/** Set to different value in implements. */
|
||||
public type: ITEM_TYPE;
|
||||
public renderExtensions: any; // TODO
|
||||
/** Cache the animation instances to stop at next lifecycle. */
|
||||
public animations: IAnimation[];
|
||||
|
||||
/** Cache the dirty tags for states when data changed, to re-map the state styles when state changed */
|
||||
private stateDirtyMap: { [stateName: string]: boolean } = {};
|
||||
private cacheStateStyles: { [stateName: string]: ItemShapeStyles } = {};
|
||||
public themeStyles: {
|
||||
default?: ItemShapeStyles;
|
||||
[stateName: string]: ItemShapeStyles;
|
||||
};
|
||||
/** The zoom strategy to show and hide shapes according to their showLevel. */
|
||||
public zoomStrategy: ZoomStrategyObj;
|
||||
/** Last zoom ratio. */
|
||||
public zoom: number;
|
||||
/** Cache the chaging states which are not consomed by draw */
|
||||
public changedStates: string[];
|
||||
/** The listener for the animations frames. */
|
||||
public onframe: Function;
|
||||
|
||||
private device: any; // for 3d
|
||||
/** Cache the dirty tags for states when data changed, to re-map the state styles when state changed */
|
||||
private stateDirtyMap: { [stateName: string]: boolean } = {};
|
||||
private cacheStateStyles: { [stateName: string]: ItemShapeStyles } = {};
|
||||
private cacheHiddenShape: { [shapeId: string]: boolean } = {};
|
||||
|
||||
// TODO: props type
|
||||
constructor(props) {
|
||||
this.device = props.device;
|
||||
this.onframe = props.onframe;
|
||||
}
|
||||
|
||||
/** Initiate the item. */
|
||||
public init(props) {
|
||||
const {
|
||||
model,
|
||||
@ -67,7 +95,8 @@ export default abstract class Item implements IItem {
|
||||
mapper,
|
||||
stateMapper,
|
||||
renderExtensions,
|
||||
themeStyles = {},
|
||||
zoom = 1,
|
||||
theme = {},
|
||||
} = props;
|
||||
this.group = new Group();
|
||||
this.group.setAttribute('data-item-type', this.type);
|
||||
@ -75,30 +104,40 @@ export default abstract class Item implements IItem {
|
||||
containerGroup.appendChild(this.group);
|
||||
this.model = model;
|
||||
this.mapper = mapper;
|
||||
this.zoom = zoom;
|
||||
this.stateMapper = stateMapper;
|
||||
this.displayModel = this.getDisplayModelAndChanges(model).model;
|
||||
this.renderExtensions = renderExtensions;
|
||||
const { type = this.type === 'node' ? 'circle-node' : 'line-edge' } =
|
||||
this.displayModel.data;
|
||||
const RenderExtension = renderExtensions.find((ext) => ext.type === type);
|
||||
this.themeStyles = themeStyles;
|
||||
this.themeStyles = theme.styles;
|
||||
this.renderExt = new RenderExtension({
|
||||
themeStyles: this.themeStyles.default,
|
||||
zoomStrategy: theme.zoomStrategy,
|
||||
device: this.device,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the shapes.
|
||||
* @internal
|
||||
* */
|
||||
public draw(
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
// call this.renderExt.draw in extend implementations
|
||||
const afterDrawShapes =
|
||||
this.renderExt.afterDraw?.(displayModel, this.shapeMap) || {};
|
||||
this.afterDrawShapeMap =
|
||||
this.renderExt.afterDraw?.(displayModel, {
|
||||
...this.shapeMap,
|
||||
...this.afterDrawShapeMap,
|
||||
}) || {};
|
||||
this.shapeMap = updateShapes(
|
||||
this.shapeMap,
|
||||
afterDrawShapes,
|
||||
this.afterDrawShapeMap,
|
||||
this.group,
|
||||
false,
|
||||
(id) => {
|
||||
@ -111,17 +150,30 @@ export default abstract class Item implements IItem {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
this.changedStates = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the shapes.
|
||||
* @internal
|
||||
* */
|
||||
public update(
|
||||
model: ItemModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
isReplace?: boolean,
|
||||
themeStyles?: NodeStyleSet | EdgeStyleSet,
|
||||
itemTheme?: {
|
||||
styles: NodeStyleSet | EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
onfinish?: Function,
|
||||
) {
|
||||
// 1. merge model into this model
|
||||
this.model = model;
|
||||
if (themeStyles) this.themeStyles = themeStyles;
|
||||
if (itemTheme) {
|
||||
this.themeStyles = itemTheme.styles;
|
||||
this.zoomStrategy = itemTheme.zoomStrategy;
|
||||
}
|
||||
// 2. map new merged model to displayModel, keep prevModel and newModel for 3.
|
||||
const { model: displayModel, typeChange } = this.getDisplayModelAndChanges(
|
||||
this.model,
|
||||
@ -130,6 +182,11 @@ export default abstract class Item implements IItem {
|
||||
);
|
||||
this.displayModel = displayModel;
|
||||
|
||||
if (onlyMove) {
|
||||
this.updatePosition(displayModel, diffData, onfinish);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeChange) {
|
||||
Object.values(this.shapeMap).forEach((child) => child.destroy());
|
||||
this.shapeMap = { keyShape: undefined };
|
||||
@ -140,22 +197,37 @@ export default abstract class Item implements IItem {
|
||||
);
|
||||
this.renderExt = new RenderExtension({
|
||||
themeStyles: this.themeStyles.default,
|
||||
zoomStrategy: this.zoomStrategy,
|
||||
device: this.device,
|
||||
});
|
||||
} else {
|
||||
this.renderExt.themeStyles = this.themeStyles.default;
|
||||
this.renderExt.zoomStrategy = this.zoomStrategy;
|
||||
}
|
||||
// 3. call element update fn from useLib
|
||||
if (this.states?.length) {
|
||||
this.drawWithStates(this.states);
|
||||
this.drawWithStates(this.states, onfinish);
|
||||
} else {
|
||||
this.draw(this.displayModel, diffData);
|
||||
this.draw(this.displayModel, diffData, undefined, onfinish);
|
||||
}
|
||||
// 4. tag all the states with 'dirty', for state style regenerating when state changed
|
||||
this.stateDirtyMap = {};
|
||||
this.states.forEach(({ name }) => (this.stateDirtyMap[name] = true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the group's position, e.g. node, combo.
|
||||
* @param displayModel
|
||||
* @param diffData
|
||||
* @param onfinish
|
||||
* @returns
|
||||
*/
|
||||
public updatePosition(
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
onfinish?: Function,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Maps (mapper will be function, value, or encode format) model to displayModel and find out the shapes to be update for incremental updating.
|
||||
* @param model inner model
|
||||
@ -203,15 +275,13 @@ export default abstract class Item implements IItem {
|
||||
// === fields' values in mapper are final value or Encode ===
|
||||
const dataChangedFields = isReplace
|
||||
? undefined
|
||||
: // ? Array.from(new Set(Object.keys(current).concat(Object.keys(previous)))) // all the fields for replacing all data
|
||||
Object.keys(current).concat(Object.keys(otherFields)); // only fields in current data for partial updating
|
||||
: Object.keys(current).concat(Object.keys(otherFields)); // only fields in current data for partial updating
|
||||
|
||||
let typeChange = false;
|
||||
const { data, ...otherProps } = innerModel;
|
||||
const displayModelData = defaultMapper(innerModel).data; //clone(data);
|
||||
// const defaultMappedModel = defaultMapper(innerModel);
|
||||
Object.keys(mapper).forEach((fieldName) => {
|
||||
debugger;
|
||||
let subMapper = mapper[fieldName];
|
||||
const isReservedShapeId = RESERVED_SHAPE_IDS.includes(fieldName);
|
||||
const isShapeId =
|
||||
@ -287,30 +357,96 @@ export default abstract class Item implements IItem {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public show() {
|
||||
/** Show the item. */
|
||||
public show(animate: boolean = true) {
|
||||
// TODO: utilize graphcore's view
|
||||
this.group.show();
|
||||
this.stopAnimations();
|
||||
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.show?.length) {
|
||||
const showAnimateFieldsMap: any = {};
|
||||
Object.values(animates.show).forEach((animate) => {
|
||||
const { shapeId = 'group' } = animate;
|
||||
showAnimateFieldsMap[shapeId] = (
|
||||
showAnimateFieldsMap[shapeId] || []
|
||||
).concat(animate.fields);
|
||||
});
|
||||
const targetStyleMap = {};
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
if (!this.cacheHiddenShape[id]) {
|
||||
// set the animate fields to initial value
|
||||
if (showAnimateFieldsMap[id]) {
|
||||
targetStyleMap[id] = targetStyleMap[id] || {};
|
||||
const beginStyle = getShapeAnimateBeginStyles(shape);
|
||||
showAnimateFieldsMap[id].forEach((field) => {
|
||||
if (beginStyle.hasOwnProperty(field)) {
|
||||
targetStyleMap[id][field] = shape.style[field];
|
||||
shape.style[field] = beginStyle[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
shape.show();
|
||||
}
|
||||
});
|
||||
if (showAnimateFieldsMap.group) {
|
||||
showAnimateFieldsMap.group.forEach((field) => {
|
||||
const usingField = field === 'size' ? 'transform' : field;
|
||||
if (GROUP_ANIMATE_STYLES[0].hasOwnProperty(usingField)) {
|
||||
this.group.style[usingField] = GROUP_ANIMATE_STYLES[0][usingField];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.animations = this.runWithAnimates(animates, 'show', targetStyleMap);
|
||||
} else {
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
if (!this.cacheHiddenShape[id]) shape.show();
|
||||
});
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
public hide() {
|
||||
/** Hides the item. */
|
||||
public hide(animate: boolean = true) {
|
||||
// TODO: utilize graphcore's view
|
||||
this.group.hide();
|
||||
this.stopAnimations();
|
||||
const func = () => {
|
||||
Object.keys(this.shapeMap).forEach((id) => {
|
||||
const shape = this.shapeMap[id];
|
||||
if (this.visible && !shape.isVisible())
|
||||
this.cacheHiddenShape[id] = true;
|
||||
shape.hide();
|
||||
});
|
||||
};
|
||||
const { animates = {} } = this.displayModel.data;
|
||||
if (animate && animates.hide?.length) {
|
||||
this.animations = this.runWithAnimates(animates, 'hide', undefined, func);
|
||||
} else {
|
||||
// 2. clear group and remove group
|
||||
func();
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
/** Returns the visibility of the item. */
|
||||
public isVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
public toBack() {
|
||||
this.group.toBack();
|
||||
}
|
||||
|
||||
/** Puts the item to the front in its graphic group. */
|
||||
public toFront() {
|
||||
this.group.toFront();
|
||||
}
|
||||
|
||||
/** Puts the item to the back in its graphic group. */
|
||||
public toBack() {
|
||||
this.group.toBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* The state value for the item, false if the item does not have the state.
|
||||
* @param state state name
|
||||
@ -346,6 +482,7 @@ export default abstract class Item implements IItem {
|
||||
this.renderExt.setState(state, value, this.shapeMap);
|
||||
return;
|
||||
}
|
||||
this.changedStates = [state];
|
||||
this.drawWithStates(previousStates);
|
||||
}
|
||||
|
||||
@ -373,6 +510,7 @@ export default abstract class Item implements IItem {
|
||||
}));
|
||||
}
|
||||
this.states = newStates;
|
||||
this.changedStates = changedStates.map((state) => state.name);
|
||||
// if the renderExt overwrote the setState, run the custom setState instead of the default
|
||||
if (this.renderExt.constructor.prototype.hasOwnProperty('setState')) {
|
||||
changedStates.forEach(({ name, value }) =>
|
||||
@ -391,13 +529,40 @@ export default abstract class Item implements IItem {
|
||||
return this.states;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
// TODO: 1. stop animations
|
||||
// 2. clear group and remove group
|
||||
this.group.destroy();
|
||||
this.model = null;
|
||||
this.displayModel = null;
|
||||
this.destroyed = true;
|
||||
/**
|
||||
* Run some thing with animations, e.g. hide, show, destroy.
|
||||
* @param animates
|
||||
* @param timing
|
||||
* @param targetStyleMap
|
||||
* @param callback
|
||||
* @returns
|
||||
*/
|
||||
private runWithAnimates(
|
||||
animates: IAnimates,
|
||||
timing: AnimateTiming,
|
||||
targetStyleMap: Object,
|
||||
callback: Function = () => {},
|
||||
) {
|
||||
let targetStyle = {};
|
||||
if (!targetStyleMap) {
|
||||
Object.keys(this.shapeMap).forEach((shapeId) => {
|
||||
targetStyle[shapeId] = getShapeAnimateBeginStyles(
|
||||
this.shapeMap[shapeId],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
targetStyle = targetStyleMap;
|
||||
}
|
||||
return animateShapes(
|
||||
animates,
|
||||
targetStyle, // targetStylesMap
|
||||
this.shapeMap, // shapeMap
|
||||
this.group,
|
||||
timing,
|
||||
[],
|
||||
() => {},
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -405,7 +570,7 @@ export default abstract class Item implements IItem {
|
||||
* @param previousStates previous states
|
||||
* @returns
|
||||
*/
|
||||
private drawWithStates(previousStates: State[]) {
|
||||
private drawWithStates(previousStates: State[], onfinish?: Function) {
|
||||
const { default: _, ...themeStateStyles } = this.themeStyles;
|
||||
const { data: displayModelData } = this.displayModel;
|
||||
let styles = {}; // merged styles
|
||||
@ -477,6 +642,7 @@ export default abstract class Item implements IItem {
|
||||
previous: previousStates,
|
||||
current: this.states,
|
||||
},
|
||||
onfinish,
|
||||
);
|
||||
}
|
||||
|
||||
@ -489,6 +655,14 @@ export default abstract class Item implements IItem {
|
||||
return keyShape?.getRenderBounds() || ({ center: [0, 0, 0] } as AABB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local bounding box for the keyShape.
|
||||
* */
|
||||
public getLocalKeyBBox(): AABB {
|
||||
const { keyShape } = this.shapeMap;
|
||||
return keyShape?.getLocalBounds() || ({ center: [0, 0, 0] } as AABB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rendering bouding box of the whole item.
|
||||
* @returns item's rendering bounding box
|
||||
@ -496,6 +670,67 @@ export default abstract class Item implements IItem {
|
||||
public getBBox(): AABB {
|
||||
return this.group.getRenderBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all the animations on the item.
|
||||
*/
|
||||
public stopAnimations() {
|
||||
this.animations?.forEach((animation) => {
|
||||
const timing = animation.effect.getTiming();
|
||||
if (animation.playState !== 'running') return;
|
||||
animation.currentTime =
|
||||
Number(timing.duration) + Number(timing.delay || 0);
|
||||
animation.cancel();
|
||||
});
|
||||
this.animations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Animations' frame listemer.
|
||||
*/
|
||||
public animateFrameListener = throttle(
|
||||
(e) => {
|
||||
this.onframe?.(e);
|
||||
},
|
||||
16,
|
||||
{
|
||||
trailing: true,
|
||||
leading: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Call render extension's onZoom to response the graph zooming.
|
||||
* @param zoom
|
||||
*/
|
||||
public updateZoom(zoom) {
|
||||
this.zoom = zoom;
|
||||
this.renderExt.onZoom(this.shapeMap, zoom);
|
||||
}
|
||||
|
||||
/** Destroy the item. */
|
||||
public destroy() {
|
||||
const func = () => {
|
||||
this.group.destroy();
|
||||
this.model = null;
|
||||
this.displayModel = null;
|
||||
this.destroyed = true;
|
||||
};
|
||||
// 1. stop animations, run buildOut animations
|
||||
this.stopAnimations();
|
||||
const { animates } = this.displayModel.data;
|
||||
if (animates.buildOut?.length) {
|
||||
this.animations = this.runWithAnimates(
|
||||
animates,
|
||||
'buildOut',
|
||||
undefined,
|
||||
func,
|
||||
);
|
||||
} else {
|
||||
// 2. clear group and remove group
|
||||
func();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,17 +2,23 @@ import { Group } from '@antv/g';
|
||||
import { clone } from '@antv/util';
|
||||
import { Point } from '../types/common';
|
||||
import { NodeModel } from '../types';
|
||||
import { DisplayMapper, State } from '../types/item';
|
||||
import {
|
||||
DisplayMapper,
|
||||
State,
|
||||
ZoomStrategy,
|
||||
ZoomStrategyObj,
|
||||
} from '../types/item';
|
||||
import { NodeDisplayModel, NodeModelData } from '../types/node';
|
||||
import { NodeStyleSet } from '../types/theme';
|
||||
import { updateShapes } from '../util/shape';
|
||||
import { animateShapes, getAnimatesExcludePosition } from '../util/animate';
|
||||
import Item from './item';
|
||||
import {
|
||||
getCircleIntersectByPoint,
|
||||
getEllipseIntersectByPoint,
|
||||
getNearestPoint,
|
||||
getRectIntersectByPoint,
|
||||
} from 'util/point';
|
||||
} from '../util/point';
|
||||
|
||||
interface IProps {
|
||||
model: NodeModel;
|
||||
@ -22,8 +28,14 @@ interface IProps {
|
||||
stateMapper: {
|
||||
[stateName: string]: DisplayMapper;
|
||||
};
|
||||
themeStyles: NodeStyleSet;
|
||||
zoom?: number;
|
||||
theme: {
|
||||
styles: NodeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
};
|
||||
device?: any; // for 3d shapes
|
||||
onframe?: Function;
|
||||
onfinish?: Function;
|
||||
}
|
||||
export default class Node extends Item {
|
||||
public type: 'node';
|
||||
@ -33,20 +45,25 @@ export default class Node extends Item {
|
||||
super(props);
|
||||
this.type = 'node';
|
||||
this.init(props);
|
||||
this.draw(this.displayModel as NodeDisplayModel);
|
||||
this.draw(
|
||||
this.displayModel as NodeDisplayModel,
|
||||
undefined,
|
||||
undefined,
|
||||
props.onfinish,
|
||||
);
|
||||
}
|
||||
public draw(
|
||||
displayModel: NodeDisplayModel,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
const { group, renderExt, shapeMap: prevShapeMap, model } = this;
|
||||
const { data } = displayModel;
|
||||
const { x = 0, y = 0, z = 0 } = data;
|
||||
group.setPosition(x as number, y as number, z as number);
|
||||
renderExt.mergeStyles(displayModel);
|
||||
this.group.setAttribute('data-item-type', 'node');
|
||||
this.group.setAttribute('data-item-id', model.id);
|
||||
renderExt.mergeStyles(displayModel);
|
||||
|
||||
const firstRendering = !this.shapeMap?.keyShape;
|
||||
const shapeMap = renderExt.draw(
|
||||
displayModel,
|
||||
this.shapeMap,
|
||||
@ -56,29 +73,103 @@ export default class Node extends Item {
|
||||
|
||||
// add shapes to group, and update shapeMap
|
||||
this.shapeMap = updateShapes(prevShapeMap, shapeMap, group);
|
||||
const { haloShape, labelShape, labelBackgroundShape } = this.shapeMap;
|
||||
|
||||
const { animates, disableAnimate, x = 0, y = 0, z = 0 } = displayModel.data;
|
||||
if (firstRendering) {
|
||||
// first rendering, move the group
|
||||
group.style.x = x;
|
||||
group.style.y = y;
|
||||
group.style.z = z;
|
||||
} else {
|
||||
this.updatePosition(displayModel, diffData, onfinish);
|
||||
}
|
||||
|
||||
const { haloShape, labelBackgroundShape } = this.shapeMap;
|
||||
haloShape?.toBack();
|
||||
labelShape?.toFront();
|
||||
labelBackgroundShape?.toBack();
|
||||
|
||||
super.draw(displayModel, diffData, diffState);
|
||||
super.draw(displayModel, diffData, diffState, onfinish);
|
||||
this.anchorPointsCache = undefined;
|
||||
renderExt.updateCache(this.shapeMap);
|
||||
|
||||
if (firstRendering) {
|
||||
// update the transform
|
||||
renderExt.onZoom(this.shapeMap, this.zoom);
|
||||
}
|
||||
|
||||
// terminate previous animations
|
||||
this.stopAnimations();
|
||||
// handle shape's and group's animate
|
||||
if (!disableAnimate && animates) {
|
||||
const animatesExcludePosition = getAnimatesExcludePosition(animates);
|
||||
this.animations = animateShapes(
|
||||
animatesExcludePosition, // animates
|
||||
renderExt.mergedStyles, // targetStylesMap
|
||||
this.shapeMap, // shapeMap
|
||||
group,
|
||||
firstRendering ? 'buildIn' : 'update',
|
||||
this.changedStates,
|
||||
this.animateFrameListener,
|
||||
() => onfinish(model.id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public update(
|
||||
model: NodeModel,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
isReplace?: boolean,
|
||||
themeStyles?: NodeStyleSet,
|
||||
theme?: {
|
||||
styles: NodeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
},
|
||||
onlyMove?: boolean,
|
||||
onfinish?: Function,
|
||||
) {
|
||||
super.update(model, diffData, isReplace, themeStyles);
|
||||
const { data } = this.displayModel;
|
||||
const { x = 0, y = 0 } = data;
|
||||
this.group.style.x = x;
|
||||
this.group.style.y = y;
|
||||
super.update(model, diffData, isReplace, theme, onlyMove, onfinish);
|
||||
}
|
||||
|
||||
public clone(containerGroup: Group, onlyKeyShape?: boolean) {
|
||||
/**
|
||||
* Update the node's position,
|
||||
* do not update other styles which leads to better performance than updating position by updateData.
|
||||
*/
|
||||
public updatePosition(
|
||||
displayModel: NodeDisplayModel,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
onfinish: Function = () => {},
|
||||
) {
|
||||
const { group } = this;
|
||||
const { x = 0, y = 0, z = 0, animates, disableAnimate } = displayModel.data;
|
||||
if (!disableAnimate && animates?.update) {
|
||||
const groupAnimates = animates.update.filter(
|
||||
({ shapeId, fields }) =>
|
||||
(!shapeId || shapeId === 'group') &&
|
||||
(fields.includes('x') || fields.includes('y')),
|
||||
);
|
||||
if (groupAnimates.length) {
|
||||
animateShapes(
|
||||
{ update: groupAnimates },
|
||||
{ group: { x, y, z } } as any, // targetStylesMap
|
||||
this.shapeMap, // shapeMap
|
||||
group,
|
||||
'update',
|
||||
[],
|
||||
this.animateFrameListener,
|
||||
() => onfinish(displayModel.id),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
group.style.x = x;
|
||||
group.style.y = y;
|
||||
group.style.z = z;
|
||||
}
|
||||
|
||||
public clone(
|
||||
containerGroup: Group,
|
||||
onlyKeyShape?: boolean,
|
||||
disableAnimate?: boolean,
|
||||
) {
|
||||
if (onlyKeyShape) {
|
||||
const clonedKeyShape = this.shapeMap.keyShape.cloneNode();
|
||||
const { x, y } = this.group.attributes;
|
||||
@ -88,20 +179,28 @@ export default class Node extends Item {
|
||||
containerGroup.appendChild(clonedGroup);
|
||||
return clonedGroup;
|
||||
}
|
||||
const clonedModel = clone(this.model);
|
||||
clonedModel.data.disableAnimate = disableAnimate;
|
||||
return new Node({
|
||||
model: clone(this.model),
|
||||
model: clonedModel,
|
||||
renderExtensions: this.renderExtensions,
|
||||
containerGroup,
|
||||
mapper: this.mapper,
|
||||
stateMapper: this.stateMapper,
|
||||
themeStyles: clone(this.themeStyles),
|
||||
zoom: this.zoom,
|
||||
theme: {
|
||||
styles: clone(this.themeStyles),
|
||||
zoomStrategy: this.zoomStrategy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public getAnchorPoint(point: Point) {
|
||||
const { keyShape } = this.shapeMap;
|
||||
const shapeType = keyShape.nodeName;
|
||||
const { x, y, anchorPoints = [] } = this.model.data as NodeModelData;
|
||||
const { x, y, z, anchorPoints = [] } = this.model.data as NodeModelData;
|
||||
|
||||
return { x, y, z };
|
||||
|
||||
let intersectPoint: Point | null;
|
||||
switch (shapeType) {
|
||||
|
@ -52,8 +52,14 @@ export class DataController {
|
||||
condition: ID[] | Function,
|
||||
): EdgeModel[] | NodeModel[] | ComboModel[] {
|
||||
const { graphCore } = this;
|
||||
if (isString(condition) || isNumber(condition) || isArray(condition)) {
|
||||
const ids = isArray(condition) ? condition : [condition];
|
||||
const conditionType = typeof condition;
|
||||
const conditionIsArray = isArray(condition);
|
||||
if (
|
||||
conditionType === 'string' ||
|
||||
conditionType === 'number' ||
|
||||
conditionIsArray
|
||||
) {
|
||||
const ids = conditionIsArray ? condition : [condition];
|
||||
switch (type) {
|
||||
case 'node':
|
||||
return ids.map((id) =>
|
||||
@ -67,7 +73,7 @@ export class DataController {
|
||||
// TODO;
|
||||
return;
|
||||
}
|
||||
} else if (isFunction(condition)) {
|
||||
} else if (conditionType === 'function') {
|
||||
const getDatas =
|
||||
type === 'node' ? graphCore.getAllNodes : graphCore.getAllEdges;
|
||||
if (type === 'combo') {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AABB, Canvas, DisplayObject, Group } from '@antv/g';
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { debounce, isArray, isObject, throttle } from '@antv/util';
|
||||
import registry from '../../stdlib';
|
||||
import {
|
||||
ComboModel,
|
||||
@ -23,7 +24,12 @@ import Combo from '../../item/combo';
|
||||
import { upsertShape } from '../../util/shape';
|
||||
import { getExtension } from '../../util/extension';
|
||||
import { upsertTransientItem } from '../../util/item';
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../../types/item';
|
||||
import {
|
||||
ITEM_TYPE,
|
||||
ShapeStyle,
|
||||
SHAPE_TYPE,
|
||||
ZoomStrategyObj,
|
||||
} from '../../types/item';
|
||||
import {
|
||||
ThemeSpecification,
|
||||
NodeThemeSpecifications,
|
||||
@ -31,8 +37,9 @@ import {
|
||||
NodeStyleSet,
|
||||
EdgeStyleSet,
|
||||
} from '../../types/theme';
|
||||
import { isArray, isObject } from '@antv/util';
|
||||
import { DirectionalLight, AmbientLight } from '@antv/g-plugin-3d';
|
||||
import { ViewportChangeHookParams } from '../../types/hook';
|
||||
import { formatZoomStrategy } from '../../util/zoom';
|
||||
|
||||
/**
|
||||
* Manages and stores the node / edge / combo items.
|
||||
@ -115,6 +122,7 @@ export class ItemController {
|
||||
this.onItemVisibilityChange.bind(this),
|
||||
);
|
||||
this.graph.hooks.transientupdate.tap(this.onTransientUpdate.bind(this));
|
||||
this.graph.hooks.viewportchange.tap(this.onViewportChange.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,8 +216,9 @@ export class ItemController {
|
||||
changes: GraphChange<NodeModelData, EdgeModelData>[];
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
action?: 'updateNodePosition';
|
||||
}) {
|
||||
const { changes, graphCore, theme = {} } = param;
|
||||
const { changes, graphCore, theme = {}, action } = param;
|
||||
const groupedChanges = {
|
||||
NodeRemoved: [],
|
||||
EdgeRemoved: [],
|
||||
@ -275,33 +284,54 @@ export class ItemController {
|
||||
});
|
||||
const { dataTypeField: nodeDataTypeField } = nodeTheme;
|
||||
const edgeToUpdate = {};
|
||||
const updateEdges = throttle(
|
||||
() => {
|
||||
Object.keys(edgeToUpdate).forEach((id) => {
|
||||
const item = itemMap[id] as Edge;
|
||||
if (item && !item.destroyed) item.forceUpdate();
|
||||
});
|
||||
},
|
||||
16,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
Object.keys(nodeUpdate).forEach((id) => {
|
||||
const { isReplace, previous, current } = nodeUpdate[id];
|
||||
// update the theme if the dataType value is changed
|
||||
let themeStyles;
|
||||
let itemTheme;
|
||||
if (
|
||||
nodeDataTypeField &&
|
||||
previous[nodeDataTypeField] !== current[nodeDataTypeField]
|
||||
) {
|
||||
themeStyles = getThemeStyles(
|
||||
itemTheme = getItemTheme(
|
||||
this.nodeDataTypeSet,
|
||||
nodeDataTypeField,
|
||||
current[nodeDataTypeField],
|
||||
nodeTheme,
|
||||
);
|
||||
}
|
||||
const item = itemMap[id];
|
||||
const node = itemMap[id] as Node;
|
||||
const innerModel = graphCore.getNode(id);
|
||||
item.update(innerModel, { previous, current }, isReplace, themeStyles);
|
||||
const relatedEdgeInnerModels = graphCore.getRelatedEdges(id);
|
||||
relatedEdgeInnerModels.forEach(
|
||||
(edge) => (edgeToUpdate[edge.id] = edge),
|
||||
node.onframe = updateEdges;
|
||||
node.update(
|
||||
innerModel,
|
||||
{ previous, current },
|
||||
isReplace,
|
||||
itemTheme,
|
||||
action === 'updateNodePosition',
|
||||
// call after updating finished
|
||||
() => {
|
||||
node.onframe = undefined;
|
||||
},
|
||||
);
|
||||
const relatedEdgeInnerModels = graphCore.getRelatedEdges(id);
|
||||
relatedEdgeInnerModels.forEach((edge) => {
|
||||
edgeToUpdate[edge.id] = edge;
|
||||
});
|
||||
});
|
||||
Object.keys(edgeToUpdate).forEach((id) => {
|
||||
const item = itemMap[id] as Edge;
|
||||
item.forceUpdate();
|
||||
});
|
||||
updateEdges();
|
||||
}
|
||||
|
||||
// === 6. update edges' data ===
|
||||
@ -326,9 +356,9 @@ export class ItemController {
|
||||
Object.keys(edgeUpdate).forEach((id) => {
|
||||
const { isReplace, current, previous } = edgeUpdate[id];
|
||||
// update the theme if the dataType value is changed
|
||||
let themeStyles;
|
||||
let itemTheme;
|
||||
if (previous[edgeDataTypeField] !== current[edgeDataTypeField]) {
|
||||
themeStyles = getThemeStyles(
|
||||
itemTheme = getItemTheme(
|
||||
this.edgeDataTypeSet,
|
||||
edgeDataTypeField,
|
||||
current[edgeDataTypeField],
|
||||
@ -337,7 +367,7 @@ export class ItemController {
|
||||
}
|
||||
const item = itemMap[id];
|
||||
const innerModel = graphCore.getEdge(id);
|
||||
item.update(innerModel, { current, previous }, isReplace, themeStyles);
|
||||
item.update(innerModel, { current, previous }, isReplace, itemTheme);
|
||||
});
|
||||
}
|
||||
|
||||
@ -392,8 +422,12 @@ export class ItemController {
|
||||
});
|
||||
}
|
||||
|
||||
private onItemVisibilityChange(param: { ids: ID[]; value: boolean }) {
|
||||
const { ids, value } = param;
|
||||
private onItemVisibilityChange(param: {
|
||||
ids: ID[];
|
||||
value: boolean;
|
||||
animate?: boolean;
|
||||
}) {
|
||||
const { ids, value, animate = true } = param;
|
||||
ids.forEach((id) => {
|
||||
const item = this.itemMap[id];
|
||||
if (!item) {
|
||||
@ -403,13 +437,28 @@ export class ItemController {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
item.show();
|
||||
item.show(animate);
|
||||
} else {
|
||||
item.hide();
|
||||
item.hide(animate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onViewportChange = debounce(
|
||||
({ transform, effectTiming }: ViewportChangeHookParams) => {
|
||||
const { zoom } = transform;
|
||||
if (zoom) {
|
||||
const zoomRatio = this.graph.getZoom();
|
||||
Object.values(this.itemMap).forEach((item) =>
|
||||
item.updateZoom(zoomRatio),
|
||||
);
|
||||
this.zoom = zoomRatio;
|
||||
}
|
||||
},
|
||||
500,
|
||||
false,
|
||||
);
|
||||
|
||||
private onTransientUpdate(param: {
|
||||
type: ITEM_TYPE | SHAPE_TYPE;
|
||||
id: ID;
|
||||
@ -493,7 +542,7 @@ export class ItemController {
|
||||
return;
|
||||
}
|
||||
|
||||
const { shape } = upsertShape(type, String(id), style, transientObjectMap);
|
||||
const shape = upsertShape(type, String(id), style, transientObjectMap);
|
||||
shape.style.pointerEvents = capture ? 'auto' : 'none';
|
||||
canvas.getRoot().appendChild(shape);
|
||||
}
|
||||
@ -511,11 +560,12 @@ export class ItemController {
|
||||
) {
|
||||
const { nodeExtensions, nodeGroup, nodeDataTypeSet, graph } = this;
|
||||
const { dataTypeField } = nodeTheme;
|
||||
const zoom = graph.getZoom();
|
||||
models.forEach((node) => {
|
||||
// get the base styles from theme
|
||||
let dataType;
|
||||
if (dataTypeField) dataType = node.data[dataTypeField] as string;
|
||||
const themeStyle = getThemeStyles(
|
||||
const itemTheme = getItemTheme(
|
||||
nodeDataTypeSet,
|
||||
dataTypeField,
|
||||
dataType,
|
||||
@ -528,7 +578,11 @@ export class ItemController {
|
||||
containerGroup: nodeGroup,
|
||||
mapper: this.nodeMapper,
|
||||
stateMapper: this.nodeStateMapper,
|
||||
themeStyles: themeStyle as NodeStyleSet,
|
||||
zoom,
|
||||
theme: itemTheme as {
|
||||
styles: NodeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
},
|
||||
device:
|
||||
graph.rendererType === 'webgl-3d'
|
||||
? // TODO: G type
|
||||
@ -546,8 +600,9 @@ export class ItemController {
|
||||
models: EdgeModel[],
|
||||
edgeTheme: EdgeThemeSpecifications = {},
|
||||
) {
|
||||
const { edgeExtensions, edgeGroup, itemMap, edgeDataTypeSet } = this;
|
||||
const { edgeExtensions, edgeGroup, itemMap, edgeDataTypeSet, graph } = this;
|
||||
const { dataTypeField } = edgeTheme;
|
||||
const zoom = graph.getZoom();
|
||||
models.forEach((edge) => {
|
||||
const { source, target, id } = edge;
|
||||
const sourceItem = itemMap[source] as Node;
|
||||
@ -565,7 +620,7 @@ export class ItemController {
|
||||
// get the base styles from theme
|
||||
let dataType;
|
||||
if (dataTypeField) dataType = edge.data[dataTypeField] as string;
|
||||
const themeStyle = getThemeStyles(
|
||||
const itemTheme = getItemTheme(
|
||||
edgeDataTypeSet,
|
||||
dataTypeField,
|
||||
dataType,
|
||||
@ -580,7 +635,11 @@ export class ItemController {
|
||||
stateMapper: this.edgeStateMapper,
|
||||
sourceItem,
|
||||
targetItem,
|
||||
themeStyles: themeStyle as EdgeStyleSet,
|
||||
zoom,
|
||||
theme: itemTheme as {
|
||||
styles: EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -649,18 +708,23 @@ export class ItemController {
|
||||
}
|
||||
}
|
||||
|
||||
const getThemeStyles = (
|
||||
const getItemTheme = (
|
||||
dataTypeSet: Set<string>,
|
||||
dataTypeField: string,
|
||||
dataType: string,
|
||||
itemTheme: NodeThemeSpecifications | EdgeThemeSpecifications,
|
||||
): NodeStyleSet | EdgeStyleSet => {
|
||||
const { styles: themeStyles } = itemTheme;
|
||||
): {
|
||||
styles: NodeStyleSet | EdgeStyleSet;
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
} => {
|
||||
const { styles: themeStyles, zoomStrategy } = itemTheme;
|
||||
const formattedZoomStrategy = formatZoomStrategy(zoomStrategy);
|
||||
if (!dataTypeField) {
|
||||
// dataType field is not assigned
|
||||
return isArray(themeStyles)
|
||||
const styles = isArray(themeStyles)
|
||||
? themeStyles[0]
|
||||
: Object.values(themeStyles)[0];
|
||||
return { styles, zoomStrategy: formattedZoomStrategy };
|
||||
}
|
||||
dataTypeSet.add(dataType as string);
|
||||
let themeStyle;
|
||||
@ -671,5 +735,8 @@ const getThemeStyles = (
|
||||
} else if (isObject(themeStyles)) {
|
||||
themeStyle = themeStyles[dataType] || themeStyles.others;
|
||||
}
|
||||
return themeStyle;
|
||||
return {
|
||||
styles: themeStyle,
|
||||
zoomStrategy: formattedZoomStrategy,
|
||||
};
|
||||
};
|
||||
|
@ -175,15 +175,7 @@ export class LayoutController {
|
||||
}
|
||||
|
||||
private updateNodesPosition(positions: LayoutMapping) {
|
||||
positions.nodes.forEach((node) => {
|
||||
this.graph.updateData('node', {
|
||||
id: node.id,
|
||||
data: {
|
||||
x: node.data.x,
|
||||
y: node.data.y,
|
||||
},
|
||||
});
|
||||
});
|
||||
this.graph.updateNodePosition(positions.nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -147,7 +147,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
return;
|
||||
}
|
||||
this.container = containerDOM;
|
||||
let size = [width, height];
|
||||
const size = [width, height];
|
||||
if (size[0] === undefined) {
|
||||
size[0] = containerDOM.scrollWidth;
|
||||
}
|
||||
@ -257,7 +257,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* Update the specs(configurations).
|
||||
*/
|
||||
public updateSpecification(spec: Specification<B, T>) {
|
||||
// TODO
|
||||
return Object.assign(this.specification, spec);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -794,6 +794,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
const { graphCore } = this.dataController;
|
||||
const { specification } = this.themeController;
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
this.hooks.itemchange.emit({
|
||||
type: itemType,
|
||||
changes: graphCore.reduceChanges(event.changes),
|
||||
@ -832,6 +833,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
itemType === 'edge' ? userGraphCore.getEdge : userGraphCore.getNode; // TODO: combo
|
||||
data[`${itemType}s`] = idArr.map((id) => getItem.bind(userGraphCore)(id));
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
this.hooks.itemchange.emit({
|
||||
type: itemType,
|
||||
changes: event.changes,
|
||||
@ -847,9 +849,9 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
}
|
||||
/**
|
||||
* Update one or more node/edge/combo data on the graph.
|
||||
* @param {Item} item item or id
|
||||
* @param {EdgeConfig | NodeConfig} cfg incremental updated configs
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
* @param {ITEM_TYPE} itemType 'node' | 'edge' | 'combo'
|
||||
* @param models new configurations for every node/edge/combo, which has id field to indicate the specific item
|
||||
* @param {boolean} stack whether push this operation into graph's stack, true by default
|
||||
* @group Data
|
||||
*/
|
||||
public updateData(
|
||||
@ -901,17 +903,66 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
);
|
||||
return isArray(models) ? dataList : dataList[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update one or more nodes' positions,
|
||||
* do not update other styles which leads to better performance than updating positions by updateData.
|
||||
* @param models new configurations with x and y for every node, which has id field to indicate the specific item
|
||||
* @param {boolean} stack whether push this operation into graph's stack, true by default
|
||||
* @group Data
|
||||
*/
|
||||
public updateNodePosition(
|
||||
models:
|
||||
| Partial<NodeUserModel>
|
||||
| Partial<
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
stack?: boolean,
|
||||
) {
|
||||
const modelArr = isArray(models) ? models : [models];
|
||||
const { graphCore } = this.dataController;
|
||||
const { specification } = this.themeController;
|
||||
graphCore.once('changed', (event) => {
|
||||
if (!event.changes.length) return;
|
||||
this.hooks.itemchange.emit({
|
||||
type: 'node',
|
||||
changes: event.changes,
|
||||
graphCore,
|
||||
theme: specification,
|
||||
action: 'updateNodePosition',
|
||||
});
|
||||
this.emit('afteritemchange', {
|
||||
type: 'node',
|
||||
action: 'updateNodePosition',
|
||||
models,
|
||||
});
|
||||
});
|
||||
|
||||
this.hooks.datachange.emit({
|
||||
data: {
|
||||
nodes: modelArr as NodeUserModel[],
|
||||
edges: [],
|
||||
},
|
||||
type: 'update',
|
||||
});
|
||||
const dataList = this.dataController.findData(
|
||||
'node',
|
||||
modelArr.map((model) => model.id),
|
||||
);
|
||||
return isArray(models) ? dataList : dataList[0];
|
||||
}
|
||||
/**
|
||||
* Show the item(s).
|
||||
* @param item the item to be shown
|
||||
* @returns
|
||||
* @group Item
|
||||
*/
|
||||
public showItem(ids: ID | ID[]) {
|
||||
public showItem(ids: ID | ID[], disableAniamte?: boolean) {
|
||||
const idArr = isArray(ids) ? ids : [ids];
|
||||
this.hooks.itemvisibilitychange.emit({
|
||||
ids: idArr as ID[],
|
||||
value: true,
|
||||
animate: !disableAniamte,
|
||||
});
|
||||
}
|
||||
/**
|
||||
@ -920,11 +971,12 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns
|
||||
* @group Item
|
||||
*/
|
||||
public hideItem(ids: ID | ID[]) {
|
||||
public hideItem(ids: ID | ID[], disableAniamte?: boolean) {
|
||||
const idArr = isArray(ids) ? ids : [ids];
|
||||
this.hooks.itemvisibilitychange.emit({
|
||||
ids: idArr as ID[],
|
||||
value: false,
|
||||
animate: !disableAniamte,
|
||||
});
|
||||
}
|
||||
/**
|
||||
|
@ -72,6 +72,7 @@ export default class ClickSelect extends Behavior {
|
||||
getEvents = () => {
|
||||
return {
|
||||
'node:click': this.onClick,
|
||||
'edge:click': this.onClick,
|
||||
'canvas:click': this.onCanvasClick,
|
||||
};
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ID, IG6GraphEvent } from 'types';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { Point } from 'types/common';
|
||||
import { Point } from '../../types/common';
|
||||
|
||||
const VALID_TRIGGERS = ['drag', 'directionKeys'];
|
||||
export interface DragCanvasOptions {
|
||||
@ -125,7 +125,7 @@ export default class DragCanvas extends Behavior {
|
||||
.getAllEdgesData()
|
||||
.map((edge) => edge.id)
|
||||
.filter((id) => graph.getItemVisible(id) === true);
|
||||
graph.hideItem(this.hiddenEdgeIds);
|
||||
graph.hideItem(this.hiddenEdgeIds, true);
|
||||
this.hiddenNodeIds = graph
|
||||
.getAllNodesData()
|
||||
.map((node) => node.id)
|
||||
@ -136,7 +136,7 @@ export default class DragCanvas extends Behavior {
|
||||
onlyDrawKeyShape: true,
|
||||
});
|
||||
});
|
||||
graph.hideItem(this.hiddenNodeIds);
|
||||
graph.hideItem(this.hiddenNodeIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,8 +185,8 @@ export default class DragCanvas extends Behavior {
|
||||
const { graph } = this;
|
||||
const { client } = event;
|
||||
const { eventName, direction } = this.options;
|
||||
let diffX = client.x - this.pointerDownAt.x;
|
||||
let diffY = client.y - this.pointerDownAt.y;
|
||||
const diffX = client.x - this.pointerDownAt.x;
|
||||
const diffY = client.y - this.pointerDownAt.y;
|
||||
if (direction === 'x' && !diffX) return;
|
||||
if (direction === 'y' && !diffY) return;
|
||||
if (direction === 'both' && !diffX && !diffY) return;
|
||||
@ -215,13 +215,13 @@ export default class DragCanvas extends Behavior {
|
||||
const { graph } = this;
|
||||
if (this.options.enableOptimize) {
|
||||
if (this.hiddenEdgeIds) {
|
||||
graph.showItem(this.hiddenEdgeIds);
|
||||
graph.showItem(this.hiddenEdgeIds, true);
|
||||
}
|
||||
if (this.hiddenNodeIds) {
|
||||
this.hiddenNodeIds.forEach((id) => {
|
||||
this.graph.drawTransient('node', id, { action: 'remove' });
|
||||
});
|
||||
graph.showItem(this.hiddenNodeIds);
|
||||
graph.showItem(this.hiddenNodeIds, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { debounce, uniq } from '@antv/util';
|
||||
import { throttle, uniq } from '@antv/util';
|
||||
import { EdgeModel } from '../../types';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { IG6GraphEvent } from '../../types/event';
|
||||
@ -37,10 +37,10 @@ export interface DragNodeOptions {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* The time in milliseconds to debounce moving. Useful to avoid the frequent calculation.
|
||||
* The time in milliseconds to throttle moving. Useful to avoid the frequent calculation.
|
||||
* Defaults to 0.
|
||||
*/
|
||||
debounce?: number;
|
||||
throttle?: number;
|
||||
/**
|
||||
* Whether to hide the related edges to avoid calculation while dragging nodes.
|
||||
* Ignored when enableTransient or enableDelegate is true.
|
||||
@ -72,7 +72,7 @@ const DEFAULT_OPTIONS: Required<DragNodeOptions> = {
|
||||
strokeOpacity: 0.9,
|
||||
lineDash: [5, 5],
|
||||
},
|
||||
debounce: 0,
|
||||
throttle: 16,
|
||||
hideRelatedEdges: false,
|
||||
selectedState: 'selected',
|
||||
eventName: '',
|
||||
@ -167,7 +167,10 @@ export class DragNode extends Behavior {
|
||||
// Hide related edge.
|
||||
if (this.options.hideRelatedEdges && !enableTransient) {
|
||||
this.hiddenEdges = this.getRelatedEdges(selectedNodeIds);
|
||||
this.graph.hideItem(this.hiddenEdges.map((edge) => edge.id));
|
||||
this.graph.hideItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw transient nodes and edges.
|
||||
@ -182,15 +185,22 @@ export class DragNode extends Behavior {
|
||||
});
|
||||
|
||||
// Hide original edges and nodes. They will be restored when pointerup.
|
||||
this.graph.hideItem(selectedNodeIds);
|
||||
this.graph.hideItem(this.hiddenEdges.map((edge) => edge.id));
|
||||
this.graph.hideItem(selectedNodeIds, true);
|
||||
this.graph.hideItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
// Debounce moving.
|
||||
if (this.options.debounce > 0) {
|
||||
this.debouncedMoveNodes = debounce(this.moveNodes, this.options.debounce);
|
||||
// Throttle moving.
|
||||
if (this.options.throttle > 0) {
|
||||
this.throttledMoveNodes = throttle(
|
||||
this.moveNodes,
|
||||
this.options.throttle,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
} else {
|
||||
this.debouncedMoveNodes = this.moveNodes;
|
||||
this.throttledMoveNodes = this.moveNodes;
|
||||
}
|
||||
|
||||
// @ts-ignore FIXME: Type
|
||||
@ -216,7 +226,7 @@ export class DragNode extends Behavior {
|
||||
} else {
|
||||
const enableTransient =
|
||||
this.options.enableTransient && this.graph.rendererType !== 'webgl-3d';
|
||||
this.debouncedMoveNodes(deltaX, deltaY, enableTransient);
|
||||
this.throttledMoveNodes(deltaX, deltaY, enableTransient);
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,17 +255,17 @@ export class DragNode extends Behavior {
|
||||
this.graph.drawTransient('edge', edge.id, {});
|
||||
});
|
||||
} else {
|
||||
this.graph.updateData('node', positionChanges);
|
||||
this.graph.updateNodePosition(positionChanges);
|
||||
}
|
||||
}
|
||||
|
||||
public debouncedMoveNodes(
|
||||
public throttledMoveNodes: Function = (
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
transient: boolean,
|
||||
) {
|
||||
) => {
|
||||
// Should be overrided when drag start.
|
||||
}
|
||||
};
|
||||
|
||||
public moveDelegate(deltaX: number, deltaY: number) {
|
||||
const x1 = Math.min(
|
||||
@ -298,13 +308,19 @@ export class DragNode extends Behavior {
|
||||
|
||||
public restoreHiddenItems() {
|
||||
if (this.hiddenEdges.length) {
|
||||
this.graph.showItem(this.hiddenEdges.map((edge) => edge.id));
|
||||
this.graph.showItem(
|
||||
this.hiddenEdges.map((edge) => edge.id),
|
||||
true,
|
||||
);
|
||||
this.hiddenEdges = [];
|
||||
}
|
||||
const enableTransient =
|
||||
this.options.enableTransient && this.graph.rendererType !== 'webgl-3d';
|
||||
if (enableTransient) {
|
||||
this.graph.showItem(this.originPositions.map((position) => position.id));
|
||||
this.graph.showItem(
|
||||
this.originPositions.map((position) => position.id),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,7 +376,7 @@ export class DragNode extends Behavior {
|
||||
const positionChanges = this.originPositions.map(({ id, x, y }) => {
|
||||
return { id, data: { x, y } };
|
||||
});
|
||||
this.graph.updateData('node', positionChanges);
|
||||
this.graph.updateNodePosition(positionChanges);
|
||||
}
|
||||
|
||||
this.originPositions = [];
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ID, IG6GraphEvent } from 'types';
|
||||
import { Behavior } from '../../types/behavior';
|
||||
import { Point } from 'types/common';
|
||||
|
||||
const VALID_TRIGGERS = ['wheel', 'upDownKeys'];
|
||||
export interface ZoomCanvasOptions {
|
||||
@ -49,7 +48,7 @@ export interface ZoomCanvasOptions {
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<ZoomCanvasOptions> = {
|
||||
enableOptimize: true,
|
||||
enableOptimize: false,
|
||||
zoomOnItems: false,
|
||||
sensitivity: 1,
|
||||
trigger: 'wheel',
|
||||
|
@ -17,19 +17,27 @@ import {
|
||||
SHAPE_TYPE,
|
||||
ShapeStyle,
|
||||
State,
|
||||
ZoomStrategyObj,
|
||||
} from '../../../types/item';
|
||||
import {
|
||||
LOCAL_BOUNDS_DIRTY_FLAG_KEY,
|
||||
formatPadding,
|
||||
isStyleAffectBBox,
|
||||
mergeStyles,
|
||||
upsertShape,
|
||||
} from '../../../util/shape';
|
||||
import { DEFAULT_ANIMATE_CFG, fadeIn, fadeOut } from '../../../util/animate';
|
||||
import { getWordWrapWidthByEnds } from '../../../util/text';
|
||||
import { AnimateCfg } from '../../../types/animate';
|
||||
import { getZoomLevel } from '../../../util/zoom';
|
||||
|
||||
export abstract class BaseEdge {
|
||||
type: string;
|
||||
defaultStyles: EdgeShapeStyles = {};
|
||||
themeStyles: EdgeShapeStyles;
|
||||
mergedStyles: EdgeShapeStyles;
|
||||
sourcePoint: Point;
|
||||
targetPoint: Point;
|
||||
zoomStrategy?: ZoomStrategyObj;
|
||||
labelPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -39,11 +47,48 @@ export abstract class BaseEdge {
|
||||
boundsCache: {
|
||||
labelShapeGeometry?: AABB;
|
||||
labelBackgroundShapeGeometry?: AABB;
|
||||
labelShapeTransform?: string;
|
||||
labelBackgroundShapeTransform?: string;
|
||||
};
|
||||
// cache the zoom level infomations
|
||||
private zoomCache: {
|
||||
// the id of shapes which are hidden by zoom changing.
|
||||
hiddenShape: { [shapeId: string]: boolean };
|
||||
// timeout timer for scaling shapes with balanceRatio, simulates debounce in function.
|
||||
balanceTimer: NodeJS.Timeout;
|
||||
// the ratio to scale the size of shapes whose visual size should be kept, e.g. label and badges.
|
||||
balanceRatio: number;
|
||||
// last responsed zoom ratio.
|
||||
zoom: number;
|
||||
// last responsed zoom level where the zoom ratio at. Zoom ratio 1 at level 0.
|
||||
zoomLevel: number;
|
||||
// shape ids in different zoom levels.
|
||||
levelShapes: {
|
||||
[level: string]: string[];
|
||||
};
|
||||
// wordWrapWidth of labelShape according to the maxWidth
|
||||
wordWrapWidth: number;
|
||||
// animate configurations for zoom level changing
|
||||
animateConfig: AnimateCfg;
|
||||
} = {
|
||||
hiddenShape: {},
|
||||
balanceRatio: 1,
|
||||
zoom: 1,
|
||||
zoomLevel: 0,
|
||||
balanceTimer: undefined,
|
||||
levelShapes: {},
|
||||
wordWrapWidth: 50,
|
||||
animateConfig: DEFAULT_ANIMATE_CFG.zoom,
|
||||
};
|
||||
constructor(props) {
|
||||
const { themeStyles } = props;
|
||||
const { themeStyles, zoomStrategy } = props;
|
||||
if (themeStyles) this.themeStyles = themeStyles;
|
||||
this.zoomStrategy = zoomStrategy;
|
||||
this.boundsCache = {};
|
||||
this.zoomCache.animateConfig = {
|
||||
...DEFAULT_ANIMATE_CFG.zoom,
|
||||
...zoomStrategy?.animateCfg,
|
||||
};
|
||||
}
|
||||
public mergeStyles(model: EdgeDisplayModel) {
|
||||
this.mergedStyles = this.getMergedStyles(model);
|
||||
@ -61,8 +106,56 @@ export abstract class BaseEdge {
|
||||
);
|
||||
}
|
||||
});
|
||||
return mergeStyles([this.themeStyles, this.defaultStyles, dataStyles]);
|
||||
const merged = mergeStyles([
|
||||
this.themeStyles,
|
||||
this.defaultStyles,
|
||||
dataStyles,
|
||||
]) as EdgeShapeStyles;
|
||||
|
||||
const padding = merged.labelBackgroundShape?.padding;
|
||||
if (padding) {
|
||||
merged.labelBackgroundShape.padding = formatPadding(
|
||||
padding,
|
||||
DEFAULT_LABEL_BG_PADDING,
|
||||
);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call it after calling draw function to update cache about bounds and zoom levels.
|
||||
*/
|
||||
public updateCache(shapeMap) {
|
||||
['labelShape', 'labelBackgroundShape'].forEach((id) => {
|
||||
const shape = shapeMap[id];
|
||||
if (shape?.getAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY)) {
|
||||
this.boundsCache[`${id}Geometry`] = shape.getGeometryBounds();
|
||||
this.boundsCache[`${id}Transform`] = shape.style.transform;
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, false);
|
||||
}
|
||||
});
|
||||
|
||||
const { levelShapes, zoom } = this.zoomCache;
|
||||
Object.keys(shapeMap).forEach((shapeId) => {
|
||||
const { showLevel } = shapeMap[shapeId].attributes;
|
||||
if (showLevel !== undefined) {
|
||||
levelShapes[showLevel] = levelShapes[showLevel] || [];
|
||||
levelShapes[showLevel].push(shapeId);
|
||||
}
|
||||
});
|
||||
|
||||
const { maxWidth = '60%' } = this.mergedStyles.labelShape || {};
|
||||
this.zoomCache.wordWrapWidth = getWordWrapWidthByEnds(
|
||||
[this.sourcePoint, this.targetPoint],
|
||||
maxWidth,
|
||||
1,
|
||||
);
|
||||
|
||||
this.zoomCache.zoom = 1;
|
||||
this.zoomCache.zoomLevel = 0;
|
||||
if (zoom !== 1) this.onZoom(shapeMap, zoom);
|
||||
}
|
||||
|
||||
abstract draw(
|
||||
model: EdgeDisplayModel,
|
||||
sourcePoint: Point,
|
||||
@ -104,6 +197,7 @@ export abstract class BaseEdge {
|
||||
offsetX: propsOffsetX,
|
||||
offsetY: propsOffsetY,
|
||||
autoRotate = true,
|
||||
maxWidth,
|
||||
...otherStyle
|
||||
} = shapeStyle;
|
||||
|
||||
@ -177,22 +271,19 @@ export abstract class BaseEdge {
|
||||
...positionStyle,
|
||||
isRevert,
|
||||
};
|
||||
const wordWrapWidth = getWordWrapWidthByEnds(
|
||||
[this.sourcePoint, this.targetPoint],
|
||||
maxWidth,
|
||||
this.zoomCache.zoom,
|
||||
);
|
||||
const style = {
|
||||
...this.defaultStyles.labelShape,
|
||||
textAlign: positionPreset.textAlign,
|
||||
wordWrapWidth,
|
||||
...positionStyle,
|
||||
...otherStyle,
|
||||
};
|
||||
const { shape, updateStyles } = this.upsertShape(
|
||||
'text',
|
||||
'labelShape',
|
||||
style,
|
||||
shapeMap,
|
||||
);
|
||||
if (isStyleAffectBBox('text', updateStyles)) {
|
||||
this.boundsCache.labelShapeGeometry = shape.getGeometryBounds();
|
||||
}
|
||||
return shape;
|
||||
return this.upsertShape('text', 'labelShape', style, shapeMap, model);
|
||||
}
|
||||
|
||||
public drawLabelBackgroundShape(
|
||||
@ -210,17 +301,17 @@ export abstract class BaseEdge {
|
||||
const textBBox =
|
||||
this.boundsCache.labelShapeGeometry || labelShape.getGeometryBounds();
|
||||
const { x, y, transform, isRevert } = this.labelPosition;
|
||||
const { padding: propsPadding, ...backgroundStyle } = labelBackgroundShape;
|
||||
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
|
||||
const { padding, ...backgroundStyle } = labelBackgroundShape;
|
||||
const { balanceRatio = 1 } = this.zoomCache;
|
||||
const textWidth = textBBox.max[0] - textBBox.min[0];
|
||||
const textHeight = textBBox.max[1] - textBBox.min[1];
|
||||
const bgStyle = {
|
||||
fill: '#fff',
|
||||
...backgroundStyle,
|
||||
x: textBBox.min[0] - padding[3] + x,
|
||||
y: textBBox.min[1] - padding[0] + y,
|
||||
y: textBBox.min[1] - padding[0] / balanceRatio + y,
|
||||
width: textWidth + padding[1] + padding[3],
|
||||
height: textHeight + padding[0] + padding[2],
|
||||
height: textHeight + (padding[0] + padding[2]) / balanceRatio,
|
||||
transform: transform,
|
||||
};
|
||||
if (labelShapeStyle.position === 'start') {
|
||||
@ -245,16 +336,13 @@ export abstract class BaseEdge {
|
||||
}`;
|
||||
}
|
||||
|
||||
const { shape, updateStyles } = this.upsertShape(
|
||||
return this.upsertShape(
|
||||
'rect',
|
||||
'labelBackgroundShape',
|
||||
bgStyle,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
if (isStyleAffectBBox('rect', updateStyles)) {
|
||||
this.boundsCache.labelBackgroundShapeGeometry = shape.getGeometryBounds();
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
public drawIconShape(
|
||||
@ -336,7 +424,8 @@ export abstract class BaseEdge {
|
||||
'iconShape',
|
||||
shapeStyle as GShapeStyle,
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
public drawHaloShape(
|
||||
@ -357,7 +446,99 @@ export abstract class BaseEdge {
|
||||
isBillboard: true,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener for graph zooming.
|
||||
* 1. show / hide some shapes while zoom level changed;
|
||||
* 2. change the shapes' sizes to make them have same visual size while zooming, e.g. labelShape, labelBackgroundShape.
|
||||
* @param shapeMap
|
||||
* @param zoom
|
||||
*/
|
||||
public onZoom = (shapeMap: EdgeShapeMap, zoom: number) => {
|
||||
// balance the size for label, badges
|
||||
this.balanceShapeSize(shapeMap, zoom);
|
||||
|
||||
// zoomLevel changed
|
||||
if (!this.zoomStrategy) return;
|
||||
const { levels } = this.zoomStrategy;
|
||||
const {
|
||||
levelShapes,
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
zoomLevel: previousLevel,
|
||||
} = this.zoomCache;
|
||||
|
||||
// last zoom ratio responsed by zoom changing, which might not equal to zoom.previous in props since the function is debounced.
|
||||
const currentLevel = getZoomLevel(levels, zoom);
|
||||
if (currentLevel < previousLevel) {
|
||||
// zoomLevel changed, from higher to lower, hide something
|
||||
levelShapes[currentLevel + 1]?.forEach((id) =>
|
||||
fadeOut(id, shapeMap[id], hiddenShape, animateConfig),
|
||||
);
|
||||
} else if (currentLevel > previousLevel) {
|
||||
// zoomLevel changed, from lower to higher, show something
|
||||
levelShapes[String(currentLevel)]?.forEach((id) =>
|
||||
fadeIn(
|
||||
id,
|
||||
shapeMap[id],
|
||||
this.mergedStyles[id] ||
|
||||
this.mergedStyles[id.replace('Background', '')],
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.zoomCache.zoom = zoom;
|
||||
this.zoomCache.zoomLevel = currentLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the shapes' sizes e.g. labelShape, labelBackgroundShape, to keep the visual size while zooming.
|
||||
* @param shapeMap
|
||||
* @param zoom
|
||||
* @returns
|
||||
*/
|
||||
private balanceShapeSize(shapeMap, zoom) {
|
||||
const { labelShape, labelBackgroundShape } = shapeMap;
|
||||
const balanceRatio = 1 / zoom || 1;
|
||||
this.zoomCache.balanceRatio = balanceRatio;
|
||||
const { labelShape: labelStyle } = this.mergedStyles;
|
||||
const { position = 'bottom' } = labelStyle;
|
||||
if (!labelShape) return;
|
||||
|
||||
if (position === 'bottom') labelShape.style.transformOrigin = '0';
|
||||
else labelShape.style.transformOrigin = '';
|
||||
|
||||
const oriTransform = this.boundsCache.labelShapeTransform;
|
||||
labelShape.style.transform = `${oriTransform} scale(${balanceRatio}, ${balanceRatio})`;
|
||||
|
||||
const wordWrapWidth = this.zoomCache.wordWrapWidth * zoom;
|
||||
labelShape.style.wordWrapWidth = wordWrapWidth;
|
||||
|
||||
if (!labelBackgroundShape) return;
|
||||
|
||||
const oriBgTransform = this.boundsCache.labelBackgroundShapeTransform;
|
||||
labelBackgroundShape.style.transform = `${oriBgTransform} scale(1, ${balanceRatio})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the source point { x, y } for the edge. Called in item's draw func.
|
||||
* @param point
|
||||
*/
|
||||
public setSourcePoint(point: Point) {
|
||||
this.sourcePoint = point;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the target point { x, y } for the edge. Called in item's draw func.
|
||||
* @param point
|
||||
*/
|
||||
public setTargetPoint(point: Point) {
|
||||
this.targetPoint = point;
|
||||
}
|
||||
|
||||
public upsertShape(
|
||||
@ -365,10 +546,8 @@ export abstract class BaseEdge {
|
||||
id: string,
|
||||
style: ShapeStyle,
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
): {
|
||||
updateStyles: ShapeStyle;
|
||||
shape: DisplayObject;
|
||||
} {
|
||||
return upsertShape(type, id, style as GShapeStyle, shapeMap);
|
||||
model: EdgeDisplayModel,
|
||||
): DisplayObject {
|
||||
return upsertShape(type, id, style as GShapeStyle, shapeMap, model);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { isStyleAffectBBox } from 'util/shape';
|
||||
import { Point } from '../../../types/common';
|
||||
import {
|
||||
EdgeDisplayModel,
|
||||
@ -93,6 +92,7 @@ export class LineEdge extends BaseEdge {
|
||||
isBillboard: true,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import { AABB, DisplayObject, ImageStyleProps, TextStyleProps } from '@antv/g';
|
||||
import {
|
||||
DEFAULT_LABEL_BG_PADDING,
|
||||
OTHER_SHAPES_FIELD_NAME,
|
||||
RESERVED_SHAPE_IDS,
|
||||
} from '../../../constant';
|
||||
import { AABB, DisplayObject } from '@antv/g';
|
||||
import { OTHER_SHAPES_FIELD_NAME, RESERVED_SHAPE_IDS } from '../../../constant';
|
||||
import { NodeDisplayModel } from '../../../types';
|
||||
import {
|
||||
GShapeStyle,
|
||||
@ -11,6 +7,7 @@ import {
|
||||
SHAPE_TYPE_3D,
|
||||
ShapeStyle,
|
||||
State,
|
||||
ZoomStrategyObj,
|
||||
} from '../../../types/item';
|
||||
import {
|
||||
NodeModelData,
|
||||
@ -18,25 +15,66 @@ import {
|
||||
NodeShapeStyles,
|
||||
} from '../../../types/node';
|
||||
import {
|
||||
LOCAL_BOUNDS_DIRTY_FLAG_KEY,
|
||||
formatPadding,
|
||||
isStyleAffectBBox,
|
||||
mergeStyles,
|
||||
upsertShape,
|
||||
} from '../../../util/shape';
|
||||
import { getWordWrapWidthByBox } from '../../../util/text';
|
||||
import { DEFAULT_ANIMATE_CFG, fadeIn, fadeOut } from '../../../util/animate';
|
||||
import { getZoomLevel } from '../../../util/zoom';
|
||||
import { AnimateCfg } from '../../../types/animate';
|
||||
|
||||
export abstract class BaseNode {
|
||||
type: string;
|
||||
defaultStyles: NodeShapeStyles;
|
||||
themeStyles: NodeShapeStyles;
|
||||
mergedStyles: NodeShapeStyles;
|
||||
zoomStrategy?: ZoomStrategyObj;
|
||||
boundsCache: {
|
||||
keyShapeLocal?: AABB;
|
||||
labelShapeLocal?: AABB;
|
||||
};
|
||||
// cache the zoom level infomations
|
||||
private zoomCache: {
|
||||
// the id of shapes which are hidden by zoom changing.
|
||||
hiddenShape: { [shapeId: string]: boolean };
|
||||
// timeout timer for scaling shapes with to keep the visual size, simulates debounce in function.
|
||||
balanceTimer: NodeJS.Timeout;
|
||||
// the ratio to scale the shapes (e.g. labelShape, labelBackgroundShape) to keep to visual size while zooming.
|
||||
balanceRatio: number;
|
||||
// last responsed zoom ratio.
|
||||
zoom: number;
|
||||
// last responsed zoom level where the zoom ratio at. Zoom ratio 1 at level 0.
|
||||
zoomLevel: number;
|
||||
// shape ids in different zoom levels.
|
||||
levelShapes: {
|
||||
[level: string]: string[];
|
||||
};
|
||||
// wordWrapWidth of labelShape according to the maxWidth
|
||||
wordWrapWidth: number;
|
||||
// animate configurations for zoom level changing
|
||||
animateConfig: AnimateCfg;
|
||||
} = {
|
||||
hiddenShape: {},
|
||||
zoom: 1,
|
||||
zoomLevel: 0,
|
||||
balanceTimer: undefined,
|
||||
balanceRatio: 1,
|
||||
levelShapes: {},
|
||||
wordWrapWidth: 32,
|
||||
animateConfig: DEFAULT_ANIMATE_CFG.zoom,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
const { themeStyles } = props;
|
||||
const { themeStyles, zoomStrategy } = props;
|
||||
if (themeStyles) this.themeStyles = themeStyles;
|
||||
this.zoomStrategy = zoomStrategy;
|
||||
this.boundsCache = {};
|
||||
this.zoomCache.animateConfig = {
|
||||
...DEFAULT_ANIMATE_CFG.zoom,
|
||||
...zoomStrategy?.animateCfg,
|
||||
};
|
||||
}
|
||||
public mergeStyles(model: NodeDisplayModel) {
|
||||
this.mergedStyles = this.getMergedStyles(model);
|
||||
@ -46,11 +84,21 @@ export abstract class BaseNode {
|
||||
const dataStyles = {} as NodeShapeStyles;
|
||||
Object.keys(data).forEach((fieldName) => {
|
||||
if (RESERVED_SHAPE_IDS.includes(fieldName)) {
|
||||
if (fieldName === 'BadgeShapes') {
|
||||
Object.keys(data[fieldName]).forEach(
|
||||
(badgeShapeId) =>
|
||||
(dataStyles[badgeShapeId] = data[fieldName][badgeShapeId]),
|
||||
);
|
||||
if (fieldName === 'badgeShapes') {
|
||||
Object.keys(data[fieldName]).forEach((idx) => {
|
||||
const { position } = data[fieldName][idx];
|
||||
dataStyles[`${position}BadgeShape`] = {
|
||||
...data[fieldName][idx],
|
||||
tag: 'badgeShape',
|
||||
};
|
||||
});
|
||||
} else if (fieldName === 'anchorShapes') {
|
||||
Object.keys(data[fieldName]).forEach((idx) => {
|
||||
dataStyles[`anchorShape${idx}`] = {
|
||||
...data[fieldName][idx],
|
||||
tag: 'anchorShape',
|
||||
};
|
||||
});
|
||||
} else {
|
||||
dataStyles[fieldName] = data[fieldName] as ShapeStyle;
|
||||
}
|
||||
@ -61,7 +109,44 @@ export abstract class BaseNode {
|
||||
);
|
||||
}
|
||||
});
|
||||
return mergeStyles([this.themeStyles, this.defaultStyles, dataStyles]);
|
||||
const merged = mergeStyles([
|
||||
this.themeStyles,
|
||||
this.defaultStyles,
|
||||
dataStyles,
|
||||
]) as NodeShapeStyles;
|
||||
const padding = merged.labelBackgroundShape?.padding;
|
||||
if (padding) {
|
||||
merged.labelBackgroundShape.padding = formatPadding(padding);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
/**
|
||||
* Call it after calling draw function to update cache about bounds and zoom levels.
|
||||
*/
|
||||
public updateCache(shapeMap) {
|
||||
['keyShape', 'labelShape'].forEach((id) => {
|
||||
const shape = shapeMap[id];
|
||||
if (shape?.getAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY)) {
|
||||
this.boundsCache[`${id}Local`] = shape.getLocalBounds();
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, false);
|
||||
}
|
||||
});
|
||||
|
||||
const { levelShapes } = this.zoomCache;
|
||||
Object.keys(shapeMap).forEach((shapeId) => {
|
||||
const { showLevel } = shapeMap[shapeId].attributes;
|
||||
if (showLevel !== undefined) {
|
||||
levelShapes[showLevel] = levelShapes[showLevel] || [];
|
||||
levelShapes[showLevel].push(shapeId);
|
||||
}
|
||||
});
|
||||
|
||||
const { maxWidth = '200%' } = this.mergedStyles.labelShape || {};
|
||||
this.zoomCache.wordWrapWidth = getWordWrapWidthByBox(
|
||||
this.boundsCache.keyShapeLocal,
|
||||
maxWidth,
|
||||
1,
|
||||
);
|
||||
}
|
||||
abstract draw(
|
||||
model: NodeDisplayModel,
|
||||
@ -100,20 +185,27 @@ export abstract class BaseNode {
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { oldState: State[]; newState: State[] },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
const { keyShape } = shapeMap;
|
||||
this.boundsCache.keyShapeLocal =
|
||||
this.boundsCache.keyShapeLocal || keyShape.getLocalBounds();
|
||||
const keyShapeBox = this.boundsCache.keyShapeLocal;
|
||||
const { labelShape: shapeStyle } = this.mergedStyles;
|
||||
|
||||
const {
|
||||
position,
|
||||
offsetX: propsOffsetX,
|
||||
offsetY: propsOffsetY,
|
||||
maxWidth,
|
||||
...otherStyle
|
||||
} = shapeStyle;
|
||||
|
||||
const wordWrapWidth = getWordWrapWidthByBox(
|
||||
keyShapeBox,
|
||||
maxWidth,
|
||||
this.zoomCache.zoom,
|
||||
);
|
||||
|
||||
const positionPreset = {
|
||||
x: keyShapeBox.center[0],
|
||||
y: keyShapeBox.max[1],
|
||||
@ -121,6 +213,7 @@ export abstract class BaseNode {
|
||||
textAlign: 'center',
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
wordWrapWidth,
|
||||
};
|
||||
switch (position) {
|
||||
case 'center':
|
||||
@ -136,14 +229,14 @@ export abstract class BaseNode {
|
||||
positionPreset.y = keyShapeBox.center[1];
|
||||
positionPreset.textAlign = 'right';
|
||||
positionPreset.textBaseline = 'middle';
|
||||
positionPreset.offsetX = -4;
|
||||
positionPreset.offsetX = -8;
|
||||
break;
|
||||
case 'right':
|
||||
positionPreset.x = keyShapeBox.max[0];
|
||||
positionPreset.y = keyShapeBox.center[1];
|
||||
positionPreset.textAlign = 'left';
|
||||
positionPreset.textBaseline = 'middle';
|
||||
positionPreset.offsetX = 4;
|
||||
positionPreset.offsetX = 8;
|
||||
break;
|
||||
default: // at bottom by default
|
||||
positionPreset.offsetY = 4;
|
||||
@ -163,17 +256,7 @@ export abstract class BaseNode {
|
||||
...positionPreset,
|
||||
...otherStyle,
|
||||
};
|
||||
const { shape, updateStyles } = this.upsertShape(
|
||||
'text',
|
||||
'labelShape',
|
||||
style,
|
||||
shapeMap,
|
||||
);
|
||||
|
||||
if (isStyleAffectBBox('text', updateStyles)) {
|
||||
this.boundsCache.labelShapeLocal = undefined;
|
||||
}
|
||||
return shape;
|
||||
return this.upsertShape('text', 'labelShape', style, shapeMap, model);
|
||||
}
|
||||
|
||||
public drawLabelBackgroundShape(
|
||||
@ -184,46 +267,44 @@ export abstract class BaseNode {
|
||||
): DisplayObject {
|
||||
const { labelShape } = shapeMap;
|
||||
if (!labelShape || !model.data.labelShape) return;
|
||||
this.boundsCache.labelShapeLocal =
|
||||
this.boundsCache.labelShapeLocal || labelShape.getLocalBounds();
|
||||
if (
|
||||
!this.boundsCache.labelShapeLocal ||
|
||||
labelShape.getAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY)
|
||||
) {
|
||||
this.boundsCache.labelShapeLocal = labelShape.getLocalBounds();
|
||||
labelShape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, false);
|
||||
}
|
||||
const { labelShapeLocal: textBBox } = this.boundsCache;
|
||||
|
||||
const { padding: propsPadding, ...backgroundStyle } =
|
||||
const { padding, ...backgroundStyle } =
|
||||
this.mergedStyles.labelBackgroundShape;
|
||||
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
|
||||
const { balanceRatio = 1 } = this.zoomCache;
|
||||
const bgStyle: any = {
|
||||
fill: '#fff',
|
||||
...backgroundStyle,
|
||||
x: textBBox.min[0] - padding[3],
|
||||
y: textBBox.min[1] - padding[0],
|
||||
y: textBBox.min[1] - padding[0] / balanceRatio,
|
||||
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
|
||||
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
|
||||
height:
|
||||
textBBox.max[1] -
|
||||
textBBox.min[1] +
|
||||
(padding[0] + padding[2]) / balanceRatio,
|
||||
};
|
||||
const labelShapeAttr = labelShape.attributes;
|
||||
if (labelShapeAttr.transform) {
|
||||
bgStyle.transform = labelShapeAttr.transform;
|
||||
bgStyle.transformOrigin = 'center';
|
||||
if (labelShapeAttr.textAlign === 'left') {
|
||||
bgStyle.transformOrigin = `${padding[3]} ${
|
||||
padding[0] + bgStyle.height / 2
|
||||
}`;
|
||||
}
|
||||
if (labelShapeAttr.textAlign === 'right') {
|
||||
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
|
||||
padding[0] + bgStyle.height / 2
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
return this.upsertShape('rect', 'labelBackgroundShape', bgStyle, shapeMap)
|
||||
.shape;
|
||||
return this.upsertShape(
|
||||
'rect',
|
||||
'labelBackgroundShape',
|
||||
bgStyle,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
public drawIconShape(
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { oldState: State[]; newState: State[] },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
const { iconShape: shapeStyle } = this.mergedStyles;
|
||||
const {
|
||||
@ -255,7 +336,8 @@ export abstract class BaseNode {
|
||||
'iconShape',
|
||||
shapeStyle as GShapeStyle,
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
public drawHaloShape(
|
||||
@ -276,7 +358,8 @@ export abstract class BaseNode {
|
||||
isBillboard: true,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
|
||||
public drawAnchorShapes(
|
||||
@ -287,19 +370,12 @@ export abstract class BaseNode {
|
||||
): {
|
||||
[shapeId: string]: DisplayObject;
|
||||
} {
|
||||
const { anchorShapes: configs, keyShape: keyShapeStyle } =
|
||||
const { anchorShapes: commonStyle, keyShape: keyShapeStyle } =
|
||||
this.mergedStyles;
|
||||
const commonStyle = {};
|
||||
const individualConfigs = [];
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const value = configs[key];
|
||||
if (typeof value === 'object' && value.position) {
|
||||
individualConfigs.push(value);
|
||||
} else {
|
||||
commonStyle[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
const individualConfigs = Object.values(this.mergedStyles).filter(
|
||||
(style) => style.tag === 'anchorShape',
|
||||
);
|
||||
if (!individualConfigs.length) return;
|
||||
this.boundsCache.keyShapeLocal =
|
||||
this.boundsCache.keyShapeLocal || shapeMap.keyShape.getLocalBounds();
|
||||
@ -322,7 +398,8 @@ export abstract class BaseNode {
|
||||
...style,
|
||||
} as GShapeStyle,
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
});
|
||||
return shapes;
|
||||
}
|
||||
@ -335,18 +412,10 @@ export abstract class BaseNode {
|
||||
): {
|
||||
[shapeId: string]: DisplayObject;
|
||||
} {
|
||||
const configs = this.mergedStyles.badgeShapes;
|
||||
const commonStyle = {};
|
||||
const individualConfigs = [];
|
||||
Object.keys(configs).forEach((key) => {
|
||||
const value = configs[key];
|
||||
if (typeof value === 'object' && value.position) {
|
||||
individualConfigs.push(value);
|
||||
} else {
|
||||
commonStyle[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
const commonStyle = this.mergedStyles.badgeShapes;
|
||||
const individualConfigs = Object.values(this.mergedStyles).filter(
|
||||
(style) => style.tag === 'badgeShape',
|
||||
);
|
||||
if (!individualConfigs.length) return {};
|
||||
this.boundsCache.keyShapeLocal =
|
||||
this.boundsCache.keyShapeLocal || shapeMap.keyShape.getLocalBounds();
|
||||
@ -430,7 +499,8 @@ export abstract class BaseNode {
|
||||
zIndex: (zIndex as number) + 1,
|
||||
} as GShapeStyle,
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
const bbox = shapes[id].getLocalBounds();
|
||||
const bgShapeId = `${position}BadgeBackgroundShape`;
|
||||
const bgWidth =
|
||||
@ -452,7 +522,8 @@ export abstract class BaseNode {
|
||||
...otherStyles,
|
||||
} as GShapeStyle,
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
});
|
||||
|
||||
return shapes;
|
||||
@ -467,15 +538,115 @@ export abstract class BaseNode {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener for graph zooming.
|
||||
* 1. show / hide some shapes while zoom level changed;
|
||||
* 2. change the shapes' sizes to make them have same visual size while zooming, e.g. labelShape, labelBackgroundShape.
|
||||
* @param shapeMap
|
||||
* @param zoom
|
||||
*/
|
||||
public onZoom = (shapeMap: NodeShapeMap, zoom: number) => {
|
||||
this.balanceShapeSize(shapeMap, zoom);
|
||||
// zoomLevel changed
|
||||
if (!this.zoomStrategy) return;
|
||||
const { levels } = this.zoomStrategy;
|
||||
// last zoom ratio responsed by zoom changing, which might not equal to zoom.previous in props since the function is debounced.
|
||||
const {
|
||||
levelShapes,
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
zoomLevel: previousLevel,
|
||||
} = this.zoomCache;
|
||||
const currentLevel = getZoomLevel(levels, zoom);
|
||||
if (currentLevel < previousLevel) {
|
||||
// zoomLevel changed, from higher to lower, hide something
|
||||
levelShapes[currentLevel + 1]?.forEach((id) =>
|
||||
fadeOut(id, shapeMap[id], hiddenShape, animateConfig),
|
||||
);
|
||||
} else if (currentLevel > previousLevel) {
|
||||
// zoomLevel changed, from lower to higher, show something
|
||||
levelShapes[String(currentLevel)]?.forEach((id) =>
|
||||
fadeIn(
|
||||
id,
|
||||
shapeMap[id],
|
||||
this.mergedStyles[id] ||
|
||||
this.mergedStyles[id.replace('Background', '')],
|
||||
hiddenShape,
|
||||
animateConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
this.zoomCache.zoomLevel = currentLevel;
|
||||
this.zoomCache.zoom = zoom;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the shapes' sizes e.g. labelShape, labelBackgroundShape, to keep the visual size while zooming.
|
||||
* @param shapeMap
|
||||
* @param zoom
|
||||
* @returns
|
||||
*/
|
||||
private balanceShapeSize(shapeMap: NodeShapeMap, zoom: number) {
|
||||
// balance the size for label, badges
|
||||
const { labelShape, labelBackgroundShape } = shapeMap;
|
||||
const balanceRatio = 1 / zoom || 1;
|
||||
this.zoomCache.balanceRatio = balanceRatio;
|
||||
const { labelShape: labelStyle } = this.mergedStyles;
|
||||
const { position = 'bottom' } = labelStyle;
|
||||
if (!labelShape) return;
|
||||
|
||||
if (position === 'bottom') labelShape.style.transformOrigin = '0';
|
||||
else labelShape.style.transformOrigin = '';
|
||||
labelShape.style.transform = `scale(${balanceRatio}, ${balanceRatio})`;
|
||||
const wordWrapWidth = this.zoomCache.wordWrapWidth * zoom;
|
||||
labelShape.style.wordWrapWidth = wordWrapWidth;
|
||||
|
||||
if (!labelBackgroundShape) return;
|
||||
|
||||
const { padding } = this.mergedStyles.labelBackgroundShape;
|
||||
const { width, height } = labelBackgroundShape.attributes;
|
||||
const [paddingTop, paddingRight, paddingBottom, paddingLeft] =
|
||||
padding as number[];
|
||||
|
||||
switch (position) {
|
||||
case 'top':
|
||||
labelBackgroundShape.style.transformOrigin = `${
|
||||
paddingLeft + (width - paddingLeft - paddingRight) / 2
|
||||
} ${height - paddingBottom}`;
|
||||
break;
|
||||
case 'left':
|
||||
labelBackgroundShape.style.transformOrigin = `${width - paddingRight} ${
|
||||
paddingTop + (height - paddingTop - paddingBottom) / 2
|
||||
}`;
|
||||
break;
|
||||
case 'right':
|
||||
labelBackgroundShape.style.transformOrigin = `${paddingLeft} ${
|
||||
paddingTop + (height - paddingTop - paddingBottom) / 2
|
||||
}`;
|
||||
break;
|
||||
case 'bottom':
|
||||
default:
|
||||
labelBackgroundShape.style.transformOrigin = `${
|
||||
paddingLeft + (width - paddingLeft - paddingRight) / 2
|
||||
} ${paddingTop + (height - paddingTop - paddingBottom) / 2}`;
|
||||
}
|
||||
// only scale y-asix, to expand the text range while zoom-in
|
||||
labelBackgroundShape.style.transform = `scale(1, ${balanceRatio})`;
|
||||
}
|
||||
|
||||
public upsertShape(
|
||||
type: SHAPE_TYPE | SHAPE_TYPE_3D,
|
||||
id: string,
|
||||
style: ShapeStyle,
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
): {
|
||||
updateStyles: ShapeStyle;
|
||||
shape: DisplayObject;
|
||||
} {
|
||||
return upsertShape(type as SHAPE_TYPE, id, style as GShapeStyle, shapeMap);
|
||||
shapeMap: NodeShapeMap,
|
||||
model: NodeDisplayModel,
|
||||
): DisplayObject {
|
||||
return upsertShape(
|
||||
type as SHAPE_TYPE,
|
||||
id,
|
||||
style as GShapeStyle,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { oldState: State[]; newState: State[] },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
return super.drawLabelShape(model, shapeMap, diffData, diffState);
|
||||
}
|
||||
@ -43,7 +43,7 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
model: NodeDisplayModel,
|
||||
shapeMap: NodeShapeMap,
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { oldState: State[]; newState: State[] },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
return super.drawIconShape(model, shapeMap, diffData, diffState);
|
||||
}
|
||||
@ -67,7 +67,7 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
isBillboard: true,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,10 +116,7 @@ export abstract class BaseNode3D extends BaseNode {
|
||||
id: string,
|
||||
style: ShapeStyle,
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
): {
|
||||
shape: DisplayObject;
|
||||
updateStyles: ShapeStyle;
|
||||
} {
|
||||
): DisplayObject {
|
||||
return upsertShape3D(type, id, style as GShapeStyle, shapeMap, this.device);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
NodeShapeStyles,
|
||||
} from '../../../types/node';
|
||||
import { BaseNode } from './base';
|
||||
import { isStyleAffectBBox } from 'util/shape';
|
||||
|
||||
export class CircleNode extends BaseNode {
|
||||
override defaultStyles = {
|
||||
@ -103,15 +102,12 @@ export class CircleNode extends BaseNode {
|
||||
diffData?: { previous: NodeModelData; current: NodeModelData },
|
||||
diffState?: { previous: State[]; current: State[] },
|
||||
): DisplayObject {
|
||||
const { shape, updateStyles } = this.upsertShape(
|
||||
return this.upsertShape(
|
||||
'circle',
|
||||
'keyShape',
|
||||
this.mergedStyles.keyShape,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
if (isStyleAffectBBox('circle', updateStyles)) {
|
||||
this.boundsCache.keyShapeLocal = shape.getLocalBounds();
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,6 @@ export class SphereNode extends BaseNode3D {
|
||||
'keyShape',
|
||||
this.mergedStyles.keyShape,
|
||||
shapeMap,
|
||||
).shape;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,18 @@ export default {
|
||||
'#5241A8',
|
||||
'#95CF21',
|
||||
],
|
||||
zoomStrategy: {
|
||||
levels: [
|
||||
{ range: [0, 0.65] },
|
||||
{ range: [0.65, 0.8] },
|
||||
{ range: [0.8, 1.6], primary: true },
|
||||
{ range: [1.6, 2] },
|
||||
{ range: [2, Infinity] },
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 200,
|
||||
},
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
default: {
|
||||
@ -44,32 +56,40 @@ export default {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: '#000',
|
||||
position: 'bottom',
|
||||
offsetY: 4,
|
||||
zIndex: 2,
|
||||
showLevel: 0,
|
||||
maxWidth: '200%',
|
||||
textOverflow: 'ellipsis',
|
||||
wordWrap: true,
|
||||
maxLines: 1,
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
padding: [4, 4, 4, 4],
|
||||
padding: [2, 4, 2, 4],
|
||||
lineWidth: 0,
|
||||
fill: '#fff',
|
||||
opacity: 0.75,
|
||||
zIndex: -1,
|
||||
showLevel: 0,
|
||||
},
|
||||
iconShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
fill: '#fff',
|
||||
fontSize: 16,
|
||||
zIndex: 1,
|
||||
showLevel: -1,
|
||||
},
|
||||
anchorShapes: {
|
||||
lineWidth: 1,
|
||||
stroke: 'rgba(0, 0, 0, 0.65)',
|
||||
zIndex: 2,
|
||||
r: 3,
|
||||
showLevel: 0,
|
||||
},
|
||||
badgeShapes: {
|
||||
color: 'rgb(140, 140, 140)',
|
||||
textColor: '#fff',
|
||||
zIndex: 2,
|
||||
zIndex: 3,
|
||||
showLevel: -1,
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
@ -140,6 +160,18 @@ export default {
|
||||
'#A192E8',
|
||||
'#CEFB75',
|
||||
],
|
||||
zoomStrategy: {
|
||||
levels: [
|
||||
{ range: [0, 0.65] },
|
||||
{ range: [0.65, 0.8] },
|
||||
{ range: [0.8, 1.6], primary: true },
|
||||
{ range: [1.6, 2] },
|
||||
{ range: [2, Infinity] },
|
||||
],
|
||||
animateCfg: {
|
||||
duration: 200,
|
||||
},
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
default: {
|
||||
@ -155,6 +187,11 @@ export default {
|
||||
position: 'middle',
|
||||
textBaseline: 'middle',
|
||||
zIndex: 2,
|
||||
textOverflow: 'ellipsis',
|
||||
wordWrap: true,
|
||||
maxLines: 1,
|
||||
maxWidth: '60%',
|
||||
showLevel: 0,
|
||||
},
|
||||
labelBackgroundShape: {
|
||||
padding: [4, 4, 4, 4],
|
||||
@ -162,6 +199,7 @@ export default {
|
||||
fill: '#fff',
|
||||
opacity: 0.75,
|
||||
zIndex: 1,
|
||||
showLevel: 0,
|
||||
},
|
||||
iconShape: {
|
||||
...DEFAULT_TEXT_STYLE,
|
||||
@ -169,6 +207,7 @@ export default {
|
||||
fontSize: 16,
|
||||
zIndex: 2,
|
||||
offsetX: -10,
|
||||
showLevel: -1,
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
@ -278,6 +317,6 @@ export default {
|
||||
],
|
||||
},
|
||||
canvas: {
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
} as ThemeSpecification;
|
||||
|
@ -6,7 +6,6 @@ import {
|
||||
} from '../../types/theme';
|
||||
import { mergeStyles } from '../../util/shape';
|
||||
import BaseThemeSolver, { ThemeSpecificationMap } from './base';
|
||||
import { GraphData } from 'types';
|
||||
|
||||
interface SpecThemeSolverOptions {
|
||||
base: 'light' | 'dark';
|
||||
@ -47,6 +46,7 @@ export default class SpecThemeSolver extends BaseThemeSolver {
|
||||
if (!specification[itemType]) return;
|
||||
let {
|
||||
palette = mergedSpec[itemType].palette,
|
||||
zoomStrategy = mergedSpec[itemType].zoomStrategy,
|
||||
dataTypeField,
|
||||
getStyleSets,
|
||||
} = specification[itemType];
|
||||
@ -96,6 +96,7 @@ export default class SpecThemeSolver extends BaseThemeSolver {
|
||||
mergedSpec[itemType] = {
|
||||
dataTypeField,
|
||||
palette,
|
||||
zoomStrategy,
|
||||
styles: mergedStyles,
|
||||
};
|
||||
});
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { IAnimationEffectTiming } from '@antv/g';
|
||||
|
||||
export interface AnimateCfg {
|
||||
/**
|
||||
* Whether enable animation.
|
||||
* @type {boolean}
|
||||
*/
|
||||
enable: boolean;
|
||||
/**
|
||||
* Duration of one animation.
|
||||
* @type {number}
|
||||
@ -22,10 +17,10 @@ export interface AnimateCfg {
|
||||
*/
|
||||
delay?: number;
|
||||
/**
|
||||
* Whether repeat.
|
||||
* @type {boolean}
|
||||
* Iteration number for the animation, Inifinity means repeat.
|
||||
* @type {number | typeof Infinity}
|
||||
*/
|
||||
repeat?: boolean;
|
||||
iterations?: number | typeof Infinity;
|
||||
/**
|
||||
* Called after the animation is finished.
|
||||
* @type {function}
|
||||
@ -43,12 +38,32 @@ export interface AnimateCfg {
|
||||
resumeCallback?: () => void;
|
||||
}
|
||||
|
||||
export type AnimateWhen = 'show' | 'exit' | 'update' | 'last';
|
||||
export type AnimateTiming = 'buildIn' | 'buildOut' | 'show' | 'hide' | 'update';
|
||||
|
||||
export interface AnimateAttr {
|
||||
when: AnimateWhen;
|
||||
type: string;
|
||||
[param: string]: unknown;
|
||||
export interface IAnimate {
|
||||
// style fields to animate
|
||||
fields?: string[];
|
||||
// shapeId for the animate, 'group' by default, means animation on whole graphics group
|
||||
shapeId?: string;
|
||||
// the order of the animate, 0 by dfault
|
||||
order?: number;
|
||||
// animate options
|
||||
duration?: number;
|
||||
interations?: number;
|
||||
easing?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export interface IStateAnimate extends IAnimate {
|
||||
states: string[];
|
||||
}
|
||||
|
||||
export interface IAnimates {
|
||||
buildIn?: IAnimate[];
|
||||
buildOut?: IAnimate[];
|
||||
show?: IAnimate[];
|
||||
hide?: IAnimate[];
|
||||
update?: (IAnimate | IStateAnimate)[];
|
||||
}
|
||||
|
||||
export type CameraAnimationOptions = Pick<
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Node as GNode, PlainObject } from '@antv/graphlib';
|
||||
import { AnimateAttr } from './animate';
|
||||
import { IAnimates } from './animate';
|
||||
import { Padding } from './common';
|
||||
import {
|
||||
BadgePosition,
|
||||
@ -107,6 +107,7 @@ export interface ComboShapesEncode extends ShapesEncode {
|
||||
}
|
||||
export interface ComboEncode extends ComboShapesEncode {
|
||||
type?: string | Encode<string>;
|
||||
animates?: IAnimates;
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DisplayObject } from '@antv/g';
|
||||
import { Edge as GEdge, PlainObject } from '@antv/graphlib';
|
||||
import { IAnimates } from './animate';
|
||||
import {
|
||||
BadgePosition,
|
||||
Encode,
|
||||
@ -61,6 +62,8 @@ export interface EdgeShapeStyles extends ItemShapeStyles {
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
autoRotate?: boolean;
|
||||
// if it is a string, means the percentage of the keyShape, number means pixel
|
||||
maxWidth?: string | number;
|
||||
};
|
||||
labelBackgroundShape?: ShapeStyle & {
|
||||
padding?: number | number[];
|
||||
@ -98,6 +101,7 @@ export interface EdgeShapesEncode extends ShapesEncode {
|
||||
}
|
||||
export interface EdgeEncode extends EdgeShapesEncode {
|
||||
type?: string | Encode<string>;
|
||||
animates?: IAnimates;
|
||||
}
|
||||
|
||||
export interface EdgeShapeMap {
|
||||
|
@ -205,6 +205,22 @@ export interface IGraph<
|
||||
| EdgeModel[]
|
||||
| ComboModel[];
|
||||
|
||||
/**
|
||||
* Update one or more nodes' positions,
|
||||
* do not update other styles which leads to better performance than updating positions by updateData.
|
||||
* @param models new configurations with x and y for every node, which has id field to indicate the specific item
|
||||
* @param {boolean} stack whether push this operation into graph's stack, true by default
|
||||
* @group Data
|
||||
*/
|
||||
updateNodePosition: (
|
||||
models:
|
||||
| Partial<NodeUserModel>
|
||||
| Partial<
|
||||
ComboUserModel | Partial<NodeUserModel>[] | Partial<ComboUserModel>[]
|
||||
>,
|
||||
stack?: boolean,
|
||||
) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
|
||||
|
||||
// ===== view operations =====
|
||||
|
||||
/**
|
||||
@ -378,14 +394,14 @@ export interface IGraph<
|
||||
* @returns
|
||||
* @group Data
|
||||
*/
|
||||
showItem: (ids: ID | ID[]) => void;
|
||||
showItem: (ids: ID | ID[], disableAniamte?: boolean) => void;
|
||||
/**
|
||||
* Hide the item(s).
|
||||
* @param ids the item id(s) to be hidden
|
||||
* @returns
|
||||
* @group Item
|
||||
*/
|
||||
hideItem: (ids: ID | ID[]) => void;
|
||||
hideItem: (ids: ID | ID[], disableAniamte?: boolean) => void;
|
||||
/**
|
||||
* Set state for the item(s).
|
||||
* @param ids the id(s) for the item(s) to be set
|
||||
|
@ -42,6 +42,7 @@ export interface Hooks {
|
||||
changes: GraphChange<NodeModelData, EdgeModelData>[];
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
action?: 'updateNodePosition';
|
||||
}>;
|
||||
render: IHook<{
|
||||
graphCore: GraphCore;
|
||||
@ -64,6 +65,7 @@ export interface Hooks {
|
||||
itemvisibilitychange: IHook<{
|
||||
ids: ID[];
|
||||
value?: boolean;
|
||||
animate?: boolean;
|
||||
}>;
|
||||
transientupdate: IHook<{
|
||||
type: ITEM_TYPE | SHAPE_TYPE;
|
||||
|
@ -10,8 +10,11 @@ import {
|
||||
PolylineStyleProps,
|
||||
TextStyleProps,
|
||||
ImageStyleProps,
|
||||
Group,
|
||||
DisplayObject,
|
||||
IAnimation,
|
||||
} from '@antv/g';
|
||||
import { AnimateAttr } from './animate';
|
||||
import { AnimateCfg, IAnimates } from './animate';
|
||||
import {
|
||||
ComboDisplayModel,
|
||||
ComboEncode,
|
||||
@ -24,6 +27,7 @@ import {
|
||||
EdgeEncode,
|
||||
EdgeModel,
|
||||
EdgeModelData,
|
||||
EdgeShapeMap,
|
||||
EdgeUserModel,
|
||||
} from './edge';
|
||||
import {
|
||||
@ -31,6 +35,7 @@ import {
|
||||
NodeEncode,
|
||||
NodeModel,
|
||||
NodeModelData,
|
||||
NodeShapeMap,
|
||||
NodeUserModel,
|
||||
} from './node';
|
||||
import {
|
||||
@ -55,7 +60,8 @@ export type GShapeStyle = CircleStyleProps &
|
||||
|
||||
export type ShapeStyle = Partial<
|
||||
GShapeStyle & {
|
||||
animate?: AnimateAttr;
|
||||
animates?: IAnimates;
|
||||
showLevel?: number;
|
||||
}
|
||||
>;
|
||||
export interface Encode<T> {
|
||||
@ -65,7 +71,7 @@ export interface Encode<T> {
|
||||
|
||||
export interface ShapeAttrEncode {
|
||||
[shapeAttr: string]: unknown | Encode<unknown>;
|
||||
animate?: AnimateAttr | Encode<AnimateAttr>;
|
||||
animates?: IAnimates | Encode<IAnimates>;
|
||||
}
|
||||
|
||||
export interface LabelBackground {
|
||||
@ -82,7 +88,7 @@ export interface ShapesEncode {
|
||||
otherShapes?: {
|
||||
[shapeId: string]: {
|
||||
[shapeAtrr: string]: unknown | Encode<unknown>;
|
||||
animate: AnimateAttr | Encode<AnimateAttr>;
|
||||
animates: IAnimates | Encode<IAnimates>;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -118,7 +124,7 @@ export type DisplayMapper =
|
||||
|
||||
export type State = {
|
||||
name: string;
|
||||
value: boolean | string;
|
||||
value: boolean | string | number;
|
||||
};
|
||||
|
||||
export enum BadgePosition {
|
||||
@ -147,11 +153,32 @@ export type ItemShapeStyles = {
|
||||
ImageStyleProps & {
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
showLevel?: number;
|
||||
}
|
||||
>;
|
||||
haloShape?: ShapeStyle;
|
||||
group?: ShapeStyle;
|
||||
otherShapes?: {
|
||||
[shapeId: string]: ShapeStyle;
|
||||
};
|
||||
animates?: IAnimates;
|
||||
};
|
||||
|
||||
export interface ZoomStrategy {
|
||||
levels: {
|
||||
range: [number, number];
|
||||
primary: boolean;
|
||||
}[];
|
||||
animateCfg: AnimateCfg;
|
||||
}
|
||||
|
||||
export interface ZoomStrategyObj {
|
||||
levels: {
|
||||
[levelIdx: number]: [number, number];
|
||||
};
|
||||
animateCfg: AnimateCfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base item of node / edge / combo.
|
||||
*/
|
||||
@ -159,25 +186,58 @@ export interface IItem {
|
||||
destroyed: boolean;
|
||||
/** Inner model. */
|
||||
model: ItemModel;
|
||||
// /** Display model, user will not touch it. */
|
||||
// displayModel: ItemDisplayModel;
|
||||
// /** The graphic group for item drawing. */
|
||||
// group: Group;
|
||||
// /** Visibility. */
|
||||
// visible: boolean;
|
||||
// /** The states on the item. */
|
||||
// states: {
|
||||
// name: string,
|
||||
// value: string | boolean
|
||||
// }[];
|
||||
// type: 'node' | 'edge' | 'combo';
|
||||
/** Display model, user will not touch it. */
|
||||
displayModel: ItemDisplayModel;
|
||||
/** The style mapper configured at graph with field name 'node' / 'edge' / 'combo'. */
|
||||
mapper: DisplayMapper;
|
||||
/** The state sstyle mapper configured at traph with field name 'nodeState' / 'edgeState' / 'comboState'. */
|
||||
stateMapper: {
|
||||
[stateName: string]: DisplayMapper;
|
||||
};
|
||||
/** The graphic group for item drawing. */
|
||||
group: Group;
|
||||
/** The keyShape of the item. */
|
||||
keyShape: DisplayObject;
|
||||
/** render extension for this item. */
|
||||
renderExt;
|
||||
/** Visibility. */
|
||||
visible: boolean;
|
||||
/** The states on the item. */
|
||||
states: {
|
||||
name: string;
|
||||
value: string | boolean;
|
||||
}[];
|
||||
/** The map caches the shapes of the item. The key is the shape id, the value is the g shape. */
|
||||
shapeMap: NodeShapeMap | EdgeShapeMap;
|
||||
afterDrawShapeMap: Object;
|
||||
/** Item's type. Set to different value in implements. */
|
||||
type: ITEM_TYPE;
|
||||
/** Render extensions where could the renderExt be selected from according to the type. */
|
||||
renderExtensions: any;
|
||||
/** Cache the animation instances to stop at next lifecycle. */
|
||||
animations: IAnimation[];
|
||||
/** Theme styles to response the state changes. */
|
||||
themeStyles: {
|
||||
default?: ItemShapeStyles;
|
||||
[stateName: string]: ItemShapeStyles;
|
||||
};
|
||||
/** The zoom strategy to show and hide shapes according to their showLevel. */
|
||||
zoomStrategy: ZoomStrategyObj;
|
||||
/** Last zoom ratio. */
|
||||
zoom: number;
|
||||
/** Cache the chaging states which are not consomed by draw */
|
||||
changedStates: string[];
|
||||
/** The listener for the animations frames. */
|
||||
onframe: Function;
|
||||
|
||||
/** Gets the inner model. */
|
||||
// getModel: () => ItemModel;
|
||||
/** Gets the id in model. */
|
||||
getID: () => ID;
|
||||
/** Gets the item's type. */
|
||||
getType: () => 'node' | 'edge' | 'combo';
|
||||
getType: () => ITEM_TYPE;
|
||||
/** Initiate the item. */
|
||||
init: (props) => void;
|
||||
/**
|
||||
* Draws the shapes.
|
||||
* @internal
|
||||
@ -196,16 +256,45 @@ export interface IItem {
|
||||
diffData: { previous: ItemModelData; current: ItemModelData },
|
||||
isUpdate?: boolean,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Update the group's position, e.g. node, combo.
|
||||
* @param displayModel
|
||||
* @param diffData
|
||||
* @param onfinish
|
||||
* @returns
|
||||
*/
|
||||
updatePosition: (
|
||||
displayModel: ItemDisplayModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
onfinish?: Function,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Maps (mapper will be function, value, or encode format) model to displayModel and find out the shapes to be update for incremental updating.
|
||||
* @param model inner model
|
||||
* @param diffData changes from graphCore changed event
|
||||
* @param isReplace whether replace the whole data or partial update
|
||||
* @returns
|
||||
*/
|
||||
getDisplayModelAndChanges: (
|
||||
innerModel: ItemModel,
|
||||
diffData?: { previous: ItemModelData; current: ItemModelData },
|
||||
isReplace?: boolean,
|
||||
) => {
|
||||
model: ItemDisplayModel;
|
||||
typeChange?: boolean;
|
||||
};
|
||||
/** Show the item. */
|
||||
show: (animate: boolean) => void;
|
||||
/** Hides the item. */
|
||||
hide: (animate: boolean) => void;
|
||||
/** Returns the visibility of the item. */
|
||||
isVisible: () => boolean;
|
||||
/** Puts the item to the front in its graphic group. */
|
||||
toFront: () => void;
|
||||
/** Puts the item to the back in its graphic group. */
|
||||
toBack: () => void;
|
||||
/** Showsthe item. */
|
||||
show: () => void;
|
||||
/** Hides the item. */
|
||||
hide: () => void;
|
||||
/** Returns the visibility of the item. */
|
||||
isVisible: () => boolean;
|
||||
/** Sets a state value to the item. */
|
||||
setState: (state: string, value: string | boolean) => void;
|
||||
/** Returns the state if it is true/string. Returns false otherwise. */
|
||||
@ -218,9 +307,17 @@ export interface IItem {
|
||||
/** Set all the state to false. */
|
||||
clearStates: (states?: string[]) => void;
|
||||
/** Get the rendering bounding box for the keyShape. */
|
||||
getKeyBBox(): AABB;
|
||||
getKeyBBox: () => AABB;
|
||||
/** Get the local bounding box for the keyShape. */
|
||||
getLocalKeyBBox: () => AABB;
|
||||
/** Get the rendering bounding box for the whole item. */
|
||||
getBBox(): AABB;
|
||||
getBBox: () => AABB;
|
||||
/** Stop all the animations on the item. */
|
||||
stopAnimations: () => void;
|
||||
/** Animations' frame listemer. */
|
||||
animateFrameListener: Function;
|
||||
/** Call render extension's onZoom to response the graph zooming. */
|
||||
updateZoom: (zoom: number) => void;
|
||||
/** Destroy the item. */
|
||||
destroy: () => void;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DisplayObject, Point } from '@antv/g';
|
||||
import { Node as GNode, PlainObject } from '@antv/graphlib';
|
||||
import { AnimateAttr } from './animate';
|
||||
import { IAnimates } from './animate';
|
||||
import {
|
||||
BadgePosition,
|
||||
Encode,
|
||||
@ -78,6 +78,8 @@ export interface NodeShapeStyles extends ItemShapeStyles {
|
||||
position?: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
// string means the percentage of the keyShape, number means pixel
|
||||
maxWidth?: string | number;
|
||||
};
|
||||
labelBackgroundShape?: ShapeStyle & {
|
||||
padding?: number | number[];
|
||||
@ -138,6 +140,7 @@ export interface NodeShapesEncode extends ShapesEncode {
|
||||
}
|
||||
export interface NodeEncode extends NodeShapesEncode {
|
||||
type?: string | Encode<string>;
|
||||
animates?: IAnimates;
|
||||
}
|
||||
|
||||
export interface NodeShapeMap {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ComboShapeStyles } from './combo';
|
||||
import { EdgeShapeStyles } from './edge';
|
||||
import { ZoomStrategy } from './item';
|
||||
import { NodeShapeStyles } from './node';
|
||||
|
||||
export interface ThemeOption {}
|
||||
@ -72,16 +73,19 @@ export interface NodeThemeSpecifications {
|
||||
dataTypeField?: string;
|
||||
palette?: string[] | { [dataTypeValue: string]: string };
|
||||
styles?: NodeStyleSets;
|
||||
zoomStrategy?: ZoomStrategy;
|
||||
}
|
||||
export interface EdgeThemeSpecifications {
|
||||
dataTypeField?: string;
|
||||
palette?: string[] | { [dataTypeValue: string]: string };
|
||||
styles?: EdgeStyleSets;
|
||||
zoomStrategy?: ZoomStrategy;
|
||||
}
|
||||
export interface ComboThemeSpecifications {
|
||||
dataTypeField?: string;
|
||||
palette?: string[] | { [dataTypeValue: string]: string };
|
||||
styles?: ComboStyleSets;
|
||||
zoomStrategy?: ZoomStrategy;
|
||||
}
|
||||
/**
|
||||
* Theme specification
|
||||
|
423
packages/g6/src/util/animate.ts
Normal file
423
packages/g6/src/util/animate.ts
Normal file
@ -0,0 +1,423 @@
|
||||
import {
|
||||
DisplayObject,
|
||||
Group,
|
||||
IAnimation,
|
||||
Line,
|
||||
Path,
|
||||
Polyline,
|
||||
} from '@antv/g';
|
||||
import {
|
||||
AnimateTiming,
|
||||
IAnimate,
|
||||
IAnimates,
|
||||
IStateAnimate,
|
||||
} from '../types/animate';
|
||||
import { ItemShapeStyles, ShapeStyle } from '../types/item';
|
||||
import { isArrayOverlap, replaceElements } from './array';
|
||||
|
||||
/**
|
||||
* Initial(timing = show) shape animation start from init shape styles, and end to the shape's style config.
|
||||
*/
|
||||
export const getShapeAnimateBeginStyles = (shape) => {
|
||||
if (!shape) return {};
|
||||
const shapeType = shape.nodeName;
|
||||
const commonStyles = {
|
||||
opacity: 0,
|
||||
strokeOpacity: 0,
|
||||
lineWidth: 1,
|
||||
offsetDistance: 0,
|
||||
};
|
||||
if (['line', 'polyline', 'path'].includes(shapeType)) {
|
||||
const totalLength = shape.getTotalLength();
|
||||
return {
|
||||
lineDash: [0, totalLength],
|
||||
...commonStyles,
|
||||
};
|
||||
} else if (shapeType === 'circle') {
|
||||
return {
|
||||
r: 0,
|
||||
...commonStyles,
|
||||
};
|
||||
} else if (shapeType === 'ellipse') {
|
||||
return {
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
...commonStyles,
|
||||
};
|
||||
} else if (shapeType === 'text') {
|
||||
return {
|
||||
fontSize: 0,
|
||||
opacity: 0,
|
||||
strokeOpacity: 0,
|
||||
offsetDistance: 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
...commonStyles,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Initial(timing = show) group animation start from GROUP_ANIMATE_STYLES[0], and end to GROUP_ANIMATE_STYLES[1].
|
||||
*/
|
||||
export const GROUP_ANIMATE_STYLES = [
|
||||
{
|
||||
opacity: 0,
|
||||
transform: 'scale(0)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Default animate options for different timing.
|
||||
*/
|
||||
export const DEFAULT_ANIMATE_CFG = {
|
||||
buildIn: {
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
||||
iterations: 1,
|
||||
delay: 1000,
|
||||
fill: 'both',
|
||||
},
|
||||
show: {
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
||||
iterations: 1,
|
||||
fill: 'both',
|
||||
},
|
||||
update: {
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
||||
iterations: 1,
|
||||
fill: 'both',
|
||||
},
|
||||
zoom: {
|
||||
duration: 200,
|
||||
easing: 'linear',
|
||||
iterations: 1,
|
||||
delay: 0,
|
||||
fill: 'both',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get different key value map between style1 and style2.
|
||||
* @param style1
|
||||
* @param style2
|
||||
* @returns
|
||||
*/
|
||||
const getStyleDiff = (style1: ShapeStyle, style2: ShapeStyle) => {
|
||||
const diff = [{}, {}];
|
||||
Object.keys(style1).forEach((key) => {
|
||||
if (style2[key] !== style1[key]) {
|
||||
diff[0][key] = style1[key];
|
||||
diff[1][key] = style2[key];
|
||||
}
|
||||
});
|
||||
return diff;
|
||||
};
|
||||
|
||||
/**
|
||||
* Grouping the animates at a timing by order.
|
||||
* @param animates
|
||||
* @param timing
|
||||
* @returns
|
||||
*/
|
||||
const groupTimingAnimates = (
|
||||
animates: IAnimates,
|
||||
segmentedTiming: AnimateTiming | 'stateUpdate',
|
||||
changedStates: string[],
|
||||
) => {
|
||||
const timingAnimateGroups = {};
|
||||
const isStateUpdate = segmentedTiming === 'stateUpdate';
|
||||
animates[isStateUpdate ? 'update' : segmentedTiming].forEach((item: any) => {
|
||||
const { order = 0, states } = item;
|
||||
if (
|
||||
!isStateUpdate ||
|
||||
(isStateUpdate && isArrayOverlap(states, changedStates))
|
||||
) {
|
||||
timingAnimateGroups[order] = timingAnimateGroups[order] || [];
|
||||
timingAnimateGroups[order].push(item);
|
||||
}
|
||||
});
|
||||
return timingAnimateGroups;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute animations in order at a timing.
|
||||
* @param shapeMap
|
||||
* @param group
|
||||
* @param timingAnimates
|
||||
* @param targetStylesMap
|
||||
* @param timing
|
||||
* @param onfinish
|
||||
*/
|
||||
const runAnimateGroupOnShapes = (
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
group: Group,
|
||||
timingAnimates: IAnimate[],
|
||||
targetStylesMap: ItemShapeStyles,
|
||||
timing: AnimateTiming,
|
||||
onfinish: Function,
|
||||
cancelAnimations: Function,
|
||||
canceled: Boolean,
|
||||
) => {
|
||||
let maxDuration = -Infinity;
|
||||
let maxDurationIdx = -1;
|
||||
let hasCanceled = canceled;
|
||||
const isOut = timing === 'buildOut' || timing === 'hide';
|
||||
const animations = timingAnimates.map((animate: any, i) => {
|
||||
const { fields, shapeId, order, states, ...animateCfg } = animate;
|
||||
const animateConfig = {
|
||||
...DEFAULT_ANIMATE_CFG[timing],
|
||||
...animateCfg,
|
||||
};
|
||||
const { duration } = animateConfig;
|
||||
|
||||
let animation;
|
||||
if (!shapeId || shapeId === 'group') {
|
||||
// animate on group
|
||||
const usingFields = [];
|
||||
let hasOpacity = false;
|
||||
fields?.forEach((field) => {
|
||||
if (field === 'size') usingFields.push('transform');
|
||||
else if (field !== 'opacity') usingFields.push(field);
|
||||
else hasOpacity = true;
|
||||
});
|
||||
const targetStyle =
|
||||
targetStylesMap.group || GROUP_ANIMATE_STYLES[isOut ? 0 : 1];
|
||||
if (hasCanceled) {
|
||||
Object.keys(targetStyle).forEach((key) => {
|
||||
group.style[key] = targetStyle[key];
|
||||
});
|
||||
} else {
|
||||
if (hasOpacity) {
|
||||
// opacity on group, animate on all shapes
|
||||
Object.keys(shapeMap).forEach((shapeId) => {
|
||||
const { opacity: targetOpaicty = 1 } =
|
||||
targetStylesMap[shapeId] ||
|
||||
targetStylesMap.otherShapes?.[shapeId] ||
|
||||
{};
|
||||
animation = runAnimateOnShape(
|
||||
shapeMap[shapeId],
|
||||
['opacity'],
|
||||
{ opacity: targetOpaicty },
|
||||
getShapeAnimateBeginStyles(shapeMap[shapeId]),
|
||||
animateConfig,
|
||||
);
|
||||
});
|
||||
}
|
||||
if (usingFields.length) {
|
||||
animation = runAnimateOnShape(
|
||||
group,
|
||||
usingFields,
|
||||
targetStyle,
|
||||
GROUP_ANIMATE_STYLES[isOut ? 1 : 0],
|
||||
animateConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const shape = shapeMap[shapeId];
|
||||
if (shape && shape.style.display !== 'none') {
|
||||
const targetStyle =
|
||||
targetStylesMap[shapeId] ||
|
||||
targetStylesMap.otherShapes?.[shapeId] ||
|
||||
{};
|
||||
if (hasCanceled) {
|
||||
Object.keys(targetStyle).forEach((key) => {
|
||||
shape.style[key] = targetStyle[key];
|
||||
});
|
||||
} else {
|
||||
animation = runAnimateOnShape(
|
||||
shape,
|
||||
fields,
|
||||
targetStyle,
|
||||
getShapeAnimateBeginStyles(shape),
|
||||
animateConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxDuration < duration && animation) {
|
||||
maxDuration = duration;
|
||||
maxDurationIdx = i;
|
||||
}
|
||||
if (animation) {
|
||||
animation.oncancel = () => {
|
||||
hasCanceled = true;
|
||||
cancelAnimations();
|
||||
};
|
||||
}
|
||||
return animation;
|
||||
});
|
||||
if (maxDurationIdx > -1) animations[maxDurationIdx].onfinish = onfinish;
|
||||
return animations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute one animation.
|
||||
* @param shape
|
||||
* @param fields
|
||||
* @param targetStyle
|
||||
* @param beginStyle
|
||||
* @param animateConfig
|
||||
* @returns
|
||||
*/
|
||||
const runAnimateOnShape = (
|
||||
shape: DisplayObject,
|
||||
fields: string[],
|
||||
targetStyle: ShapeStyle,
|
||||
beginStyle: ShapeStyle,
|
||||
animateConfig,
|
||||
) => {
|
||||
let animateArr;
|
||||
if (!fields?.length) {
|
||||
animateArr = getStyleDiff(shape.attributes, targetStyle);
|
||||
} else {
|
||||
animateArr = [{}, {}];
|
||||
fields.forEach((key) => {
|
||||
animateArr[0][key] = shape.attributes.hasOwnProperty(key)
|
||||
? shape.style[key]
|
||||
: beginStyle[key];
|
||||
animateArr[1][key] = targetStyle[key];
|
||||
if (key === 'lineDash' && animateArr[1][key].includes('100%')) {
|
||||
const totalLength = (shape as Line | Polyline | Path).getTotalLength();
|
||||
replaceElements(animateArr[1][key], '100%', totalLength);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (JSON.stringify(animateArr[0]) === JSON.stringify(animateArr[1])) return;
|
||||
return shape.animate(animateArr, animateConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle shape and group animations.
|
||||
* Should be called after canvas ready and shape appended.
|
||||
* @param animates
|
||||
* @param mergedStyles
|
||||
* @param shapeMap
|
||||
* @param group
|
||||
* @param timing timing to match 'when' in the animate config in style
|
||||
* @returns
|
||||
*/
|
||||
export const animateShapes = (
|
||||
animates: IAnimates,
|
||||
mergedStyles: ItemShapeStyles,
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
group: Group,
|
||||
timing: AnimateTiming = 'buildIn',
|
||||
changedStates: string[] = [],
|
||||
onAnimatesFrame: Function = () => {},
|
||||
onAnimatesEnd: Function = () => {},
|
||||
): IAnimation[] => {
|
||||
if (!animates?.[timing]) {
|
||||
onAnimatesEnd();
|
||||
return;
|
||||
}
|
||||
const segmentedTiming =
|
||||
timing === 'update' && changedStates?.length ? 'stateUpdate' : timing;
|
||||
const timingAnimateGroups = groupTimingAnimates(
|
||||
animates,
|
||||
segmentedTiming,
|
||||
changedStates,
|
||||
);
|
||||
let i = 0;
|
||||
const groupKeys = Object.keys(timingAnimateGroups);
|
||||
if (!groupKeys.length) return;
|
||||
let animations = [];
|
||||
let canceled = false;
|
||||
const onfinish = () => {
|
||||
if (i >= groupKeys.length) {
|
||||
onAnimatesEnd();
|
||||
return;
|
||||
}
|
||||
const groupAnimations = runAnimateGroupOnShapes(
|
||||
shapeMap,
|
||||
group,
|
||||
timingAnimateGroups[groupKeys[i]],
|
||||
mergedStyles,
|
||||
timing,
|
||||
onfinish, // execute next order group
|
||||
() => (canceled = true),
|
||||
canceled,
|
||||
).filter(Boolean);
|
||||
groupAnimations.forEach((animation) => {
|
||||
animation.onframe = onAnimatesFrame;
|
||||
});
|
||||
if (i === 0) {
|
||||
// collect the first group animations
|
||||
animations = groupAnimations;
|
||||
}
|
||||
i++;
|
||||
};
|
||||
onfinish();
|
||||
// only animations with order 0 will be returned
|
||||
return animations;
|
||||
};
|
||||
|
||||
export const getAnimatesExcludePosition = (animates) => {
|
||||
if (!animates.update) return animates;
|
||||
const isGroupId = (id) => !id || id === 'group';
|
||||
// const groupUpdateAnimates = animates.update.filter(
|
||||
// ({ shapeId }) => isGroupId(shapeId),
|
||||
// );
|
||||
const excludedAnimates = [];
|
||||
animates.update.forEach((animate) => {
|
||||
const { shapeId, fields } = animate;
|
||||
if (!isGroupId(shapeId)) {
|
||||
excludedAnimates.push(animate);
|
||||
return;
|
||||
}
|
||||
const newFields = fields;
|
||||
let isGroupPosition = false;
|
||||
if (fields.includes('x')) {
|
||||
const xFieldIdx = newFields.indexOf('x');
|
||||
newFields.splice(xFieldIdx, 1);
|
||||
isGroupPosition = true;
|
||||
}
|
||||
if (fields.includes('y')) {
|
||||
const yFieldIdx = newFields.indexOf('y');
|
||||
newFields.splice(yFieldIdx, 1);
|
||||
isGroupPosition = true;
|
||||
}
|
||||
|
||||
if (isGroupPosition) {
|
||||
if (newFields.length !== 0) {
|
||||
// group animation but not on x and y
|
||||
excludedAnimates.push({
|
||||
...animate,
|
||||
fields: newFields,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
excludedAnimates.push(animate);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...animates,
|
||||
update: excludedAnimates,
|
||||
};
|
||||
};
|
||||
|
||||
export const fadeIn = (id, shape, style, hiddenShape, animateConfig) => {
|
||||
// omit inexist shape and the shape which is not hidden by zoom changing
|
||||
if (!shape || !hiddenShape[id]) return;
|
||||
shape.show();
|
||||
const { opacity = 1 } = style;
|
||||
shape.animate([{ opacity: 0 }, { opacity }], animateConfig);
|
||||
};
|
||||
|
||||
export const fadeOut = (id, shape, hiddenShape, animateConfig) => {
|
||||
if (!shape?.isVisible()) return;
|
||||
hiddenShape[id] = true;
|
||||
const { opacity = 1 } = shape.attributes;
|
||||
if (opacity === 0) return;
|
||||
const animation = shape.animate([{ opacity }, { opacity: 0 }], animateConfig);
|
||||
animation.onfinish = () => shape.hide();
|
||||
};
|
@ -46,3 +46,12 @@ export function intersectSet<T>(a: T[], b: T[]): T[] {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function replaceElements(arr, target, replaceWith) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i] === target) {
|
||||
arr[i] = replaceWith;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Renderer as CanvasRenderer } from '@antv/g-canvas';
|
||||
import { Renderer as SVGRenderer } from '@antv/g-svg';
|
||||
import { Renderer as WebGLRenderer } from '@antv/g-webgl';
|
||||
import { Plugin as Plugin3D } from '@antv/g-plugin-3d';
|
||||
import { RendererName } from 'types/render';
|
||||
import { RendererName } from '../types/render';
|
||||
|
||||
/**
|
||||
* Create a canvas
|
||||
|
@ -36,7 +36,7 @@ export const upsertTransientItem = (
|
||||
return transientItem;
|
||||
}
|
||||
if (item.type === 'node') {
|
||||
const transientNode = item.clone(nodeGroup, onlyDrawKeyShape);
|
||||
const transientNode = item.clone(nodeGroup, onlyDrawKeyShape, true);
|
||||
transientItemMap[item.model.id] = transientNode;
|
||||
return transientNode;
|
||||
} else if (item.type === 'edge') {
|
||||
@ -54,7 +54,13 @@ export const upsertTransientItem = (
|
||||
transientItemMap,
|
||||
onlyDrawKeyShape,
|
||||
) as Node;
|
||||
const transientEdge = item.clone(edgeGroup, source, target);
|
||||
const transientEdge = item.clone(
|
||||
edgeGroup,
|
||||
source,
|
||||
target,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
transientItemMap[item.model.id] = transientEdge;
|
||||
return transientEdge;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { NodeDisplayModelData } from 'types/node';
|
||||
import { NodeDisplayModelData } from '../types/node';
|
||||
|
||||
/**
|
||||
* Default mapper to transform simple styles in inner data.
|
||||
|
@ -16,14 +16,17 @@ import {
|
||||
import { clone, isArray, isNumber } from '@antv/util';
|
||||
import { DEFAULT_LABEL_BG_PADDING } from '../constant';
|
||||
import { Point } from '../types/common';
|
||||
import { EdgeShapeMap } from '../types/edge';
|
||||
import { EdgeDisplayModel, EdgeShapeMap } from '../types/edge';
|
||||
import {
|
||||
GShapeStyle,
|
||||
SHAPE_TYPE,
|
||||
ItemShapeStyles,
|
||||
ShapeStyle,
|
||||
SHAPE_TYPE_3D,
|
||||
} from '../types/item';
|
||||
import { NodeShapeMap } from '../types/node';
|
||||
import { NodeDisplayModel, NodeShapeMap } from '../types/node';
|
||||
import { ComboDisplayModel } from '../types';
|
||||
import { getShapeAnimateBeginStyles } from './animate';
|
||||
import { isArrayOverlap } from './array';
|
||||
import { isBetween } from './math';
|
||||
|
||||
@ -39,44 +42,120 @@ export const ShapeTagMap = {
|
||||
path: Path,
|
||||
};
|
||||
|
||||
const LINE_TYPES = ['line', 'polyline', 'path'];
|
||||
|
||||
export const LOCAL_BOUNDS_DIRTY_FLAG_KEY = 'data-item-local-bounds-dirty';
|
||||
|
||||
export const createShape = (
|
||||
type: SHAPE_TYPE,
|
||||
style: GShapeStyle,
|
||||
id: string,
|
||||
) => {
|
||||
const ShapeClass = ShapeTagMap[type];
|
||||
return new ShapeClass({ style, id });
|
||||
const shape = new ShapeClass({ id, style });
|
||||
if (LINE_TYPES.includes(type)) {
|
||||
shape.style.increasedLineWidthForHitTesting = Math.max(
|
||||
shape.style.lineWidth as number,
|
||||
6,
|
||||
);
|
||||
}
|
||||
return shape;
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect the fields to be animated at the timing on shape with shapeId.
|
||||
* @param animates animate configs
|
||||
* @param timing
|
||||
* @param shapeId
|
||||
* @returns
|
||||
*/
|
||||
const findAnimateFields = (animates, timing, shapeId) => {
|
||||
if (!animates?.[timing]) return [];
|
||||
let animateFields = [];
|
||||
animates[timing].forEach(({ fields, shapeId: animateShapeId }) => {
|
||||
if (animateShapeId === shapeId) {
|
||||
animateFields = animateFields.concat(fields);
|
||||
} else if (
|
||||
(!animateShapeId || animateShapeId === 'group') &&
|
||||
fields.includes('opacity')
|
||||
) {
|
||||
// group opacity, all shapes animates with opacity
|
||||
animateFields.push('opacity');
|
||||
}
|
||||
});
|
||||
if (animateFields.includes(undefined)) {
|
||||
// there is an animate on all styles
|
||||
return [];
|
||||
}
|
||||
return animateFields;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create (if does not exit in shapeMap) or update the shape according to the configurations.
|
||||
* @param type shape's type
|
||||
* @param id unique string to indicates the shape
|
||||
* @param style style to be updated
|
||||
* @param shapeMap the shape map of a node / edge / combo
|
||||
* @param model data model of the node / edge / combo
|
||||
* @returns
|
||||
*/
|
||||
export const upsertShape = (
|
||||
type: SHAPE_TYPE,
|
||||
id: string,
|
||||
style: GShapeStyle,
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
): {
|
||||
updateStyles: ShapeStyle;
|
||||
shape: DisplayObject;
|
||||
} => {
|
||||
model?: NodeDisplayModel | EdgeDisplayModel | ComboDisplayModel,
|
||||
): DisplayObject => {
|
||||
let shape = shapeMap[id];
|
||||
let updateStyles = {};
|
||||
const { animates, disableAnimate } = model?.data || {};
|
||||
if (!shape) {
|
||||
// create
|
||||
shape = createShape(type, style, id);
|
||||
updateStyles = style;
|
||||
// find the animate styles, set them to be INIT_SHAPE_STYLES
|
||||
if (!disableAnimate && animates) {
|
||||
const animateFields = findAnimateFields(animates, 'buildIn', id);
|
||||
const initShapeStyles = getShapeAnimateBeginStyles(shape);
|
||||
animateFields.forEach((key) => {
|
||||
shape.style[key] = initShapeStyles[key];
|
||||
});
|
||||
}
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, true);
|
||||
} else if (shape.nodeName !== type) {
|
||||
// remove and create for the shape changed type
|
||||
shape.remove();
|
||||
shape = createShape(type, style, id);
|
||||
updateStyles = style;
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, true);
|
||||
} else {
|
||||
const updateStyles = {};
|
||||
const oldStyles = shape.attributes;
|
||||
Object.keys(style).forEach((key) => {
|
||||
if (oldStyles[key] !== style[key]) {
|
||||
updateStyles[key] = style[key];
|
||||
shape.style[key] = style[key];
|
||||
}
|
||||
});
|
||||
// update
|
||||
if (disableAnimate || !animates?.update) {
|
||||
// update all the style directly when there are no animates for update timing
|
||||
Object.keys(style).forEach((key) => {
|
||||
if (oldStyles[key] !== style[key]) {
|
||||
updateStyles[key] = style[key];
|
||||
shape.style[key] = style[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// update the styles excludes the ones in the animate fields
|
||||
const animateFields = findAnimateFields(animates, 'update', id);
|
||||
if (!animateFields.length) return shape;
|
||||
Object.keys(style).forEach((key) => {
|
||||
if (oldStyles[key] !== style[key]) {
|
||||
updateStyles[key] = style[key];
|
||||
if (!animateFields.includes(key)) {
|
||||
shape.style[key] = style[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (isStyleAffectBBox(type, updateStyles)) {
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, true);
|
||||
}
|
||||
}
|
||||
shapeMap[id] = shape;
|
||||
return { shape, updateStyles };
|
||||
return shape;
|
||||
};
|
||||
|
||||
export const getGroupSucceedMap = (
|
||||
@ -124,11 +203,15 @@ export const updateShapes = (
|
||||
if (prevShape !== newShape) {
|
||||
prevShape.remove();
|
||||
}
|
||||
group.appendChild(newShape);
|
||||
if (newShape.style.display !== 'none') {
|
||||
group.appendChild(newShape);
|
||||
}
|
||||
} else if (!prevShape && newShape) {
|
||||
// add newShapeMap - prevShapeMap
|
||||
finalShapeMap[id] = newShape;
|
||||
group.appendChild(newShape);
|
||||
if (newShape.style.display !== 'none') {
|
||||
group.appendChild(newShape);
|
||||
}
|
||||
} else if (prevShape && !newShape && removeDiff) {
|
||||
// remove prevShapeMap - newShapeMap
|
||||
delete finalShapeMap[id];
|
||||
@ -428,11 +511,14 @@ const FEILDS_AFFECT_BBOX = {
|
||||
rect: ['width', 'height', 'lineWidth'],
|
||||
image: ['width', 'height', 'lineWidth'],
|
||||
ellipse: ['rx', 'ry', 'lineWidth'],
|
||||
text: ['fontSize', 'fontWeight'],
|
||||
text: ['fontSize', 'fontWeight', 'text', 'wordWrapWidth'],
|
||||
polygon: ['points', 'lineWidth'],
|
||||
line: ['x1', 'x2', 'y1', 'y2', 'lineWidth'],
|
||||
polyline: ['points', 'lineWidth'],
|
||||
path: ['points', 'lineWidth'],
|
||||
sphere: ['radius'],
|
||||
cube: ['width', 'height', 'depth'],
|
||||
plane: ['width', 'depth'],
|
||||
};
|
||||
/**
|
||||
* Will the fields in style affect the bbox.
|
||||
@ -440,6 +526,9 @@ const FEILDS_AFFECT_BBOX = {
|
||||
* @param style style object
|
||||
* @returns
|
||||
*/
|
||||
export const isStyleAffectBBox = (type: SHAPE_TYPE, style: ShapeStyle) => {
|
||||
export const isStyleAffectBBox = (
|
||||
type: SHAPE_TYPE | SHAPE_TYPE_3D,
|
||||
style: ShapeStyle,
|
||||
) => {
|
||||
return isArrayOverlap(Object.keys(style), FEILDS_AFFECT_BBOX[type]);
|
||||
};
|
||||
|
@ -16,7 +16,11 @@ import {
|
||||
SHAPE_TYPE_3D,
|
||||
ShapeStyle,
|
||||
} from '../types/item';
|
||||
import { ShapeTagMap, createShape } from './shape';
|
||||
import {
|
||||
LOCAL_BOUNDS_DIRTY_FLAG_KEY,
|
||||
createShape,
|
||||
isStyleAffectBBox,
|
||||
} from './shape';
|
||||
|
||||
const GeometryTagMap = {
|
||||
sphere: SphereGeometry,
|
||||
@ -108,20 +112,17 @@ export const upsertShape3D = (
|
||||
style: GShapeStyle,
|
||||
shapeMap: { [shapeId: string]: DisplayObject },
|
||||
device: any,
|
||||
): {
|
||||
updateStyles: ShapeStyle;
|
||||
shape: DisplayObject;
|
||||
} => {
|
||||
): DisplayObject => {
|
||||
let shape = shapeMap[id];
|
||||
let updateStyles = {};
|
||||
if (!shape) {
|
||||
shape = createShape3D(type, style, id, device);
|
||||
updateStyles = style;
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, true);
|
||||
} else if (shape.nodeName !== type) {
|
||||
shape.remove();
|
||||
shape = createShape3D(type, style, id, device);
|
||||
updateStyles = style;
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, true);
|
||||
} else {
|
||||
const updateStyles = {};
|
||||
const oldStyles = shape.attributes;
|
||||
Object.keys(style).forEach((key) => {
|
||||
if (oldStyles[key] !== style[key]) {
|
||||
@ -129,9 +130,12 @@ export const upsertShape3D = (
|
||||
shape.style[key] = style[key];
|
||||
}
|
||||
});
|
||||
if (isStyleAffectBBox(type, updateStyles)) {
|
||||
shape.setAttribute(LOCAL_BOUNDS_DIRTY_FLAG_KEY, true);
|
||||
}
|
||||
}
|
||||
shapeMap[id] = shape;
|
||||
return { shape, updateStyles };
|
||||
return shape;
|
||||
};
|
||||
|
||||
/**
|
||||
|
50
packages/g6/src/util/text.ts
Normal file
50
packages/g6/src/util/text.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { AABB } from '@antv/g';
|
||||
import { getEuclideanDistance } from '@antv/layout';
|
||||
import { Point } from '../types/common';
|
||||
|
||||
/**
|
||||
* Get the proper wordWrapWidth for a labelShape according the the 'maxWidth' of keyShape.
|
||||
* @param keyShapeBox
|
||||
* @param maxWidth
|
||||
* @param zoom
|
||||
* @returns
|
||||
*/
|
||||
export const getWordWrapWidthByBox = (
|
||||
keyShapeBox: AABB,
|
||||
maxWidth: string | number,
|
||||
zoom: number = 1,
|
||||
) => {
|
||||
const keyShapeWidth = (keyShapeBox.max[0] - keyShapeBox.min[0]) * zoom;
|
||||
const wordWrapWidth = 2 * keyShapeWidth;
|
||||
return getWordWrapWidthWithBase(wordWrapWidth, maxWidth);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the proper wordWrapWidth for a labelShape according the the distance between two end points and 'maxWidth'.
|
||||
* @param points
|
||||
* @param maxWidth
|
||||
* @param zoom
|
||||
* @returns
|
||||
*/
|
||||
export const getWordWrapWidthByEnds = (
|
||||
points: Point[],
|
||||
maxWidth: string | number,
|
||||
zoom: number = 1,
|
||||
) => {
|
||||
const dist = getEuclideanDistance(points[0], points[1]) * zoom;
|
||||
return getWordWrapWidthWithBase(dist, maxWidth);
|
||||
};
|
||||
|
||||
const getWordWrapWidthWithBase = (
|
||||
length: number,
|
||||
maxWidth: string | number,
|
||||
) => {
|
||||
let wordWrapWidth = 2 * length;
|
||||
if (typeof maxWidth === 'string') {
|
||||
wordWrapWidth = (length * Number(maxWidth.replace('%', ''))) / 100;
|
||||
} else if (typeof maxWidth === 'number') {
|
||||
wordWrapWidth = maxWidth;
|
||||
}
|
||||
if (isNaN(wordWrapWidth)) wordWrapWidth = 2 * length;
|
||||
return wordWrapWidth;
|
||||
};
|
41
packages/g6/src/util/zoom.ts
Normal file
41
packages/g6/src/util/zoom.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { ZoomStrategy, ZoomStrategyObj } from '../types/item';
|
||||
|
||||
/**
|
||||
* Format zoomStrategy to the pattern that ratio 1 (primary level) at level 0, and higher the ratio, higher the level.
|
||||
* @param zoomStrategy
|
||||
* @returns
|
||||
*/
|
||||
export const formatZoomStrategy = (
|
||||
zoomStrategy: ZoomStrategy,
|
||||
): ZoomStrategyObj => {
|
||||
const { levels, animateCfg } = zoomStrategy || {};
|
||||
if (!levels) return undefined;
|
||||
const primaryLevel = levels.find((level) => level.primary);
|
||||
const primaryIndex = levels.indexOf(primaryLevel);
|
||||
const formattedLevels = {};
|
||||
levels.forEach((level, i) => {
|
||||
formattedLevels[i - primaryIndex] = level.range;
|
||||
});
|
||||
return {
|
||||
animateCfg,
|
||||
levels: formattedLevels,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get zoom level idx in levels array.
|
||||
* @param levels
|
||||
* @param zoom
|
||||
* @returns
|
||||
*/
|
||||
export const getZoomLevel = (
|
||||
levels: { [idx: number | string]: [number, number] },
|
||||
zoom: number,
|
||||
) => {
|
||||
let level = 0;
|
||||
Object.keys(levels).forEach((idx) => {
|
||||
const range = levels[idx];
|
||||
if (zoom >= range[0] && zoom < range[1]) level = Number(idx);
|
||||
});
|
||||
return level;
|
||||
};
|
@ -701,7 +701,7 @@ describe('state', () => {
|
||||
this.defaultStyles.labelShape,
|
||||
propsLabelStyle,
|
||||
);
|
||||
const labelShape = this.upsertShape(
|
||||
return this.upsertShape(
|
||||
'text',
|
||||
'labelShape',
|
||||
{
|
||||
@ -709,8 +709,7 @@ describe('state', () => {
|
||||
text: model.id,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
return labelShape;
|
||||
);
|
||||
}
|
||||
public drawOtherShapes(
|
||||
model: NodeDisplayModel,
|
||||
@ -728,7 +727,7 @@ describe('state', () => {
|
||||
y: 0,
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -753,7 +752,7 @@ describe('state', () => {
|
||||
...model.data?.otherShapes?.buShape, // merged style from mappers and states
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ describe('node item', () => {
|
||||
it('new graph with one node', () => {
|
||||
//done
|
||||
const nodes = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
nodes.push({
|
||||
id: 'node-' + i,
|
||||
data: {
|
||||
@ -19,11 +19,11 @@ describe('node item', () => {
|
||||
});
|
||||
}
|
||||
const edges = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
edges.push({
|
||||
id: 'edge1-' + i,
|
||||
source: 'node-' + i,
|
||||
target: 'node-' + (i + Math.floor(Math.random() * 4)),
|
||||
target: 'node-' + (i + Math.floor(Math.random() * 50)),
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
@ -48,6 +48,13 @@ describe('node item', () => {
|
||||
nodes,
|
||||
edges,
|
||||
},
|
||||
edge: {
|
||||
type: 'line-edge',
|
||||
keyShape: {
|
||||
lineWidth: 2,
|
||||
stroke: '#000',
|
||||
},
|
||||
},
|
||||
node: {
|
||||
type: 'sphere-node',
|
||||
// type: 'circle-node',
|
||||
@ -55,12 +62,12 @@ describe('node item', () => {
|
||||
opacity: 0.6,
|
||||
// materialType: 'basic',
|
||||
},
|
||||
labelShape: {
|
||||
text: 'node-label',
|
||||
},
|
||||
iconShape: {
|
||||
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
},
|
||||
// labelShape: {
|
||||
// text: 'node-label',
|
||||
// },
|
||||
// iconShape: {
|
||||
// img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
|
||||
// },
|
||||
},
|
||||
nodeState: {
|
||||
selected: {
|
||||
|
1275
packages/g6/tests/unit/item-animate-spec.ts
Normal file
1275
packages/g6/tests/unit/item-animate-spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -260,7 +260,8 @@ describe('register node', () => {
|
||||
text: model.id,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
public drawOtherShapes(
|
||||
model: NodeDisplayModel,
|
||||
@ -278,7 +279,8 @@ describe('register node', () => {
|
||||
y: 0,
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
model,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -302,7 +304,8 @@ describe('register node', () => {
|
||||
fill: '#0f0',
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
model,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -421,8 +424,13 @@ describe('register node', () => {
|
||||
this.defaultStyles.keyShape,
|
||||
model.data.labelShape,
|
||||
);
|
||||
return this.upsertShape('rect', 'keyShape', keyShapeStyle, shapeMap)
|
||||
.shape;
|
||||
return this.upsertShape(
|
||||
'rect',
|
||||
'keyShape',
|
||||
keyShapeStyle,
|
||||
shapeMap,
|
||||
model,
|
||||
);
|
||||
}
|
||||
public drawOtherShapes(
|
||||
model: NodeDisplayModel,
|
||||
@ -441,7 +449,8 @@ describe('register node', () => {
|
||||
lineWidth: 2,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
return { testShape };
|
||||
}
|
||||
}
|
||||
@ -845,7 +854,8 @@ describe('state', () => {
|
||||
text: model.id,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
public drawOtherShapes(
|
||||
model: NodeDisplayModel,
|
||||
@ -865,7 +875,8 @@ describe('state', () => {
|
||||
y: 0,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
return { extraShape };
|
||||
}
|
||||
}
|
||||
@ -890,7 +901,8 @@ describe('state', () => {
|
||||
...model.data?.otherShapes?.buShape, // merged style from mappers and states
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
model,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
2191
packages/g6/tests/unit/show-animate-spec.ts
Normal file
2191
packages/g6/tests/unit/show-animate-spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -651,7 +651,8 @@ describe('theme', () => {
|
||||
text: model.id,
|
||||
},
|
||||
shapeMap,
|
||||
).shape;
|
||||
model,
|
||||
);
|
||||
}
|
||||
public drawOtherShapes(
|
||||
model: NodeDisplayModel,
|
||||
@ -669,7 +670,8 @@ describe('theme', () => {
|
||||
y: 0,
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
model,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -693,7 +695,8 @@ describe('theme', () => {
|
||||
fill: '#0f0',
|
||||
},
|
||||
shapeMap,
|
||||
).shape,
|
||||
model,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user