From 3f775f9ef80c859f602e8e70150b8db9de430cd7 Mon Sep 17 00:00:00 2001
From: Yuxin <55794321+yvonneyx@users.noreply.github.com>
Date: Wed, 17 Jul 2024 16:23:53 +0800
Subject: [PATCH] feat(behavior): hide elements when zooming, scrolling and
dragging canvas (#6054)
* feat(behavior): hide elements when zooming,scrolling and dragging canvas
* fix: fix cr issues
* test: supplement viewport transfron test
* refactor: rename ViewportEvent to IViewportEvent
---
.../behavior-optimize-viewport-transform.ts | 31 +
packages/g6/__tests__/demos/index.ts | 1 +
.../after-viewport-change.svg | 1427 +++++++++++++++++
.../optimize-viewport-transform/default.svg | 1427 +++++++++++++++++
.../viewport-change.svg | 1427 +++++++++++++++++
.../optimize-viewport-transform.spec.ts | 36 +
packages/g6/src/behaviors/index.ts | 2 +
.../behaviors/optimize-viewport-transform.ts | 127 ++
packages/g6/src/registry/build-in.ts | 18 +-
packages/g6/src/utils/visibility.ts | 11 +-
.../examples/behavior/canvas/demo/meta.json | 16 +
.../examples/behavior/canvas/demo/optimize.js | 27 +
.../examples/behavior/canvas/demo/zoom.js | 22 +
13 files changed, 4562 insertions(+), 10 deletions(-)
create mode 100644 packages/g6/__tests__/demos/behavior-optimize-viewport-transform.ts
create mode 100644 packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/after-viewport-change.svg
create mode 100644 packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/default.svg
create mode 100644 packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/viewport-change.svg
create mode 100644 packages/g6/__tests__/unit/behaviors/optimize-viewport-transform.spec.ts
create mode 100644 packages/g6/src/behaviors/optimize-viewport-transform.ts
create mode 100644 packages/site/examples/behavior/canvas/demo/optimize.js
create mode 100644 packages/site/examples/behavior/canvas/demo/zoom.js
diff --git a/packages/g6/__tests__/demos/behavior-optimize-viewport-transform.ts b/packages/g6/__tests__/demos/behavior-optimize-viewport-transform.ts
new file mode 100644
index 00000000000..0179f7fa3d7
--- /dev/null
+++ b/packages/g6/__tests__/demos/behavior-optimize-viewport-transform.ts
@@ -0,0 +1,31 @@
+import { Graph } from '@/src';
+import data from '@@/dataset/cluster.json';
+
+export const behaviorOptimizeViewportTransform: TestCase = async (context) => {
+ const graph = new Graph({
+ ...context,
+ data,
+ layout: {
+ type: 'd3-force',
+ },
+ node: {
+ style: {
+ iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
+ labelFontSize: 8,
+ labelText: (datum) => datum.id,
+ size: 20,
+ },
+ },
+ edge: {
+ style: {
+ labelFontSize: 8,
+ labelText: (datum) => datum.id,
+ },
+ },
+ behaviors: ['drag-canvas', 'zoom-canvas', 'scroll-canvas', 'optimize-viewport-transform'],
+ });
+
+ await graph.render();
+
+ return graph;
+};
diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts
index 0b426c3b385..ba6f25b071a 100644
--- a/packages/g6/__tests__/demos/index.ts
+++ b/packages/g6/__tests__/demos/index.ts
@@ -15,6 +15,7 @@ export { behaviorExpandCollapseNode } from './behavior-expand-collapse-node';
export { behaviorFocusElement } from './behavior-focus-element';
export { behaviorHoverActivate } from './behavior-hover-activate';
export { behaviorLassoSelect } from './behavior-lasso-select';
+export { behaviorOptimizeViewportTransform } from './behavior-optimize-viewport-transform';
export { behaviorScrollCanvas } from './behavior-scroll-canvas';
export { behaviorZoomCanvas } from './behavior-zoom-canvas';
export { caseIndentedTree } from './case-indented-tree';
diff --git a/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/after-viewport-change.svg b/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/after-viewport-change.svg
new file mode 100644
index 00000000000..74a7672ebc2
--- /dev/null
+++ b/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/after-viewport-change.svg
@@ -0,0 +1,1427 @@
+
\ No newline at end of file
diff --git a/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/default.svg b/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/default.svg
new file mode 100644
index 00000000000..b1551da4484
--- /dev/null
+++ b/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/default.svg
@@ -0,0 +1,1427 @@
+
\ No newline at end of file
diff --git a/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/viewport-change.svg b/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/viewport-change.svg
new file mode 100644
index 00000000000..4be55bc3573
--- /dev/null
+++ b/packages/g6/__tests__/snapshots/behaviors/optimize-viewport-transform/viewport-change.svg
@@ -0,0 +1,1427 @@
+
\ No newline at end of file
diff --git a/packages/g6/__tests__/unit/behaviors/optimize-viewport-transform.spec.ts b/packages/g6/__tests__/unit/behaviors/optimize-viewport-transform.spec.ts
new file mode 100644
index 00000000000..3c9eeb29f76
--- /dev/null
+++ b/packages/g6/__tests__/unit/behaviors/optimize-viewport-transform.spec.ts
@@ -0,0 +1,36 @@
+import { behaviorOptimizeViewportTransform } from '@/__tests__/demos';
+import { GraphEvent, type Graph } from '@/src';
+import { createDemoGraph } from '@@/utils';
+
+describe('behavior optimize canvas', () => {
+ let graph: Graph;
+
+ beforeAll(async () => {
+ graph = await createDemoGraph(behaviorOptimizeViewportTransform, { animation: false });
+ });
+
+ it('viewport', async () => {
+ await expect(graph).toMatchSnapshot(__filename);
+
+ graph.emit(GraphEvent.BEFORE_TRANSFORM, {
+ type: GraphEvent.BEFORE_TRANSFORM,
+ data: {
+ mode: 'relative',
+ translate: [0, -3],
+ },
+ });
+ await expect(graph).toMatchSnapshot(__filename, 'viewport-change');
+ graph.emit(GraphEvent.AFTER_TRANSFORM, {
+ type: GraphEvent.AFTER_TRANSFORM,
+ data: {
+ mode: 'relative',
+ translate: [0, -1],
+ },
+ });
+ await expect(graph).toMatchSnapshot(__filename, 'after-viewport-change');
+ });
+
+ it('destroy', () => {
+ graph.destroy();
+ });
+});
diff --git a/packages/g6/src/behaviors/index.ts b/packages/g6/src/behaviors/index.ts
index 85972af6bf6..8f05867237b 100644
--- a/packages/g6/src/behaviors/index.ts
+++ b/packages/g6/src/behaviors/index.ts
@@ -9,6 +9,7 @@ export { DragElementForce } from './drag-element-force';
export { FocusElement } from './focus-element';
export { HoverActivate } from './hover-activate';
export { LassoSelect } from './lasso-select';
+export { OptimizeViewportTransform } from './optimize-viewport-transform';
export { ScrollCanvas } from './scroll-canvas';
export { ZoomCanvas } from './zoom-canvas';
@@ -23,5 +24,6 @@ export type { DragElementForceOptions } from './drag-element-force';
export type { FocusElementOptions } from './focus-element';
export type { HoverActivateOptions } from './hover-activate';
export type { LassoSelectOptions } from './lasso-select';
+export type { OptimizeViewportTransformOptions } from './optimize-viewport-transform';
export type { ScrollCanvasOptions } from './scroll-canvas';
export type { ZoomCanvasOptions } from './zoom-canvas';
diff --git a/packages/g6/src/behaviors/optimize-viewport-transform.ts b/packages/g6/src/behaviors/optimize-viewport-transform.ts
new file mode 100644
index 00000000000..9b3a9671ef6
--- /dev/null
+++ b/packages/g6/src/behaviors/optimize-viewport-transform.ts
@@ -0,0 +1,127 @@
+import type { BaseStyleProps, DisplayObject } from '@antv/g';
+import { debounce, isFunction } from '@antv/util';
+import { GraphEvent } from '../constants';
+import type { RuntimeContext } from '../runtime/types';
+import type { IViewportEvent } from '../types/event';
+import { setVisibility } from '../utils/visibility';
+import type { BaseBehaviorOptions } from './base-behavior';
+import { BaseBehavior } from './base-behavior';
+
+/**
+ * 画布优化交互配置项
+ *
+ * Canvas optimization behavior options
+ */
+export interface OptimizeViewportTransformOptions extends BaseBehaviorOptions {
+ /**
+ * 是否启用画布优化功能
+ *
+ * Whether to enable canvas optimization function
+ * @defaultValue true
+ */
+ enable?: boolean | ((event: IViewportEvent) => boolean);
+ /**
+ * 始终保留的图形类名。操作画布过程中会隐藏元素reservedShapes(除了指定类名的图形),以提高性能
+ *
+ * Persistently reserved shape classnames. Elements are hidden during canvas manipulation (except for shapes with specified classnames) to enhance performance.
+ * @defaultValue `{ node: ['key'] }`
+ */
+ shapes?: {
+ node?: string[];
+ edge?: string[];
+ combo?: string[];
+ };
+ /**
+ * 设置防抖时间
+ *
+ * Set debounce time
+ * @defaultValue 200
+ */
+ debounce?: number;
+}
+
+/**
+ * 操作画布过程中隐藏元素
+ *
+ * Hide elements during canvas operations (dragging, zooming, scrolling)
+ */
+export class OptimizeViewportTransform extends BaseBehavior {
+ static defaultOptions: Partial = {
+ enable: true,
+ debounce: 200,
+ shapes: { node: ['key'] },
+ };
+
+ private isVisible: boolean = true;
+
+ constructor(context: RuntimeContext, options: OptimizeViewportTransformOptions) {
+ super(context, Object.assign({}, OptimizeViewportTransform.defaultOptions, options));
+ this.bindEvents();
+ }
+
+ private filterShapes = (shapes: DisplayObject[], classnames?: string[]) => {
+ return shapes.filter((shape) => shape.className && !classnames?.includes(shape.className));
+ };
+
+ private setElementsVisibility = (
+ elements: DisplayObject[],
+ visibility: BaseStyleProps['visibility'],
+ excludedClassnames?: string[],
+ ) => {
+ elements.forEach((element) => {
+ setVisibility(
+ element,
+ visibility,
+ excludedClassnames && ((shapes) => this.filterShapes(shapes, excludedClassnames)),
+ );
+ });
+ };
+
+ private hideShapes = (event: IViewportEvent) => {
+ if (!this.validate(event) || !this.isVisible) return;
+
+ const { element } = this.context;
+ const { shapes = {} } = this.options;
+ this.setElementsVisibility(element!.getNodes(), 'hidden', shapes.node);
+ this.setElementsVisibility(element!.getEdges(), 'hidden', shapes.edge);
+ this.setElementsVisibility(element!.getCombos(), 'hidden', shapes.combo);
+ this.isVisible = false;
+ };
+
+ private showShapes = debounce((event: IViewportEvent) => {
+ if (!this.validate(event) || this.isVisible) return;
+
+ const { element } = this.context;
+ this.setElementsVisibility(element!.getNodes(), 'visible');
+ this.setElementsVisibility(element!.getEdges(), 'visible');
+ this.setElementsVisibility(element!.getCombos(), 'visible');
+ this.isVisible = true;
+ }, this.options.debounce);
+
+ private bindEvents() {
+ const { graph } = this.context;
+
+ graph.on(GraphEvent.BEFORE_TRANSFORM, this.hideShapes);
+ graph.on(GraphEvent.AFTER_TRANSFORM, this.showShapes);
+ }
+
+ private unbindEvents() {
+ const { graph } = this.context;
+
+ graph.off(GraphEvent.BEFORE_TRANSFORM, this.hideShapes);
+ graph.off(GraphEvent.AFTER_TRANSFORM, this.showShapes);
+ }
+
+ private validate(event: IViewportEvent) {
+ if (this.destroyed) return false;
+
+ const { enable } = this.options;
+ if (isFunction(enable)) return enable(event);
+ return !!enable;
+ }
+
+ public destroy() {
+ this.unbindEvents();
+ super.destroy();
+ }
+}
diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts
index 4dad0748615..06746e0a760 100644
--- a/packages/g6/src/registry/build-in.ts
+++ b/packages/g6/src/registry/build-in.ts
@@ -10,6 +10,7 @@ import {
FocusElement,
HoverActivate,
LassoSelect,
+ OptimizeViewportTransform,
ScrollCanvas,
ZoomCanvas,
} from '../behaviors';
@@ -94,18 +95,19 @@ export const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
translate: Translate,
},
behavior: {
- 'zoom-canvas': ZoomCanvas,
+ 'brush-select': BrushSelect,
+ 'click-select': ClickSelect,
+ 'collapse-expand': CollapseExpand,
+ 'create-edge': CreateEdge,
'drag-canvas': DragCanvas,
- 'drag-element': DragElement,
'drag-element-force': DragElementForce,
- 'scroll-canvas': ScrollCanvas,
- 'collapse-expand': CollapseExpand,
- 'click-select': ClickSelect,
- 'hover-activate': HoverActivate,
+ 'drag-element': DragElement,
'focus-element': FocusElement,
- 'create-edge': CreateEdge,
- 'brush-select': BrushSelect,
+ 'hover-activate': HoverActivate,
'lasso-select': LassoSelect,
+ 'optimize-viewport-transform': OptimizeViewportTransform,
+ 'scroll-canvas': ScrollCanvas,
+ 'zoom-canvas': ZoomCanvas,
},
combo: {
circle: CircleCombo,
diff --git a/packages/g6/src/utils/visibility.ts b/packages/g6/src/utils/visibility.ts
index f3074512601..a1d8cfd112e 100644
--- a/packages/g6/src/utils/visibility.ts
+++ b/packages/g6/src/utils/visibility.ts
@@ -10,13 +10,20 @@ const PropertyKey = 'visibility';
* Set the visibility of the shape instance
* @param shape - 图形实例 | shape instance
* @param visibility - 可见性 | visibility
+ * @param filter - 筛选出需要设置可见性的图形 | Filter out the shapes that need to set visibility
* @remarks
* 在设置 enableCSSParsing 为 false 的情况下,复合图形无法继承父属性,因此需要对所有子图形应用相同的可见性
*
* After setting enableCSSParsing to false, the compound shape cannot inherit the parent attribute, so the same visibility needs to be applied to all child shapes
*/
-export function setVisibility(shape: DisplayObject, visibility: BaseStyleProps['visibility']) {
- const shapes = [shape, ...getDescendantShapes(shape)];
+export function setVisibility(
+ shape: DisplayObject,
+ visibility: BaseStyleProps['visibility'],
+ filter?: (shapes: DisplayObject[]) => DisplayObject[],
+) {
+ let shapes = [shape, ...getDescendantShapes(shape)];
+
+ if (filter) shapes = filter?.(shapes);
shapes.forEach((sp) => {
if (!hasCachedStyle(sp, PropertyKey)) cacheStyle(sp, PropertyKey);
diff --git a/packages/site/examples/behavior/canvas/demo/meta.json b/packages/site/examples/behavior/canvas/demo/meta.json
index 63a5784fc9f..28c05432d5d 100644
--- a/packages/site/examples/behavior/canvas/demo/meta.json
+++ b/packages/site/examples/behavior/canvas/demo/meta.json
@@ -27,6 +27,22 @@
"en": "Scroll Y"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*c5DCSoAYsZQAAAAAAAAAAAAADmJ7AQ/original"
+ },
+ {
+ "filename": "zoom.js",
+ "title": {
+ "zh": "缩放画布",
+ "en": "Zoom Canvas"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1AXoRY9Bw-0AAAAAAAAAAAAADmJ7AQ/original"
+ },
+ {
+ "filename": "optimize.js",
+ "title": {
+ "zh": "拖拽缩放画布时隐藏元素",
+ "en": "Hide Elements When Dragging and Zooming"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AZ4IRJkIZr8AAAAAAAAAAAAADmJ7AQ/original"
}
]
}
diff --git a/packages/site/examples/behavior/canvas/demo/optimize.js b/packages/site/examples/behavior/canvas/demo/optimize.js
new file mode 100644
index 00000000000..7519de77ce1
--- /dev/null
+++ b/packages/site/examples/behavior/canvas/demo/optimize.js
@@ -0,0 +1,27 @@
+import { Graph } from '@antv/g6';
+
+const graph = new Graph({
+ container: 'container',
+ layout: {
+ type: 'grid',
+ },
+ data: {
+ nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],
+ edges: [
+ { source: 'node1', target: 'node2' },
+ { source: 'node1', target: 'node3' },
+ { source: 'node1', target: 'node4' },
+ { source: 'node2', target: 'node3' },
+ { source: 'node3', target: 'node4' },
+ { source: 'node4', target: 'node5' },
+ ],
+ },
+ node: {
+ style: {
+ labelText: (datum) => datum.id,
+ },
+ },
+ behaviors: ['zoom-canvas', 'drag-canvas', 'scroll-canvas', 'optimize-viewport-transform'],
+});
+
+graph.render();
diff --git a/packages/site/examples/behavior/canvas/demo/zoom.js b/packages/site/examples/behavior/canvas/demo/zoom.js
new file mode 100644
index 00000000000..dba081c0e57
--- /dev/null
+++ b/packages/site/examples/behavior/canvas/demo/zoom.js
@@ -0,0 +1,22 @@
+import { Graph } from '@antv/g6';
+
+const graph = new Graph({
+ container: 'container',
+ layout: {
+ type: 'grid',
+ },
+ data: {
+ nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],
+ edges: [
+ { source: 'node1', target: 'node2' },
+ { source: 'node1', target: 'node3' },
+ { source: 'node1', target: 'node4' },
+ { source: 'node2', target: 'node3' },
+ { source: 'node3', target: 'node4' },
+ { source: 'node4', target: 'node5' },
+ ],
+ },
+ behaviors: ['zoom-canvas'],
+});
+
+graph.render();