From 049b2b8c1f53e01087f436c212f5c6d0ad3659bb Mon Sep 17 00:00:00 2001
From: lxfu1 <954055752@qq.com>
Date: Tue, 19 Mar 2024 21:14:53 +0800
Subject: [PATCH] feat: tooltip plugin
---
packages/g6/__tests__/demo/case/index.ts | 1 +
.../g6/__tests__/demo/case/plugin-tooltip.ts | 50 ++++
packages/g6/package.json | 5 +-
packages/g6/src/plugins/index.ts | 2 +
packages/g6/src/plugins/tooltip.ts | 252 ++++++++++++++++++
packages/g6/src/registry/build-in.ts | 3 +-
6 files changed, 310 insertions(+), 3 deletions(-)
create mode 100644 packages/g6/__tests__/demo/case/plugin-tooltip.ts
create mode 100644 packages/g6/src/plugins/tooltip.ts
diff --git a/packages/g6/__tests__/demo/case/index.ts b/packages/g6/__tests__/demo/case/index.ts
index b4edf0cb217..73152964b47 100644
--- a/packages/g6/__tests__/demo/case/index.ts
+++ b/packages/g6/__tests__/demo/case/index.ts
@@ -43,5 +43,6 @@ export * from './layout-radial-prevent-overlap';
export * from './layout-radial-prevent-overlap-unstrict';
export * from './layout-radial-sort';
export * from './plugin-grid-line';
+export * from './plugin-tooltip';
export * from './theme';
export * from './viewport-fit';
diff --git a/packages/g6/__tests__/demo/case/plugin-tooltip.ts b/packages/g6/__tests__/demo/case/plugin-tooltip.ts
new file mode 100644
index 00000000000..baa8e0c73c9
--- /dev/null
+++ b/packages/g6/__tests__/demo/case/plugin-tooltip.ts
@@ -0,0 +1,50 @@
+import { Graph } from '@/src';
+import data from '@@/dataset/cluster.json';
+import { isObject } from '@antv/util';
+import type { STDTestCase } from '../types';
+
+export const pluginTooltip: STDTestCase = async (context) => {
+ const graph = new Graph({
+ ...context,
+ autoResize: true,
+ data,
+ layout: { type: 'd3force' },
+ behaviors: ['drag-canvas', 'drag-element'],
+ node: {
+ style: {
+ labelText: (d) => d.id,
+ },
+ },
+ plugins: [
+ {
+ type: 'tooltip',
+ getContent: (evt: any, { items }: { items: any[] }) => {
+ return `
${items[0].id}
`;
+ },
+ },
+ ],
+ });
+
+ await graph.render();
+
+ pluginTooltip.form = (panel) => {
+ const config = {
+ trigger: 'pointerenter',
+ };
+ return [
+ panel
+ .add(config, 'trigger', ['pointerenter', 'click'])
+ .name('Change Trigger Method')
+ .onChange((trigger: string) => {
+ graph.setPlugins((plugins) =>
+ plugins.map((plugin) => {
+ if (isObject(plugin) && plugin.type === 'tooltip') return { ...plugin, trigger };
+ return plugin;
+ }),
+ );
+ }),
+ ];
+ };
+
+ return graph;
+};
diff --git a/packages/g6/package.json b/packages/g6/package.json
index 2abc130a748..f2af24c7e57 100644
--- a/packages/g6/package.json
+++ b/packages/g6/package.json
@@ -34,7 +34,6 @@
"build:esm:watch": "rimraf ./esm && tsc --module ESNext --outDir esm --watch -p tsconfig.build.json",
"build:umd": "rimraf ./dist && rollup -c && npm run size",
"bundle-vis": "cross-env BUNDLE_VIS=1 npm run build:umd",
- "type-check": "tsc --noEmit",
"ci": "run-s lint type-check build test",
"coverage": "jest --coverage",
"coverage:open": "open coverage/lcov-report/index.html",
@@ -47,9 +46,11 @@
"start": "rimraf ./lib && tsc --module commonjs --outDir lib --watch",
"test": "npm run jest __tests__",
"test:integration": "npm run jest __tests__/integration",
- "test:unit": "npm run jest __tests__/unit"
+ "test:unit": "npm run jest __tests__/unit",
+ "type-check": "tsc --noEmit"
},
"dependencies": {
+ "@antv/component": "^1.0.2",
"@antv/event-emitter": "latest",
"@antv/g": "^5.18.25",
"@antv/g-canvas": "^1.11.27",
diff --git a/packages/g6/src/plugins/index.ts b/packages/g6/src/plugins/index.ts
index c5093cc5f74..22254cf5b94 100644
--- a/packages/g6/src/plugins/index.ts
+++ b/packages/g6/src/plugins/index.ts
@@ -1,5 +1,7 @@
export { BasePlugin } from './base-plugin';
export { GridLine } from './grid-line';
+export { Tooltip } from './tooltip';
export type { BasePluginOptions } from './base-plugin';
export type { GridLineOptions } from './grid-line';
+export type { TooltipOptions } from './tooltip';
diff --git a/packages/g6/src/plugins/tooltip.ts b/packages/g6/src/plugins/tooltip.ts
new file mode 100644
index 00000000000..52f83dee245
--- /dev/null
+++ b/packages/g6/src/plugins/tooltip.ts
@@ -0,0 +1,252 @@
+import type { TooltipStyleProps } from '@antv/component';
+import { Tooltip as TooltipComponent } from '@antv/component';
+import type { FederatedMouseEvent } from '@antv/g';
+import type { RuntimeContext } from '../runtime/types';
+import type { BehaviorEvent } from '../types';
+import type { BasePluginOptions } from './base-plugin';
+import { BasePlugin } from './base-plugin';
+
+interface IG6GraphEvent extends BehaviorEvent {
+ targetType: 'node' | 'edge' | 'combo';
+}
+
+export type ContentModel = {
+ items?: { [key: string]: unknown }[];
+};
+
+export interface TooltipOptions
+ extends BasePluginOptions,
+ Pick {
+ /** 触发方式 | Event type that triggers display of tooltip */
+ trigger?: 'pointerenter' | 'click';
+ /** 自定义内容 | Function for getting tooltip content */
+ getContent?: (evt: IG6GraphEvent, item: ContentModel) => HTMLElement | string;
+ /** 触发类型 | Types of items for which tooltip is allowed to be displayed */
+ itemTypes?: ('node' | 'edge' | 'combo')[];
+}
+
+export class Tooltip extends BasePlugin {
+ static defaultOptions: Partial = {
+ trigger: 'pointerenter',
+ position: 'top-right',
+ enterable: false,
+ itemTypes: ['node', 'edge', 'combo'],
+ style: {
+ '.tooltip': {
+ visibility: 'hidden',
+ },
+ },
+ container: {
+ x: 0,
+ y: 0,
+ },
+ };
+ private currentTarget: string | null = null;
+ private tooltipElement: TooltipComponent | null = null;
+ private $element: HTMLElement = document.createElement('div');
+
+ constructor(context: RuntimeContext, options: TooltipOptions) {
+ super(context, Object.assign({}, Tooltip.defaultOptions, options));
+ this.render();
+ this.bindEvents();
+ }
+
+ public getEvents(trigger: 'pointerenter' | 'click'): { [key: string]: Function } {
+ if (trigger === 'click') {
+ return {
+ 'node:click': this.onClick,
+ 'edge:click': this.onClick,
+ 'combo:click': this.onClick,
+ 'canvas:click': this.onPointerLeave,
+ afterremoveitem: this.onPointerLeave,
+ contextmenu: this.onPointerLeave,
+ drag: this.onPointerLeave,
+ };
+ }
+
+ return {
+ 'node:pointerenter': this.onPointerEnter,
+ 'node:pointermove': this.onPointerMove,
+ 'canvas:pointermove': this.onCanvasMove,
+ 'edge:pointerenter': this.onPointerEnter,
+ 'edge:pointermove': this.onPointerMove,
+ 'combo:pointerenter': this.onPointerEnter,
+ 'combo:pointermove': this.onPointerMove,
+ contextmenu: this.onPointerLeave,
+ 'node:drag': this.onPointerLeave,
+ };
+ }
+
+ public update(options: Partial) {
+ super.update(options);
+ this.unbundEvents(true);
+ if (this.tooltipElement) {
+ this.$element.removeChild(this.tooltipElement.HTMLTooltipElement);
+ }
+ this.tooltipElement = this.initTooltip();
+ }
+
+ private render() {
+ const { canvas } = this.context;
+ const $container = canvas.getContainer();
+ if (!$container) return;
+ this.$element.className = 'g6-tooltip';
+ $container.appendChild(this.$element);
+ this.tooltipElement = this.initTooltip();
+ }
+
+ private unbundEvents(isUpdate?: boolean) {
+ const { graph } = this.context;
+ const { trigger } = this.options;
+ /** The previous event binding needs to be removed when updating the trigger. */
+ const events = this.getEvents(!isUpdate ? trigger : trigger === 'click' ? 'pointerenter' : 'click');
+ Object.keys(events).forEach((eventName) => {
+ graph.off(eventName, events[eventName]);
+ });
+ }
+
+ private bindEvents() {
+ const { graph } = this.context;
+ const { trigger } = this.options;
+ const events = this.getEvents(trigger);
+ Object.keys(events).forEach((eventName) => {
+ graph.on(eventName, events[eventName]);
+ });
+ }
+
+ public onClick = (e: IG6GraphEvent) => {
+ const {
+ targetType,
+ target: { id },
+ } = e;
+ if (this.options.itemTypes.indexOf(targetType) === -1) return;
+ // click the same item twice, tooltip will be hidden
+ if (this.currentTarget === id) {
+ this.currentTarget = null;
+ this.hideTooltip(e);
+ } else {
+ this.currentTarget = id;
+ this.showTooltip(e);
+ }
+ };
+
+ public onPointerMove = (e: IG6GraphEvent) => {
+ const { targetType, target } = e;
+ if (this.options.itemTypes.indexOf(targetType) === -1) return;
+ if (!this.currentTarget || target.id === this.currentTarget) {
+ return;
+ }
+ this.showTooltip(e);
+ };
+
+ public onPointerLeave = (e: IG6GraphEvent) => {
+ this.hideTooltip(e);
+ this.currentTarget = null;
+ };
+
+ public onCanvasMove = (e: IG6GraphEvent) => {
+ this.hideTooltip(e);
+ this.currentTarget = null;
+ };
+
+ private onPointerEnter = (e: IG6GraphEvent) => {
+ const { targetType } = e;
+ if (this.options.itemTypes.indexOf(targetType) === -1) return;
+ this.showTooltip(e);
+ };
+
+ public showTooltip(e: IG6GraphEvent) {
+ const {
+ targetType,
+ client: { x, y },
+ target: { id, attributes },
+ } = e;
+ if (!this.tooltipElement) return;
+ const { getContent } = this.options;
+ const { model } = this.context;
+ const { color, stroke } = attributes;
+ this.currentTarget = id;
+ let items: { [key: string]: unknown }[] = [];
+ switch (targetType) {
+ case 'node':
+ items = model.getNodeData([id]);
+ break;
+ case 'edge':
+ items = model.getEdgeData([id]);
+ break;
+ case 'combo':
+ items = model.getComboData([id]);
+ break;
+ default:
+ break;
+ }
+ let tooltipContent: { [key: string]: unknown } = {};
+ if (getContent) {
+ tooltipContent.content = getContent(e, { items });
+ } else {
+ tooltipContent = {
+ title: targetType,
+ data: items.map((item) => {
+ return {
+ name: 'ID',
+ value: item.id || `${item.source} -> ${item.target}`,
+ color: color || stroke,
+ };
+ }),
+ };
+ }
+ this.tooltipElement.update({
+ x,
+ y,
+ style: {
+ '.tooltip': {
+ visibility: 'visible',
+ },
+ },
+ ...tooltipContent,
+ });
+ }
+
+ public hideTooltip(e: IG6GraphEvent) {
+ const {
+ client: { x, y },
+ } = e;
+ if (!this.tooltipElement) return;
+ this.tooltipElement.hide(x, y);
+ }
+
+ private initTooltip = () => {
+ const { style, position, enterable, container } = this.options;
+ const { canvas } = this.context;
+ const { center } = this.context.canvas.getBounds();
+ const [x, y] = center;
+ const [width, height] = canvas.getSize();
+ const tooltipElement = new TooltipComponent({
+ className: 'tooltip',
+ style: {
+ x,
+ y,
+ container,
+ bounding: {
+ x: 0,
+ y: 0,
+ width,
+ height,
+ },
+ position,
+ enterable,
+ title: '',
+ offset: [10, 10],
+ style,
+ },
+ });
+ this.$element.appendChild(tooltipElement.HTMLTooltipElement);
+ return tooltipElement;
+ };
+
+ public destroy(): void {
+ this.unbundEvents();
+ this.$element.remove();
+ super.destroy();
+ }
+}
diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts
index d12b62c95ec..ed5a2c7ecd1 100644
--- a/packages/g6/src/registry/build-in.ts
+++ b/packages/g6/src/registry/build-in.ts
@@ -36,7 +36,7 @@ import {
mindmap,
} from '../layouts';
import { blues, greens, oranges, spectral } from '../palettes';
-import { GridLine } from '../plugins';
+import { GridLine, Tooltip } from '../plugins';
import { dark, light } from '../themes';
import type { ExtensionRegistry } from './types';
@@ -107,5 +107,6 @@ export const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
},
plugin: {
'grid-line': GridLine,
+ tooltip: Tooltip,
},
};