Skip to content

Commit

Permalink
feat: tooltip plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
lxfu1 committed Mar 19, 2024
1 parent 75ea31f commit 049b2b8
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/g6/__tests__/demo/case/index.ts
Expand Up @@ -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';
50 changes: 50 additions & 0 deletions 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 `<div>${items[0].id}</div>`;
},
},
],
});

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;
};
5 changes: 3 additions & 2 deletions packages/g6/package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions 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';
252 changes: 252 additions & 0 deletions 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<FederatedMouseEvent> {
targetType: 'node' | 'edge' | 'combo';
}

export type ContentModel = {
items?: { [key: string]: unknown }[];
};

export interface TooltipOptions
extends BasePluginOptions,
Pick<TooltipStyleProps, 'position' | 'offset' | 'enterable' | 'style' | 'container'> {
/** <zh/> 触发方式 | <en/> Event type that triggers display of tooltip */
trigger?: 'pointerenter' | 'click';
/** <zh/> 自定义内容 | <en/> Function for getting tooltip content */
getContent?: (evt: IG6GraphEvent, item: ContentModel) => HTMLElement | string;
/** <zh/> 触发类型 | <en/> Types of items for which tooltip is allowed to be displayed */
itemTypes?: ('node' | 'edge' | 'combo')[];
}

export class Tooltip extends BasePlugin<TooltipOptions> {
static defaultOptions: Partial<TooltipOptions> = {
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<TooltipOptions>) {
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();
}
}
3 changes: 2 additions & 1 deletion packages/g6/src/registry/build-in.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -107,5 +107,6 @@ export const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
},
plugin: {
'grid-line': GridLine,
tooltip: Tooltip,
},
};

0 comments on commit 049b2b8

Please sign in to comment.