mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 10:18:14 +08:00
使用相机实现视口相关功能 (#4348)
* feat: support translate & zoom with camera #4344 * feat: support fitCenter & focusItem with Camera API #4344 * feat: support fitView #4344 * feat: support focusing multi-items & stop transition of current transform #4348
This commit is contained in:
parent
8f68192dd3
commit
918584d6b1
@ -2,5 +2,6 @@ export * from './data';
|
||||
export * from './interaction';
|
||||
export * from './item';
|
||||
export * from './layout';
|
||||
export * from './viewport';
|
||||
export * from './theme';
|
||||
export * from './extension';
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { AABB, Canvas, DisplayObject, Group } from '@antv/g';
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { Group, AABB, DisplayObject, Canvas } from '@antv/g';
|
||||
import { ComboModel, IGraph } from '../../types';
|
||||
import registry from '../../stdlib';
|
||||
import { getExtension } from '../../util/extension';
|
||||
import { GraphCore } from '../../types/data';
|
||||
import { NodeDisplayModel, NodeEncode, NodeModel, NodeModelData } from '../../types/node';
|
||||
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeModelData } from '../../types/edge';
|
||||
import Node from '../../item/node';
|
||||
import Edge from '../../item/edge';
|
||||
import Combo from '../../item/combo';
|
||||
import { ThemeSpecification, ItemThemeSpecifications, ItemStyleSet } from '../../types/theme';
|
||||
import { isArray, isObject } from '@antv/util';
|
||||
import { ITEM_TYPE, SHAPE_TYPE, ShapeStyle } from '../../types/item';
|
||||
import Combo from '../../item/combo';
|
||||
import Edge from '../../item/edge';
|
||||
import Node from '../../item/node';
|
||||
import registry from '../../stdlib';
|
||||
import { ComboModel, IGraph } from '../../types';
|
||||
import { ComboDisplayModel, ComboEncode } from '../../types/combo';
|
||||
import { GraphCore } from '../../types/data';
|
||||
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeModelData } from '../../types/edge';
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../../types/item';
|
||||
import { ItemStyleSet, ItemThemeSpecifications, ThemeSpecification } from '../../types/theme';
|
||||
import { getExtension } from '../../util/extension';
|
||||
import { upsertShape } from '../../util/shape';
|
||||
|
||||
/**
|
||||
@ -61,7 +60,7 @@ export class ItemController {
|
||||
constructor(graph: IGraph<any, any>) {
|
||||
this.graph = graph;
|
||||
// get mapper for node / edge / combo
|
||||
const { node, edge, combo, nodeState, edgeState, comboState } = graph.getSpecification();
|
||||
const { node, edge, combo, nodeState, edgeState, comboState } = graph.getSpecification();
|
||||
this.nodeMapper = node;
|
||||
this.edgeMapper = edge;
|
||||
this.comboMapper = combo;
|
||||
@ -114,7 +113,7 @@ export class ItemController {
|
||||
* Listener of runtime's render hook.
|
||||
* @param param contains inner data stored in graphCore structure
|
||||
*/
|
||||
private onRender(param: { graphCore: GraphCore, theme: ThemeSpecification }) {
|
||||
private onRender(param: { graphCore: GraphCore; theme: ThemeSpecification }) {
|
||||
const { graphCore, theme = {} } = param;
|
||||
const { graph } = this;
|
||||
// TODO: 0. clear groups on canvas, and create new groups
|
||||
@ -177,11 +176,17 @@ export class ItemController {
|
||||
|
||||
// === 3. add nodes ===
|
||||
if (groupedChanges.NodeAdded.length) {
|
||||
this.renderNodes(groupedChanges.NodeAdded.map((change) => change.value), nodeTheme);
|
||||
this.renderNodes(
|
||||
groupedChanges.NodeAdded.map((change) => change.value),
|
||||
nodeTheme,
|
||||
);
|
||||
}
|
||||
// === 4. add edges ===
|
||||
if (groupedChanges.EdgeAdded.length) {
|
||||
this.renderEdges(groupedChanges.EdgeAdded.map((change) => change.value), edgeTheme);
|
||||
this.renderEdges(
|
||||
groupedChanges.EdgeAdded.map((change) => change.value),
|
||||
edgeTheme,
|
||||
);
|
||||
}
|
||||
|
||||
// === 5. update nodes's data ===
|
||||
@ -209,7 +214,12 @@ export class ItemController {
|
||||
// update the theme if the dataType value is changed
|
||||
let themeStyles;
|
||||
if (previous[nodeDataTypeField] !== current[nodeDataTypeField]) {
|
||||
themeStyles = getThemeStyles(this.nodeDataTypeSet, nodeDataTypeField, current[nodeDataTypeField], nodeTheme);
|
||||
themeStyles = getThemeStyles(
|
||||
this.nodeDataTypeSet,
|
||||
nodeDataTypeField,
|
||||
current[nodeDataTypeField],
|
||||
nodeTheme,
|
||||
);
|
||||
}
|
||||
const item = itemMap[id];
|
||||
const innerModel = graphCore.getNode(id);
|
||||
@ -247,7 +257,12 @@ export class ItemController {
|
||||
// update the theme if the dataType value is changed
|
||||
let themeStyles;
|
||||
if (previous[edgeDataTypeField] !== current[edgeDataTypeField]) {
|
||||
themeStyles = getThemeStyles(this.edgeDataTypeSet, edgeDataTypeField, current[edgeDataTypeField], edgeTheme);
|
||||
themeStyles = getThemeStyles(
|
||||
this.edgeDataTypeSet,
|
||||
edgeDataTypeField,
|
||||
current[edgeDataTypeField],
|
||||
edgeTheme,
|
||||
);
|
||||
}
|
||||
const item = itemMap[id];
|
||||
const innerModel = graphCore.getEdge(id);
|
||||
@ -283,9 +298,9 @@ export class ItemController {
|
||||
* value: state value
|
||||
* }
|
||||
*/
|
||||
private onItemStateChange(param: { ids: ID[], states: string[], value: boolean }) {
|
||||
private onItemStateChange(param: { ids: ID[]; states: string[]; value: boolean }) {
|
||||
const { ids, states, value } = param;
|
||||
ids.forEach(id => {
|
||||
ids.forEach((id) => {
|
||||
const item = this.itemMap[id];
|
||||
if (!item) {
|
||||
console.warn(`Fail to set state for item ${id}, which is not exist.`);
|
||||
@ -295,20 +310,20 @@ export class ItemController {
|
||||
// clear all the states
|
||||
item.clearStates(states);
|
||||
} else {
|
||||
states.forEach(state => item.setState(state, value));
|
||||
states.forEach((state) => item.setState(state, value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onTransientUpdate(param: {
|
||||
type: ITEM_TYPE | SHAPE_TYPE,
|
||||
id: ID,
|
||||
type: ITEM_TYPE | SHAPE_TYPE;
|
||||
id: ID;
|
||||
config: {
|
||||
style: ShapeStyle,
|
||||
action: 'remove' | 'add' | 'update' | undefined,
|
||||
[shapeConfig: string]: unknown,
|
||||
},
|
||||
canvas: Canvas
|
||||
style: ShapeStyle;
|
||||
action: 'remove' | 'add' | 'update' | undefined;
|
||||
[shapeConfig: string]: unknown;
|
||||
};
|
||||
canvas: Canvas;
|
||||
}) {
|
||||
const { transientMap } = this;
|
||||
const { type, id, config = {}, canvas } = param;
|
||||
@ -345,7 +360,7 @@ export class ItemController {
|
||||
let dataType;
|
||||
if (dataTypeField) dataType = node.data[dataTypeField] as string;
|
||||
const themeStyle = getThemeStyles(nodeDataTypeSet, dataTypeField, dataType, nodeTheme);
|
||||
|
||||
|
||||
this.itemMap[node.id] = new Node({
|
||||
model: node,
|
||||
renderExtensions: nodeExtensions,
|
||||
@ -405,7 +420,7 @@ export class ItemController {
|
||||
*/
|
||||
public findIdByState(itemType: ITEM_TYPE, state: string, value: string | boolean = true) {
|
||||
const ids = [];
|
||||
Object.values(this.itemMap).forEach(item => {
|
||||
Object.values(this.itemMap).forEach((item) => {
|
||||
if (item.getType() !== itemType) return;
|
||||
if (item.hasState(state) === value) ids.push(item.getID());
|
||||
});
|
||||
@ -427,6 +442,10 @@ export class ItemController {
|
||||
return item.hasState(state);
|
||||
}
|
||||
|
||||
public getItemById(id: ID) {
|
||||
return this.itemMap[id];
|
||||
}
|
||||
|
||||
public getItemBBox(id: ID, isKeyShape: boolean = false): AABB | false {
|
||||
const item = this.itemMap[id];
|
||||
if (!item) {
|
||||
@ -446,7 +465,12 @@ export class ItemController {
|
||||
}
|
||||
}
|
||||
|
||||
const getThemeStyles = (dataTypeSet: Set<string>, dataTypeField: string, dataType: string, itemTheme: ItemThemeSpecifications): ItemStyleSet => {
|
||||
const getThemeStyles = (
|
||||
dataTypeSet: Set<string>,
|
||||
dataTypeField: string,
|
||||
dataType: string,
|
||||
itemTheme: ItemThemeSpecifications,
|
||||
): ItemStyleSet => {
|
||||
const { styles: themeStyles } = itemTheme;
|
||||
if (!dataTypeField) {
|
||||
// dataType field is not assigned
|
||||
@ -462,4 +486,4 @@ const getThemeStyles = (dataTypeSet: Set<string>, dataTypeField: string, dataTyp
|
||||
themeStyle = themeStyles[dataType] || themeStyles.others;
|
||||
}
|
||||
return themeStyle;
|
||||
}
|
||||
};
|
||||
|
92
packages/g6/src/runtime/controller/viewport.ts
Normal file
92
packages/g6/src/runtime/controller/viewport.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { ICamera, PointLike } from '@antv/g';
|
||||
import { IGraph } from '../../types';
|
||||
import { CameraAnimationOptions } from '../../types/animate';
|
||||
import { ViewportChangeHookParams } from '../../types/hook';
|
||||
|
||||
let landmarkCounter = 0;
|
||||
|
||||
export class ViewportController {
|
||||
public graph: IGraph;
|
||||
|
||||
constructor(graph: IGraph<any>) {
|
||||
this.graph = graph;
|
||||
this.tap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe the lifecycle of graph.
|
||||
*/
|
||||
private tap() {
|
||||
this.graph.hooks.viewportchange.tap(this.onViewportChange.bind(this));
|
||||
}
|
||||
|
||||
private async onViewportChange({ transform, effectTiming }: ViewportChangeHookParams) {
|
||||
const camera = this.graph.canvas.getCamera();
|
||||
const { translate, rotate, zoom, origin = this.graph.getViewportCenter() } = transform;
|
||||
const currentZoom = camera.getZoom();
|
||||
|
||||
if (effectTiming) {
|
||||
const { duration = 1000, easing = 'linear', easingFunction } = effectTiming;
|
||||
const landmarkOptions: Partial<{
|
||||
position: [number, number] | [number, number, number] | Float32Array;
|
||||
focalPoint: [number, number] | [number, number, number] | Float32Array;
|
||||
zoom: number;
|
||||
roll: number;
|
||||
}> = {};
|
||||
|
||||
if (translate) {
|
||||
const { dx = 0, dy = 0 } = translate;
|
||||
const [px, py] = camera.getPosition();
|
||||
const [fx, fy] = camera.getFocalPoint();
|
||||
landmarkOptions.position = [px - dx, py - dy];
|
||||
landmarkOptions.focalPoint = [fx - dx, fy - dy];
|
||||
}
|
||||
|
||||
if (zoom) {
|
||||
const { ratio } = zoom;
|
||||
landmarkOptions.zoom = currentZoom * ratio;
|
||||
}
|
||||
|
||||
if (rotate) {
|
||||
const { angle } = rotate;
|
||||
landmarkOptions.roll = camera.getRoll() + angle;
|
||||
}
|
||||
|
||||
const landmark = camera.createLandmark(`mark${landmarkCounter++}`, landmarkOptions);
|
||||
return new Promise((resolve) => {
|
||||
camera.gotoLandmark(landmark, {
|
||||
duration: Number(duration),
|
||||
easing,
|
||||
easingFunction,
|
||||
onfinish: () => {
|
||||
resolve(undefined);
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (translate) {
|
||||
const { dx = 0, dy = 0 } = translate;
|
||||
camera.pan(-dx / currentZoom, -dy / currentZoom);
|
||||
}
|
||||
|
||||
if (rotate) {
|
||||
const { angle } = rotate;
|
||||
const [x, y] = camera.getPosition();
|
||||
if (origin) {
|
||||
camera.setPosition(origin.x, origin.y);
|
||||
}
|
||||
camera.rotate(0, 0, angle);
|
||||
if (origin) {
|
||||
camera.pan(x - origin.x, y - origin.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (zoom) {
|
||||
const { ratio } = zoom;
|
||||
camera.setZoomByViewportPoint(currentZoom * ratio, [origin.x, origin.y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import EventEmitter from '@antv/event-emitter';
|
||||
import { Canvas, runtime, AABB, DisplayObject } from '@antv/g';
|
||||
import { AABB, Canvas, DisplayObject, PointLike, runtime } from '@antv/g';
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { isArray, isNil, isNumber, isObject, isString } from '@antv/util';
|
||||
import {
|
||||
@ -10,13 +10,13 @@ import {
|
||||
NodeUserModel,
|
||||
Specification,
|
||||
} from '../types';
|
||||
import { AnimateCfg } from '../types/animate';
|
||||
import { CameraAnimationOptions } from '../types/animate';
|
||||
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
|
||||
import { ComboModel } from '../types/combo';
|
||||
import { Padding, Point } from '../types/common';
|
||||
import { Padding } from '../types/common';
|
||||
import { GraphCore } from '../types/data';
|
||||
import { EdgeModel, EdgeModelData } from '../types/edge';
|
||||
import { Hooks } from '../types/hook';
|
||||
import { Hooks, ViewportChangeHookParams } from '../types/hook';
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from '../types/item';
|
||||
import {
|
||||
ImmediatelyInvokedLayoutOptions,
|
||||
@ -25,8 +25,9 @@ import {
|
||||
} from '../types/layout';
|
||||
import { NodeModel, NodeModelData } from '../types/node';
|
||||
import { ThemeRegistry, ThemeSpecification } from '../types/theme';
|
||||
import { FitViewRules, GraphAlignment } from '../types/view';
|
||||
import { FitViewRules, GraphTransformOptions } from '../types/view';
|
||||
import { createCanvas } from '../util/canvas';
|
||||
import { formatPadding } from '../util/shape';
|
||||
import {
|
||||
DataController,
|
||||
ExtensionController,
|
||||
@ -34,6 +35,7 @@ import {
|
||||
ItemController,
|
||||
LayoutController,
|
||||
ThemeController,
|
||||
ViewportController,
|
||||
} from './controller';
|
||||
import Hook from './hooks';
|
||||
|
||||
@ -42,7 +44,10 @@ import Hook from './hooks';
|
||||
*/
|
||||
runtime.enableCSSParsing = false;
|
||||
|
||||
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry> extends EventEmitter implements IGraph<B, T> {
|
||||
export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
extends EventEmitter
|
||||
implements IGraph<B, T>
|
||||
{
|
||||
public hooks: Hooks;
|
||||
// for nodes and edges, which will be separate into groups
|
||||
public canvas: Canvas;
|
||||
@ -58,6 +63,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
private dataController: DataController;
|
||||
private interactionController: InteractionController;
|
||||
private layoutController: LayoutController;
|
||||
private viewportController: ViewportController;
|
||||
private itemController: ItemController;
|
||||
private extensionController: ExtensionController;
|
||||
private themeController: ThemeController;
|
||||
@ -65,9 +71,9 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
private defaultSpecification = {
|
||||
theme: {
|
||||
type: 'spec',
|
||||
base: 'light'
|
||||
}
|
||||
}
|
||||
base: 'light',
|
||||
},
|
||||
};
|
||||
|
||||
constructor(spec: Specification<B, T>) {
|
||||
super();
|
||||
@ -82,8 +88,8 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
canvases: {
|
||||
background: this.backgroundCanvas,
|
||||
main: this.canvas,
|
||||
transient: this.transientCanvas
|
||||
}
|
||||
transient: this.transientCanvas,
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = spec;
|
||||
@ -102,6 +108,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
this.layoutController = new LayoutController(this);
|
||||
this.themeController = new ThemeController(this);
|
||||
this.itemController = new ItemController(this);
|
||||
this.viewportController = new ViewportController(this);
|
||||
this.extensionController = new ExtensionController(this);
|
||||
}
|
||||
|
||||
@ -132,10 +139,10 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
this.hooks = {
|
||||
init: new Hook<{
|
||||
canvases: {
|
||||
background: Canvas,
|
||||
main: Canvas,
|
||||
transient: Canvas
|
||||
}
|
||||
background: Canvas;
|
||||
main: Canvas;
|
||||
transient: Canvas;
|
||||
};
|
||||
}>({ name: 'init' }),
|
||||
datachange: new Hook<{ data: GraphData; type: 'replace' }>({ name: 'datachange' }),
|
||||
itemchange: new Hook<{
|
||||
@ -144,16 +151,24 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
}>({ name: 'itemchange' }),
|
||||
render: new Hook<{ graphCore: GraphCore, theme: ThemeSpecification; }>({ name: 'render' }),
|
||||
render: new Hook<{ graphCore: GraphCore; theme: ThemeSpecification }>({ name: 'render' }),
|
||||
layout: new Hook<{ graphCore: GraphCore }>({ name: 'layout' }),
|
||||
viewportchange: new Hook<ViewportChangeHookParams>({ name: 'viewport' }),
|
||||
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
|
||||
behaviorchange: new Hook<{
|
||||
action: 'update' | 'add' | 'remove';
|
||||
modes: string[];
|
||||
behaviors: BehaviorOptionsOf<{}>[];
|
||||
}>({ name: 'behaviorchange' }),
|
||||
itemstatechange: new Hook<{ ids: ID[], state: string, value: boolean }>({ name: 'itemstatechange' }),
|
||||
transientupdate: new Hook<{ type: ITEM_TYPE | SHAPE_TYPE, id: ID, config: { style: ShapeStyle, action: 'remove' | 'add' | 'update' | undefined }, canvas: Canvas }>({ name: 'transientupdate'}), // TODO
|
||||
itemstatechange: new Hook<{ ids: ID[]; state: string; value: boolean }>({
|
||||
name: 'itemstatechange',
|
||||
}),
|
||||
transientupdate: new Hook<{
|
||||
type: ITEM_TYPE | SHAPE_TYPE;
|
||||
id: ID;
|
||||
config: { style: ShapeStyle; action: 'remove' | 'add' | 'update' | undefined };
|
||||
canvas: Canvas;
|
||||
}>({ name: 'transientupdate' }), // TODO
|
||||
};
|
||||
}
|
||||
|
||||
@ -184,7 +199,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
const emitRender = async () => {
|
||||
this.hooks.render.emit({
|
||||
graphCore: this.dataController.graphCore,
|
||||
theme: this.themeController.specification
|
||||
theme: this.themeController.specification,
|
||||
});
|
||||
this.emit('afterrender');
|
||||
|
||||
@ -211,7 +226,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
this.hooks.datachange.emit({ data, type });
|
||||
this.hooks.render.emit({
|
||||
graphCore: this.dataController.graphCore,
|
||||
theme: this.themeController.specification
|
||||
theme: this.themeController.specification,
|
||||
});
|
||||
this.emit('afterrender');
|
||||
|
||||
@ -226,84 +241,223 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the graph with a relative vector.
|
||||
* @param dx x of the relative vector
|
||||
* @param dy y of the relative vector
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
*/
|
||||
public move(dx: number, dy: number, animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public getViewportCenter(): PointLike {
|
||||
const { width, height } = this.canvas.getConfig();
|
||||
return { x: width! / 2, y: height! / 2 };
|
||||
}
|
||||
public async transform(
|
||||
options: GraphTransformOptions,
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
): Promise<void> {
|
||||
await this.hooks.viewportchange.emitLinearAsync({
|
||||
transform: options,
|
||||
effectTiming,
|
||||
});
|
||||
this.emit('viewportchange', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the graph and align to a point.
|
||||
* @param x position on the canvas to align
|
||||
* @param y position on the canvas to align
|
||||
* @param alignment alignment of the graph content
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* Stop the current transition of transform immediately.
|
||||
*/
|
||||
public moveTo(x: number, y: number, alignment: GraphAlignment, animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public stopTransformTransition() {
|
||||
this.canvas.getCamera().cancelLandmarkAnimation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the graph with a relative distance under viewport coordinates.
|
||||
* @param dx x of the relative distance
|
||||
* @param dy y of the relative distance
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public async translate(dx: number, dy: number, effectTiming?: CameraAnimationOptions) {
|
||||
await this.transform(
|
||||
{
|
||||
translate: {
|
||||
dx,
|
||||
dy,
|
||||
},
|
||||
},
|
||||
effectTiming,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the graph to destination under viewport coordinates.
|
||||
* @param destination destination under viewport coordinates.
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public async translateTo({ x, y }: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||
const { x: cx, y: cy } = this.getViewportCenter();
|
||||
await this.translate(cx - x, cy - y, effectTiming);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom the graph with a relative ratio.
|
||||
* @param ratio relative ratio to zoom
|
||||
* @param center zoom center
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param origin origin under viewport coordinates.
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public zoom(ratio: number, center?: Point, animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public async zoom(ratio: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||
await this.transform(
|
||||
{
|
||||
zoom: {
|
||||
ratio,
|
||||
},
|
||||
origin,
|
||||
},
|
||||
effectTiming,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom the graph to a specified ratio.
|
||||
* @param toRatio specified ratio
|
||||
* @param center zoom center
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param zoom specified ratio
|
||||
* @param origin zoom center
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public zoomTo(toRatio: number, center?: Point, animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public async zoomTo(zoom: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||
await this.zoom(zoom / this.canvas.getCamera().getZoom(), origin, effectTiming);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current zoom level of camera.
|
||||
* @returns current zoom
|
||||
*/
|
||||
public getZoom() {
|
||||
return this.canvas.getCamera().getZoom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the graph with a relative angle.
|
||||
* @param angle
|
||||
* @param origin
|
||||
* @param effectTiming
|
||||
*/
|
||||
public async rotate(angle: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||
await this.transform(
|
||||
{
|
||||
rotate: {
|
||||
angle,
|
||||
},
|
||||
origin,
|
||||
},
|
||||
effectTiming,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the graph to an absolute angle.
|
||||
* @param angle
|
||||
* @param origin
|
||||
* @param effectTiming
|
||||
*/
|
||||
public async rotateTo(angle: number, origin?: PointLike, effectTiming?: CameraAnimationOptions) {
|
||||
await this.rotate(angle - this.canvas.getCamera().getRoll(), origin, effectTiming);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit the graph content to the view.
|
||||
* @param padding padding while fitting
|
||||
* @param rules rules for fitting
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param options.padding padding while fitting
|
||||
* @param options.rules rules for fitting
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public fitView(padding?: Padding, rules?: FitViewRules, animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public async fitView(
|
||||
options?: {
|
||||
padding: Padding;
|
||||
rules: FitViewRules;
|
||||
},
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) {
|
||||
const { padding, rules } = options || {};
|
||||
const [top, right, bottom, left] = padding ? formatPadding(padding) : [0, 0, 0, 0];
|
||||
const { direction = 'both', ratioRule = 'min' } = rules || {};
|
||||
|
||||
// Get the bounds of the whole graph.
|
||||
const {
|
||||
center: [graphCenterX, graphCenterY],
|
||||
halfExtents,
|
||||
} = this.canvas.document.documentElement.getBounds();
|
||||
const origin = this.canvas.canvas2Viewport({ x: graphCenterX, y: graphCenterY });
|
||||
const { width: viewportWidth, height: viewportHeight } = this.canvas.getConfig();
|
||||
|
||||
const graphWidth = halfExtents[0] * 2;
|
||||
const graphHeight = halfExtents[1] * 2;
|
||||
const tlInCanvas = this.canvas.viewport2Canvas({ x: left, y: top });
|
||||
const brInCanvas = this.canvas.viewport2Canvas({
|
||||
x: viewportWidth! - right,
|
||||
y: viewportHeight! - bottom,
|
||||
});
|
||||
|
||||
const targetViewWidth = brInCanvas.x - tlInCanvas.x;
|
||||
const targetViewHeight = brInCanvas.y - tlInCanvas.y;
|
||||
|
||||
const wRatio = targetViewWidth / graphWidth;
|
||||
const hRatio = targetViewHeight / graphHeight;
|
||||
|
||||
let ratio: number;
|
||||
if (direction === 'x') {
|
||||
ratio = wRatio;
|
||||
} else if (direction === 'y') {
|
||||
ratio = hRatio;
|
||||
} else {
|
||||
ratio = ratioRule === 'max' ? Math.max(wRatio, hRatio) : Math.min(wRatio, hRatio);
|
||||
}
|
||||
|
||||
await this.transform(
|
||||
{
|
||||
translate: {
|
||||
dx: viewportWidth! / 2 - origin.x,
|
||||
dy: viewportHeight! / 2 - origin.y,
|
||||
},
|
||||
zoom: {
|
||||
ratio,
|
||||
},
|
||||
},
|
||||
effectTiming,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Fit the graph center to the view center.
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public fitCenter(animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public async fitCenter(effectTiming?: CameraAnimationOptions) {
|
||||
// Get the bounds of the whole graph.
|
||||
const {
|
||||
center: [graphCenterX, graphCenterY],
|
||||
} = this.canvas.document.documentElement.getBounds();
|
||||
await this.translateTo(
|
||||
this.canvas.canvas2Viewport({ x: graphCenterX, y: graphCenterY }),
|
||||
effectTiming,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Move the graph to make the item align the view center.
|
||||
* @param item node/edge/combo item or its id
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
public focusItem(ids: ID | ID[], animateCfg?: AnimateCfg) {
|
||||
// TODO
|
||||
public async focusItem(id: ID | ID[], effectTiming?: CameraAnimationOptions) {
|
||||
let bounds: AABB | null = null;
|
||||
for (const itemId of !isArray(id) ? [id] : id) {
|
||||
const item = this.itemController.getItemById(itemId);
|
||||
if (item) {
|
||||
const itemBounds = item.group.getBounds();
|
||||
if (!bounds) {
|
||||
bounds = itemBounds;
|
||||
} else {
|
||||
bounds.add(itemBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bounds) {
|
||||
const {
|
||||
center: [itemCenterX, itemCenterY],
|
||||
} = bounds;
|
||||
await this.translateTo(
|
||||
this.canvas.canvas2Viewport({ x: itemCenterX, y: itemCenterY }),
|
||||
effectTiming,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== item operations =====
|
||||
@ -371,7 +525,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
public getRelatedEdgesData(nodeId: ID, direction: 'in' | 'out' | 'both' = 'both'): EdgeModel[] {
|
||||
return this.dataController.findRelatedEdgeIds(nodeId, direction);
|
||||
}
|
||||
/**
|
||||
/**
|
||||
* Get one-hop node ids from a start node.
|
||||
* @param nodeId id of the start node
|
||||
* @returns one-hop node ids
|
||||
@ -380,7 +534,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
public getNeighborNodesData(nodeId: ID, direction: 'in' | 'out' | 'both' = 'both'): NodeModel[] {
|
||||
return this.dataController.findNeighborNodeIds(nodeId, direction);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find items which has the state.
|
||||
* @param itemType item type
|
||||
@ -430,7 +584,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
type: itemType,
|
||||
changes: graphCore.reduceChanges(event.changes),
|
||||
graphCore,
|
||||
theme: specification
|
||||
theme: specification,
|
||||
});
|
||||
});
|
||||
|
||||
@ -466,7 +620,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
type: itemType,
|
||||
changes: event.changes,
|
||||
graphCore,
|
||||
theme: specification
|
||||
theme: specification,
|
||||
});
|
||||
});
|
||||
this.hooks.datachange.emit({
|
||||
@ -505,7 +659,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
type: itemType,
|
||||
changes: event.changes,
|
||||
graphCore,
|
||||
theme: specification
|
||||
theme: specification,
|
||||
});
|
||||
});
|
||||
|
||||
@ -587,7 +741,7 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns rendering bounding box. returns false if the item is not exist
|
||||
* @group Item
|
||||
*/
|
||||
public getRenderBBox(id: ID | undefined): AABB | false{
|
||||
public getRenderBBox(id: ID | undefined): AABB | false {
|
||||
if (!id) return this.canvas.getRoot().getRenderBounds();
|
||||
return this.itemController.getItemBBox(id);
|
||||
}
|
||||
@ -769,7 +923,11 @@ export default class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
|
||||
* @returns upserted shape or group
|
||||
* @group Interaction
|
||||
*/
|
||||
public drawTransient(type: ITEM_TYPE | SHAPE_TYPE, id: ID, config: { action: 'remove' | 'add' | 'update' | undefined, style: ShapeStyle}): DisplayObject {
|
||||
public drawTransient(
|
||||
type: ITEM_TYPE | SHAPE_TYPE,
|
||||
id: ID,
|
||||
config: { action: 'remove' | 'add' | 'update' | undefined; style: ShapeStyle },
|
||||
): DisplayObject {
|
||||
this.hooks.transientupdate.emit({ type, id, config, canvas: this.transientCanvas });
|
||||
return this.itemController.getTransient(String(id));
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { IAnimationEffectTiming } from '@antv/g';
|
||||
|
||||
export interface AnimateCfg {
|
||||
/**
|
||||
* Whether enable animation.
|
||||
@ -39,7 +41,7 @@ export interface AnimateCfg {
|
||||
* @type {function}
|
||||
*/
|
||||
resumeCallback?: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export type AnimateWhen = 'show' | 'exit' | 'update' | 'last';
|
||||
|
||||
@ -47,4 +49,7 @@ export interface AnimateAttr {
|
||||
when: AnimateWhen;
|
||||
type: string;
|
||||
[param: string]: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CameraAnimationOptions
|
||||
extends Pick<IAnimationEffectTiming, 'duration' | 'easing' | 'easingFunction'> {}
|
||||
|
@ -1,21 +1,24 @@
|
||||
import EventEmitter from '@antv/event-emitter';
|
||||
import { Canvas, AABB, DisplayObject } from '@antv/g';
|
||||
import { AABB, Canvas, DisplayObject, PointLike } from '@antv/g';
|
||||
import { ID } from '@antv/graphlib';
|
||||
import { Hooks } from '../types/hook';
|
||||
import { AnimateCfg } from './animate';
|
||||
import { CameraAnimationOptions } from './animate';
|
||||
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from './behavior';
|
||||
import { ComboModel, ComboUserModel } from './combo';
|
||||
import { Padding, Point } from './common';
|
||||
import { DataChangeType, GraphData } from './data';
|
||||
import { GraphData } from './data';
|
||||
import { EdgeModel, EdgeUserModel } from './edge';
|
||||
import { ITEM_TYPE, SHAPE_TYPE } from './item';
|
||||
import { LayoutOptions } from './layout';
|
||||
import { NodeModel, NodeUserModel } from './node';
|
||||
import { Specification } from './spec';
|
||||
import { ThemeRegistry } from './theme';
|
||||
import { FitViewRules, GraphAlignment } from './view';
|
||||
import { FitViewRules, GraphTransformOptions } from './view';
|
||||
|
||||
export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry, T extends ThemeRegistry = ThemeRegistry> extends EventEmitter {
|
||||
export interface IGraph<
|
||||
B extends BehaviorRegistry = BehaviorRegistry,
|
||||
T extends ThemeRegistry = ThemeRegistry,
|
||||
> extends EventEmitter {
|
||||
hooks: Hooks;
|
||||
canvas: Canvas;
|
||||
destroyed: boolean;
|
||||
@ -182,63 +185,98 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry, T extends
|
||||
* Move the graph with a relative vector.
|
||||
* @param dx x of the relative vector
|
||||
* @param dy y of the relative vector
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
move: (dx: number, dy: number, animateCfg?: AnimateCfg) => void;
|
||||
translate: (dx: number, dy: number, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
/**
|
||||
* Move the graph and align to a point.
|
||||
* @param x position on the canvas to align
|
||||
* @param y position on the canvas to align
|
||||
* @param alignment alignment of the graph content
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param point position on the canvas to align
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
moveTo: (x: number, y: number, alignment: GraphAlignment, animateCfg?: AnimateCfg) => void;
|
||||
translateTo: (point: PointLike, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
/**
|
||||
* Return the current zoom level of camera.
|
||||
* @returns current zoom
|
||||
*/
|
||||
getZoom: () => number;
|
||||
/**
|
||||
* Zoom the graph with a relative ratio.
|
||||
* @param ratio relative ratio to zoom
|
||||
* @param center zoom center
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
zoom: (ratio: number, center?: Point, animateCfg?: AnimateCfg) => void;
|
||||
zoom: (ratio: number, center?: Point, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
/**
|
||||
* Zoom the graph to a specified ratio.
|
||||
* @param toRatio specified ratio
|
||||
* @param center zoom center
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
zoomTo: (toRatio: number, center?: Point, animateCfg?: AnimateCfg) => void;
|
||||
zoomTo: (toRatio: number, center?: Point, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
/**
|
||||
* Rotate the graph with a relative angle in clockwise.
|
||||
* @param angle
|
||||
* @param center
|
||||
* @param effectTiming
|
||||
*/
|
||||
rotate: (angle: number, center?: Point, effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
/**
|
||||
* Rotate the graph to an absolute angle in clockwise.
|
||||
* @param toAngle
|
||||
* @param center
|
||||
* @param effectTiming
|
||||
*/
|
||||
rotateTo: (
|
||||
toAngle: number,
|
||||
center?: Point,
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Transform the graph with a CSS-Transform-like syntax.
|
||||
* @param options
|
||||
* @param effectTiming
|
||||
*/
|
||||
transform: (
|
||||
options: GraphTransformOptions,
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Stop the current transition of transform immediately.
|
||||
*/
|
||||
stopTransformTransition: () => void;
|
||||
/**
|
||||
* Return the center of viewport, e.g. for a 500 * 500 canvas, its center is [250, 250].
|
||||
*/
|
||||
getViewportCenter: () => PointLike;
|
||||
/**
|
||||
* Fit the graph content to the view.
|
||||
* @param padding padding while fitting
|
||||
* @param rules rules for fitting
|
||||
* @param animateCfg animation configurations
|
||||
* @param options.padding padding while fitting
|
||||
* @param options.rules rules for fitting
|
||||
* @param effectTiming animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
*/
|
||||
fitView: (padding?: Padding, rules?: FitViewRules, animateCfg?: AnimateCfg) => void;
|
||||
fitView: (
|
||||
options?: {
|
||||
padding: Padding;
|
||||
rules: FitViewRules;
|
||||
},
|
||||
effectTiming?: CameraAnimationOptions,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Fit the graph center to the view center.
|
||||
* @param animateCfg animation configurations
|
||||
* @param effectTiming animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
*/
|
||||
fitCenter: (animateCfg?: AnimateCfg) => void;
|
||||
fitCenter: (effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
/**
|
||||
* Move the graph to make the item align the view center.
|
||||
* @param item node/edge/combo item or its id
|
||||
* @param animateCfg animation configurations
|
||||
* @returns
|
||||
* @group View
|
||||
* @param effectTiming animation configurations
|
||||
*/
|
||||
focusItem: (ids: ID | ID[], animateCfg?: AnimateCfg) => void;
|
||||
focusItem: (id: ID | ID[], effectTiming?: CameraAnimationOptions) => Promise<void>;
|
||||
|
||||
// ===== item operations =====
|
||||
/**
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Canvas } from '@antv/g';
|
||||
import { DataChangeType, GraphCore, GraphData } from "./data";
|
||||
import { NodeModelData } from "./node";
|
||||
import { EdgeModelData } from "./edge";
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from "./item";
|
||||
import { GraphChange, ID } from "@antv/graphlib";
|
||||
import { LayoutOptions } from "./layout";
|
||||
import { ThemeSpecification } from "./theme";
|
||||
import { GraphChange, ID } from '@antv/graphlib';
|
||||
import { CameraAnimationOptions } from './animate';
|
||||
import { DataChangeType, GraphCore, GraphData } from './data';
|
||||
import { EdgeModelData } from './edge';
|
||||
import { ITEM_TYPE, ShapeStyle, SHAPE_TYPE } from './item';
|
||||
import { LayoutOptions } from './layout';
|
||||
import { NodeModelData } from './node';
|
||||
import { ThemeSpecification } from './theme';
|
||||
import { GraphTransformOptions } from './view';
|
||||
|
||||
export interface IHook<T> {
|
||||
name: string;
|
||||
@ -16,18 +18,23 @@ export interface IHook<T> {
|
||||
emitLinearAsync: (param: T) => Promise<void>;
|
||||
}
|
||||
|
||||
export type ViewportChangeHookParams = {
|
||||
transform: GraphTransformOptions;
|
||||
effectTiming?: CameraAnimationOptions;
|
||||
};
|
||||
|
||||
export interface Hooks {
|
||||
init: IHook<{
|
||||
canvases: {
|
||||
background: Canvas,
|
||||
main: Canvas,
|
||||
transient: Canvas
|
||||
}
|
||||
background: Canvas;
|
||||
main: Canvas;
|
||||
transient: Canvas;
|
||||
};
|
||||
}>;
|
||||
// data
|
||||
datachange: IHook<{
|
||||
type: DataChangeType;
|
||||
data: GraphData
|
||||
data: GraphData;
|
||||
}>;
|
||||
itemchange: IHook<{
|
||||
type: ITEM_TYPE;
|
||||
@ -35,7 +42,7 @@ export interface Hooks {
|
||||
graphCore: GraphCore;
|
||||
theme: ThemeSpecification;
|
||||
}>;
|
||||
render: IHook<{ graphCore: GraphCore, theme: ThemeSpecification }>; // TODO: define param template
|
||||
render: IHook<{ graphCore: GraphCore; theme: ThemeSpecification }>; // TODO: define param template
|
||||
layout: IHook<{ graphCore: GraphCore; options?: LayoutOptions }>; // TODO: define param template
|
||||
// 'updatelayout': IHook<any>; // TODO: define param template
|
||||
modechange: IHook<{ mode: string }>;
|
||||
@ -54,12 +61,12 @@ export interface Hooks {
|
||||
id: ID;
|
||||
canvas: Canvas;
|
||||
config: {
|
||||
style: ShapeStyle,
|
||||
action: 'remove' | 'add' | 'update' | undefined
|
||||
style: ShapeStyle;
|
||||
action: 'remove' | 'add' | 'update' | undefined;
|
||||
};
|
||||
}>;
|
||||
// TODO: define param template
|
||||
// 'viewportchange': IHook<any>; // TODO: define param template
|
||||
viewportchange: IHook<ViewportChangeHookParams>;
|
||||
// 'destroy': IHook<any>; // TODO: define param template
|
||||
// TODO: more timecycles here
|
||||
}
|
||||
|
@ -4,4 +4,27 @@ export interface FitViewRules {
|
||||
ratioRule?: 'max' | 'min'; // Ratio rule to fit.
|
||||
}
|
||||
|
||||
export type GraphAlignment = 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' | 'center' | [number, number];
|
||||
export type GraphAlignment =
|
||||
| 'left-top'
|
||||
| 'right-top'
|
||||
| 'left-bottom'
|
||||
| 'right-bottom'
|
||||
| 'center'
|
||||
| [number, number];
|
||||
|
||||
export type GraphTransformOptions = {
|
||||
translate?: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
};
|
||||
rotate?: {
|
||||
angle: number;
|
||||
};
|
||||
zoom?: {
|
||||
ratio: number;
|
||||
};
|
||||
origin?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
@ -140,7 +140,7 @@ export const formatPadding = (value, defaultArr = DEFAULT_LABEL_BG_PADDING) => {
|
||||
/**
|
||||
* Merge multiple shape style map including undefined value in incoming map.
|
||||
* @param styleMaps shapes' styles map array, the latter item in the array will be merged into the former
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export const mergeStyles = (styleMaps: ItemShapeStyles[]) => {
|
||||
let currentResult = styleMaps[0];
|
||||
@ -148,29 +148,29 @@ export const mergeStyles = (styleMaps: ItemShapeStyles[]) => {
|
||||
if (i > 0) currentResult = merge2Styles(currentResult, styleMap);
|
||||
});
|
||||
return currentResult;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge two shape style map including undefined value in incoming map.
|
||||
* @param styleMap1 shapes' styles map as current map
|
||||
* @param styleMap2 shapes' styles map as incoming map
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const merge2Styles = (styleMap1: ItemShapeStyles, styleMap2: ItemShapeStyles) => {
|
||||
if (!styleMap1) return clone(styleMap2);
|
||||
else if (!styleMap2) return clone(styleMap1);
|
||||
const mergedStyle = clone(styleMap1);
|
||||
Object.keys(styleMap2).forEach(shapeId => {
|
||||
Object.keys(styleMap2).forEach((shapeId) => {
|
||||
const style = styleMap2[shapeId];
|
||||
mergedStyle[shapeId] = mergedStyle[shapeId] || {};
|
||||
if (!style) return;
|
||||
Object.keys(style).forEach(styleName => {
|
||||
Object.keys(style).forEach((styleName) => {
|
||||
const value = style[styleName];
|
||||
mergedStyle[shapeId][styleName] = value;
|
||||
});
|
||||
});
|
||||
return mergedStyle;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether two polygons are intersected.
|
||||
@ -179,10 +179,10 @@ const merge2Styles = (styleMap1: ItemShapeStyles, styleMap2: ItemShapeStyles) =>
|
||||
*/
|
||||
export const isPolygonsIntersect = (points1: number[][], points2: number[][]): boolean => {
|
||||
const getBBox = (points): Partial<AABB> => {
|
||||
const xArr = points.map(p => p[0]);
|
||||
const yArr = points.map(p => p[1]);
|
||||
const xArr = points.map((p) => p[0]);
|
||||
const yArr = points.map((p) => p[1]);
|
||||
return {
|
||||
min: [Math.min.apply(null, xArr), Math.min.apply(null, yArr), 0],
|
||||
min: [Math.min.apply(null, xArr), Math.min.apply(null, yArr), 0],
|
||||
max: [Math.max.apply(null, xArr), Math.max.apply(null, yArr), 0],
|
||||
};
|
||||
};
|
||||
@ -235,7 +235,7 @@ export const isPolygonsIntersect = (points1: number[][], points2: number[][]): b
|
||||
|
||||
let isIn = false;
|
||||
// 判定点是否在多边形内部,一旦有一个点在另一个多边形内,则返回
|
||||
points2.forEach(point => {
|
||||
points2.forEach((point) => {
|
||||
if (isPointInPolygon(points1, point[0], point[1])) {
|
||||
isIn = true;
|
||||
return false;
|
||||
@ -244,7 +244,7 @@ export const isPolygonsIntersect = (points1: number[][], points2: number[][]): b
|
||||
if (isIn) {
|
||||
return true;
|
||||
}
|
||||
points1.forEach(point => {
|
||||
points1.forEach((point) => {
|
||||
if (isPointInPolygon(points2, point[0], point[1])) {
|
||||
isIn = true;
|
||||
return false;
|
||||
@ -257,7 +257,7 @@ export const isPolygonsIntersect = (points1: number[][], points2: number[][]): b
|
||||
const lines1 = parseToLines(points1);
|
||||
const lines2 = parseToLines(points2);
|
||||
let isIntersect = false;
|
||||
lines2.forEach(line => {
|
||||
lines2.forEach((line) => {
|
||||
if (lineIntersectPolygon(lines1, line)) {
|
||||
isIntersect = true;
|
||||
return false;
|
||||
@ -276,7 +276,7 @@ export const intersectBBox = (box1: Partial<AABB>, box2: Partial<AABB>) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether point is inside the polygon (ray algo)
|
||||
* Whether point is inside the polygon (ray algo)
|
||||
* @param points
|
||||
* @param x
|
||||
* @param y
|
||||
@ -320,7 +320,7 @@ export const isPointInPolygon = (points: number[][], x: number, y: number) => {
|
||||
* @param p1 begin of segment line
|
||||
* @param p2 end of segment line
|
||||
* @param q the point to be judged
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const onSegment = (p1, p2, q) => {
|
||||
if (
|
||||
@ -333,11 +333,11 @@ const onSegment = (p1, p2, q) => {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const lineIntersectPolygon = (lines, line) => {
|
||||
let isIntersect = false;
|
||||
lines.forEach(l => {
|
||||
lines.forEach((l) => {
|
||||
if (getLineIntersect(l.from, l.to, line.from, line.to)) {
|
||||
isIntersect = true;
|
||||
return false;
|
||||
|
608
packages/g6/tests/unit/view-spec.ts
Normal file
608
packages/g6/tests/unit/view-spec.ts
Normal file
@ -0,0 +1,608 @@
|
||||
import { Graph, Layout, LayoutMapping } from '@antv/layout';
|
||||
import { Circle } from '@antv/g';
|
||||
import G6, { IGraph, stdLib } from '../../src/index';
|
||||
import { data } from '../datasets/dataset1';
|
||||
const container = document.createElement('div');
|
||||
|
||||
// document.getElementById('__jest-electron-test-results__')?.style.position = 'absolute';
|
||||
document.querySelector('body')!.appendChild(container);
|
||||
|
||||
describe('viewport', () => {
|
||||
let graph: any;
|
||||
it('should translate viewport without animation correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 100,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', () => {
|
||||
graph.translate(250, 250);
|
||||
let [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBe(0);
|
||||
expect(py).toBe(0);
|
||||
|
||||
graph.translate(-250, -250);
|
||||
[px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBe(250);
|
||||
expect(py).toBe(250);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should translate viewport with animation correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
await graph.translate(249, 249, {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBe(-250);
|
||||
expect(translate.dy).toBe(-250);
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBe(251);
|
||||
expect(py).toBe(251);
|
||||
});
|
||||
|
||||
await graph.translateTo(
|
||||
{ x: 500, y: 500 },
|
||||
{
|
||||
duration: 2000,
|
||||
easing: 'ease-in',
|
||||
},
|
||||
);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should zoom viewport without animation correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
graph.once('viewportchange', ({ zoom }) => {
|
||||
expect(zoom.ratio).toBe(0.5);
|
||||
expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||
});
|
||||
await graph.zoom(0.5, { x: 250, y: 250 });
|
||||
expect(graph.getZoom()).toBe(0.5);
|
||||
|
||||
graph.once('viewportchange', ({ zoom }) => {
|
||||
expect(zoom.ratio).toBe(2);
|
||||
expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||
});
|
||||
await graph.zoom(2, { x: 250, y: 250 });
|
||||
|
||||
const op = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||
graph.once('viewportchange', ({ zoom }) => {
|
||||
expect(zoom.ratio).toBe(1.2);
|
||||
expect(graph.canvas.getCamera().getZoom()).toBe(1.2);
|
||||
|
||||
// Zoom origin should be fixed.
|
||||
const { x, y } = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||
expect(x).toBeCloseTo(op.x);
|
||||
expect(y).toBeCloseTo(op.y);
|
||||
});
|
||||
await graph.zoomTo(1.2, { x: 450, y: 250 });
|
||||
expect(graph.getZoom()).toBe(1.2);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should zoom viewport with animation correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
graph.once('viewportchange', ({ zoom }) => {
|
||||
expect(zoom.ratio).toBe(0.5);
|
||||
expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||
});
|
||||
await graph.zoom(
|
||||
0.5,
|
||||
{ x: 250, y: 250 },
|
||||
{
|
||||
duration: 2000,
|
||||
},
|
||||
);
|
||||
|
||||
graph.once('viewportchange', ({ zoom }) => {
|
||||
expect(zoom.ratio).toBe(2);
|
||||
expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||
expect(graph.canvas.getCamera().getPosition()[0]).toBe(250);
|
||||
expect(graph.canvas.getCamera().getPosition()[1]).toBe(250);
|
||||
});
|
||||
await graph.zoom(
|
||||
2,
|
||||
{ x: 250, y: 250 },
|
||||
{
|
||||
duration: 2000,
|
||||
},
|
||||
);
|
||||
|
||||
// const op = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||
// graph.once('viewportchange', ({ zoom }) => {
|
||||
// expect(zoom.ratio).toBe(3);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(3);
|
||||
// // Zoom origin should be fixed.
|
||||
// const { x, y } = graph.canvas.canvas2Viewport({ x: 450, y: 250 });
|
||||
// expect(x).toBeCloseTo(op.x);
|
||||
// expect(y).toBeCloseTo(op.y);
|
||||
// });
|
||||
// await graph.zoomTo(
|
||||
// 3,
|
||||
// { x: 450, y: 250 },
|
||||
// {
|
||||
// duration: 1000,
|
||||
// },
|
||||
// );
|
||||
|
||||
// graph.once('viewportchange', ({ zoom }) => {
|
||||
// expect(zoom.ratio).toBe(1 / 3);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||
// expect(graph.canvas.getCamera().getPosition()[0]).toBe(250);
|
||||
// expect(graph.canvas.getCamera().getPosition()[1]).toBe(250);
|
||||
// });
|
||||
// await graph.zoomTo(
|
||||
// 1,
|
||||
// { x: 250, y: 250 },
|
||||
// {
|
||||
// duration: 1000,
|
||||
// },
|
||||
// );
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should rotate viewport correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
graph.once('viewportchange', ({ rotate }) => {
|
||||
expect(rotate.angle).toBe(30);
|
||||
expect(graph.canvas.getCamera().getRoll()).toBe(30);
|
||||
});
|
||||
await graph.rotateTo(30, undefined, {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
graph.once('viewportchange', ({ rotate }) => {
|
||||
expect(rotate.angle).toBe(30);
|
||||
expect(graph.canvas.getCamera().getRoll()).toBe(60);
|
||||
});
|
||||
await graph.rotateTo(60, undefined, {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
graph.once('viewportchange', ({ rotate }) => {
|
||||
expect(rotate.angle).toBe(-30);
|
||||
expect(graph.canvas.getCamera().getRoll()).toBe(30);
|
||||
});
|
||||
await graph.rotateTo(30, undefined, {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
graph.once('viewportchange', ({ rotate }) => {
|
||||
expect(rotate.angle).toBe(-29);
|
||||
expect(graph.canvas.getCamera().getRoll()).toBe(1);
|
||||
});
|
||||
// without animation
|
||||
await graph.rotate(-29);
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform viewport without animation correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
const origin = new Circle({
|
||||
style: {
|
||||
cx: 450,
|
||||
cy: 250,
|
||||
r: 30,
|
||||
fill: 'green',
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
graph.canvas.appendChild(origin);
|
||||
|
||||
// graph.once('viewportchange', ({ zoom }) => {
|
||||
// expect(zoom.ratio).toBe(0.5);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||
// });
|
||||
// await graph.transform({
|
||||
// zoom: {
|
||||
// ratio: 0.5,
|
||||
// },
|
||||
// origin: { x: 250, y: 250 },
|
||||
// });
|
||||
|
||||
// graph.once('viewportchange', ({ zoom, rotate }) => {
|
||||
// expect(zoom.ratio).toBe(2);
|
||||
// expect(rotate.angle).toBe(30);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||
// expect(graph.canvas.getCamera().getRoll()).toBe(30);
|
||||
// });
|
||||
// await graph.transform({
|
||||
// zoom: {
|
||||
// ratio: 2,
|
||||
// },
|
||||
// rotate: {
|
||||
// angle: 30,
|
||||
// },
|
||||
// origin: { x: 250, y: 250 },
|
||||
// });
|
||||
|
||||
// graph.once('viewportchange', ({ rotate }) => {
|
||||
// expect(rotate.angle).toBe(-30);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(1);
|
||||
// expect(graph.canvas.getCamera().getRoll()).toBe(0);
|
||||
// });
|
||||
// await graph.transform({
|
||||
// rotate: {
|
||||
// angle: -30,
|
||||
// },
|
||||
// origin: { x: 250, y: 250 },
|
||||
// });
|
||||
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBe(-250);
|
||||
expect(translate.dy).toBe(-250);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||
// expect(graph.canvas.getCamera().getRoll()).toBe(0);
|
||||
});
|
||||
await graph.transform({
|
||||
translate: {
|
||||
dx: -250,
|
||||
dy: -250,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('viewportchange', ({ zoom }) => {
|
||||
expect(zoom.ratio).toBe(0.5);
|
||||
expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||
});
|
||||
await graph.transform({
|
||||
zoom: {
|
||||
ratio: 0.5,
|
||||
},
|
||||
origin: { x: 0, y: 0 },
|
||||
});
|
||||
|
||||
await graph.transform({
|
||||
translate: {
|
||||
dx: 250,
|
||||
dy: 250,
|
||||
},
|
||||
zoom: {
|
||||
ratio: 4,
|
||||
},
|
||||
});
|
||||
await graph.transform({
|
||||
translate: {
|
||||
dx: -250,
|
||||
dy: -250,
|
||||
},
|
||||
});
|
||||
|
||||
// graph.once('viewportchange', ({ rotate }) => {
|
||||
// // expect(rotate.angle).toBe(0);
|
||||
// expect(graph.canvas.getCamera().getZoom()).toBe(0.5);
|
||||
// // expect(graph.canvas.getCamera().getRoll()).toBe(0);
|
||||
// });
|
||||
// await graph.transform({
|
||||
// // rotate: {
|
||||
// // angle: 0,
|
||||
// // },
|
||||
// zoom: {
|
||||
// ratio: 1,
|
||||
// },
|
||||
// origin: { x: 450, y: 250 },
|
||||
// });
|
||||
|
||||
graph.destroy();
|
||||
origin.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop the current transition of transform correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBe(-250);
|
||||
expect(translate.dy).toBe(-250);
|
||||
});
|
||||
await graph.transform(
|
||||
{
|
||||
translate: {
|
||||
dx: -250,
|
||||
dy: -250,
|
||||
},
|
||||
},
|
||||
{
|
||||
duration: 2000,
|
||||
},
|
||||
);
|
||||
graph.stopTransformTransition();
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fitCenter with transition correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
await graph.translate(249, 249, {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBeCloseTo(-249);
|
||||
expect(translate.dy).toBeCloseTo(-249);
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(250);
|
||||
expect(py).toBeCloseTo(250);
|
||||
});
|
||||
|
||||
await graph.fitCenter({
|
||||
duration: 2000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
|
||||
await graph.zoom(0.5);
|
||||
await graph.translate(249, 249);
|
||||
graph.once('viewportchange', ({ translate }) => {
|
||||
expect(translate.dx).toBeCloseTo(-249);
|
||||
expect(translate.dy).toBeCloseTo(-249);
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(250);
|
||||
expect(py).toBeCloseTo(250);
|
||||
});
|
||||
await graph.fitCenter();
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should focusItem with transition correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].id).toBe('Argentina');
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
expect(nodesData[4].id).toBe('Colombia');
|
||||
expect(nodesData[4].data.x).toBeCloseTo(391.421356237309);
|
||||
expect(nodesData[4].data.y).toBeCloseTo(391.4213562373095);
|
||||
|
||||
graph.once('viewportchange', () => {
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(450);
|
||||
expect(py).toBeCloseTo(250);
|
||||
});
|
||||
await graph.focusItem('Argentina', {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
|
||||
graph.once('viewportchange', () => {
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(391.421356237309);
|
||||
expect(py).toBeCloseTo(391.4213562373095);
|
||||
});
|
||||
await graph.focusItem('Colombia', {
|
||||
duration: 1000,
|
||||
});
|
||||
|
||||
await graph.focusItem('Spain', {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
|
||||
graph.once('viewportchange', () => {
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBe(450);
|
||||
expect(py).toBe(250);
|
||||
});
|
||||
await graph.focusItem('Argentina');
|
||||
|
||||
graph.once('viewportchange', () => {
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(250);
|
||||
expect(py).toBeCloseTo(250);
|
||||
});
|
||||
await graph.focusItem(nodesData.map((node) => node.id));
|
||||
|
||||
graph.focusItem('Non existed node');
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fitView with transition correctly.', (done) => {
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width: 500,
|
||||
height: 500,
|
||||
type: 'graph',
|
||||
data,
|
||||
layout: {
|
||||
type: 'circular',
|
||||
center: [250, 250],
|
||||
radius: 200,
|
||||
},
|
||||
});
|
||||
|
||||
graph.once('afterlayout', async () => {
|
||||
const nodesData = graph.getAllNodesData();
|
||||
expect(nodesData[0].id).toBe('Argentina');
|
||||
expect(nodesData[0].data.x).toBe(450);
|
||||
expect(nodesData[0].data.y).toBe(250);
|
||||
|
||||
expect(nodesData[4].id).toBe('Colombia');
|
||||
expect(nodesData[4].data.x).toBeCloseTo(391.421356237309);
|
||||
expect(nodesData[4].data.y).toBeCloseTo(391.4213562373095);
|
||||
|
||||
graph.once('viewportchange', () => {
|
||||
const [px, py] = graph.canvas.getCamera().getPosition();
|
||||
expect(px).toBeCloseTo(450);
|
||||
expect(py).toBeCloseTo(250);
|
||||
});
|
||||
await graph.focusItem('Argentina', {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
await graph.zoom(0.5, undefined, {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
await graph.fitView(
|
||||
{
|
||||
padding: [50, 50, 50, 50],
|
||||
},
|
||||
{
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
},
|
||||
);
|
||||
await graph.translate(100, 100, {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
await graph.fitView(
|
||||
{
|
||||
padding: [150, 100],
|
||||
rules: {
|
||||
direction: 'both',
|
||||
ratioRule: 'min',
|
||||
},
|
||||
},
|
||||
{
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
},
|
||||
);
|
||||
await graph.fitView(undefined, {
|
||||
duration: 1000,
|
||||
easing: 'ease-in',
|
||||
});
|
||||
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user