feat(plugin): add fisheye plugin (#6235)

* feat(plugin): add fisheye plugin

* test: add fisheye unit tests

* docs: add fisheye demo

* fix: remove useless values

* fix: remove undefined value
This commit is contained in:
Yuxin 2024-08-27 11:27:51 +08:00 committed by GitHub
parent 6fc9a92cda
commit df139d4b6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 40226 additions and 2 deletions

View File

@ -114,6 +114,7 @@ export { pluginBubbleSets } from './plugin-bubble-sets';
export { pluginCameraSetting } from './plugin-camera-setting';
export { pluginContextmenu } from './plugin-contextmenu';
export { pluginEdgeFilterLens } from './plugin-edge-filter-lens';
export { pluginFisheye } from './plugin-fisheye';
export { pluginFullscreen } from './plugin-fullscreen';
export { pluginGridLine } from './plugin-grid-line';
export { pluginHistory } from './plugin-history';

View File

@ -0,0 +1,37 @@
import data from '@@/dataset/relations.json';
import type { NodeData } from '@antv/g6';
import { Graph } from '@antv/g6';
export const pluginFisheye: TestCase = async (context) => {
const graph = new Graph({
...context,
autoFit: 'view',
data,
node: {
style: {
size: (datum: NodeData) => datum.id.length * 2 + 10,
label: false,
labelText: (datum: NodeData) => datum.id,
labelBackground: true,
icon: false,
iconFontFamily: 'iconfont',
iconText: '\ue6f6',
iconFill: '#fff',
},
palette: {
type: 'group',
field: (datum: NodeData) => datum.id,
color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],
},
},
edge: {
style: {
stroke: '#e2e2e2',
},
},
plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],
});
await graph.render();
return graph;
};

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 158 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 173 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 182 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 178 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 185 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 172 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 185 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,135 @@
import { pluginFisheye } from '@@/demos';
import { CommonEvent, Graph } from '@antv/g6';
import { createDemoGraph, dispatchCanvasEvent } from '../../utils';
import { emitWheelEvent } from '../../utils/dom';
describe('plugin-fisheye', () => {
let graph: Graph;
beforeAll(async () => {
graph = await createDemoGraph(pluginFisheye, { animation: false });
});
afterAll(() => {
graph.destroy();
});
it('move lens by pointermove', async () => {
await expect(graph).toMatchSnapshot(__filename);
dispatchCanvasEvent(graph, CommonEvent.POINTER_MOVE, { canvas: { x: 420, y: 150 } });
await expect(graph).toMatchSnapshot(__filename, 'move-lens-pointermove');
});
it('move lens by drag', async () => {
graph.updatePlugin({ key: 'fisheye', trigger: 'drag' });
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });
dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 420, y: 150 } });
dispatchCanvasEvent(graph, CommonEvent.DRAG, { canvas: { x: 400, y: 180 } });
dispatchCanvasEvent(graph, CommonEvent.DRAG_END);
await expect(graph).toMatchSnapshot(__filename, 'move-lens-drag');
});
it('move lens by click', async () => {
graph.updatePlugin({ key: 'fisheye', trigger: 'click' });
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 180, y: 100 } });
await expect(graph).toMatchSnapshot(__filename, 'move-lens-click-1');
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 100 } });
await expect(graph).toMatchSnapshot(__filename, 'move-lens-click-2');
});
it('scale lens R/D by wheel', async () => {
graph.updatePlugin({ key: 'fisheye', scaleRBy: 'wheel', scaleDBy: 'unset' });
const emitWheelUpEvent = (count: number) => {
for (let i = 0; i < count; i++) {
emitWheelEvent(graph, { deltaX: 1, deltaY: 2, clientX: 420, clientY: 150 });
}
};
const emitWheelDownEvent = (count: number) => {
for (let i = 0; i < count; i++) {
emitWheelEvent(graph, { deltaX: -1, deltaY: -2, clientX: 420, clientY: 150 });
}
};
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });
emitWheelUpEvent(5);
await expect(graph).toMatchSnapshot(__filename, 'scale-R-wheel-larger');
emitWheelDownEvent(10);
await expect(graph).toMatchSnapshot(__filename, 'scale-R-wheel-smaller');
emitWheelUpEvent(5);
graph.updatePlugin({ key: 'fisheye', scaleRBy: 'unset', scaleDBy: 'wheel' });
emitWheelUpEvent(5);
await expect(graph).toMatchSnapshot(__filename, 'scale-D-wheel-larger');
emitWheelDownEvent(10);
await expect(graph).toMatchSnapshot(__filename, 'scale-D-wheel-smaller');
emitWheelUpEvent(5);
});
it('scale lens R/D by drag', async () => {
graph.updatePlugin({ key: 'fisheye', scaleRBy: 'drag', scaleDBy: 'unset' });
const emitPositionDragEvent = (count: number) => {
dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 420, y: 150 } });
for (let i = 0; i < count; i++) {
dispatchCanvasEvent(graph, CommonEvent.DRAG, { dx: 1, dy: -2 });
}
dispatchCanvasEvent(graph, CommonEvent.DRAG_END);
};
const emitNegativeDragEvent = (count: number) => {
dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 420, y: 150 } });
for (let i = 0; i < count; i++) {
dispatchCanvasEvent(graph, CommonEvent.DRAG, { dx: -1, dy: 2 });
}
dispatchCanvasEvent(graph, CommonEvent.DRAG_END);
};
emitPositionDragEvent(5);
await expect(graph).toMatchSnapshot(__filename, 'scale-R-drag-larger');
emitNegativeDragEvent(10);
await expect(graph).toMatchSnapshot(__filename, 'scale-R-drag-smaller');
emitPositionDragEvent(5);
graph.updatePlugin({ key: 'fisheye', scaleRBy: 'unset', scaleDBy: 'drag' });
emitPositionDragEvent(5);
await expect(graph).toMatchSnapshot(__filename, 'scale-D-drag-larger');
emitNegativeDragEvent(10);
await expect(graph).toMatchSnapshot(__filename, 'scale-D-drag-smaller');
emitPositionDragEvent(5);
});
it('show D percent', async () => {
graph.updatePlugin({ key: 'fisheye', showDPercent: false });
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });
await expect(graph).toMatchSnapshot(__filename, 'hide-D-percent');
});
it('lens style', async () => {
graph.updatePlugin({
key: 'fisheye',
showDPercent: true,
style: { fill: '#f00', lineDash: [5, 5], stroke: '#666' },
});
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });
await expect(graph).toMatchSnapshot(__filename, 'lens-style');
});
it('node style in lens', async () => {
graph.updatePlugin({ key: 'fisheye', style: { lineDash: 0 }, nodeStyle: { halo: true } });
dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });
await expect(graph).toMatchSnapshot(__filename, 'node-style');
});
});

View File

@ -0,0 +1,10 @@
import type { Graph } from '@/src';
import { CommonEvent } from '@/src';
export function emitWheelEvent(
graph: Graph,
options?: { deltaX: number; deltaY: number; clientX: number; clientY: number },
) {
const dom = graph.getCanvas().getContextService().getDomElement();
dom?.dispatchEvent(new WheelEvent(CommonEvent.WHEEL, options));
}

View File

@ -71,7 +71,6 @@ export interface ClickSelectOptions extends BaseBehaviorOptions {
* <zh/>
*
* <en/> The state to be applied to all unselected elements when some elements are selected, excluding the selected element and its affected neighbors
* @defaultValue undefined
*/
unselectedState?: State;
/**

View File

@ -21,7 +21,7 @@ export enum CanvasEvent {
*
* <en/> Triggered when the pointer enters
*/
POINTER_OVER = 'canvas:canvas:pointerover',
POINTER_OVER = 'canvas:pointerover',
/**
* <zh/>
*

View File

@ -68,6 +68,7 @@ export {
CameraSetting,
Contextmenu,
EdgeFilterLens,
Fisheye,
Fullscreen,
GridLine,
History,
@ -170,6 +171,7 @@ export type {
CameraSettingOptions,
ContextmenuOptions,
EdgeFilterLensOptions,
FisheyeOptions,
FullscreenOptions,
GridLineOptions,
HistoryOptions,

View File

@ -0,0 +1,481 @@
import { pick } from '@antv/util';
import { CommonEvent } from '../../constants';
import type { CircleStyleProps } from '../../elements';
import { Circle } from '../../elements';
import type { RuntimeContext } from '../../runtime/types';
import type { NodeData } from '../../spec';
import type { NodeStyle } from '../../spec/element/node';
import type { ID, IDragEvent, IPointerEvent, Node, Point, PointObject } from '../../types';
import { arrayDiff } from '../../utils/diff';
import { idOf } from '../../utils/id';
import { parsePoint, toPointObject } from '../../utils/point';
import { positionOf } from '../../utils/position';
import { distance } from '../../utils/vector';
import type { BasePluginOptions } from '../base-plugin';
import { BasePlugin } from '../base-plugin';
/**
* <zh/>
*
* <en/> Fisheye Plugin Options
*/
export interface FisheyeOptions extends BasePluginOptions {
/**
* <zh/>
* - `'pointermove'`
* - `'click'`
* - `'drag'`
*
* <en/> The way to move the fisheye lens
* - `'pointermove'`: always follow the mouse movement
* - `'click'`: move when the mouse is clicked
* - `'drag'`: move by dragging
* @defaultValue `'pointermove'`
*/
trigger?: 'pointermove' | 'drag' | 'click';
/**
* <zh/>
*
* <en/> The radius of the fisheye lens
* @remarks
* <img src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*unAvQqAb_NMAAAAAAAAAAAAADmJ7AQ/original" width="200" />
* @defaultValue 120
*/
r?: number;
/**
* <zh/> `scaleRBy` 使
*
* <en/> The maximum radius that the fisheye lens can be adjusted, used with `scaleRBy`
* @defaultValue
*/
maxR?: number;
/**
* <zh/> `scaleRBy` 使
*
* <en/> The minimum radius that the fisheye lens can be adjusted, used with `scaleRBy`
* @defaultValue 0
*/
minR?: number;
/**
* <zh/>
* - `'wheel'`
* - `'drag'`
*
* <en/> The way to adjust the range radius of the fisheye lens
* - `'wheel'`: adjust by wheel
* - `'drag'`: adjust by drag
* @remarks
* <zh/> `trigger``scaleRBy` `scaleDBy` `'drag'` `trigger` > `scaleRBy` > `scaleDBy` `scaleRBy` `scaleDBy` `'wheel'` `scaleRBy`
*
* <en/> If `trigger`, `scaleRBy`, and `scaleDBy` are set to `'drag'` at the same time, the priority order is `trigger` > `scaleRBy` > `scaleDBy`, and only the configuration item with the highest priority will be bound to the drag event. Similarly, if `scaleRBy` and `scaleDBy` are set to `'wheel'` at the same time, only `scaleRBy` will be bound to the wheel event
*/
scaleRBy?: 'wheel' | 'drag';
/**
* <zh/>
*
* <en/> Distortion factor
* @remarks
* <img src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4ITFR7GOl8UAAAAAAAAAAAAADmJ7AQ/original" width="200" />
* @defaultValue 1.5
*/
d?: number;
/**
* <zh/> `scaleDBy` 使
*
* <en/> The maximum distortion factor that the fisheye lens can be adjusted, used with `scaleDBy`
* @defaultValue 5
*/
maxD?: number;
/**
* <zh/> `scaleDBy` 使
*
* <en/> The minimum distortion factor that the fisheye lens can be adjusted, used with `scaleDBy`
* @defaultValue 0
*/
minD?: number;
/**
* <zh/>
* - `'wheel'`
* - `'drag'`
*
* <en/> The way to adjust the distortion factor of the fisheye lens
* - `'wheel'`: adjust by wheel
* - `'drag'`: adjust by drag
*/
scaleDBy?: 'wheel' | 'drag';
/**
* <zh/>
*
* <en/> Whether to display the value of the distortion factor in the fisheye lens
* @defaultValue true
*/
showDPercent?: boolean;
/**
* <zh/>
*
* <en/> Fisheye Lens Style
*/
style?: Partial<CircleStyleProps>;
/**
* <zh/>
*
* <en/> Node style in the fisheye lens
*/
nodeStyle?: NodeStyle | ((datum: NodeData) => NodeStyle);
/**
* <zh/>
*
* <en/> Whether to prevent the default event
* @defaultValue true
*/
preventDefault?: boolean;
}
const defaultLensStyle: Exclude<CircleStyleProps, 'r'> = {
fill: '#ccc',
fillOpacity: 0.1,
lineWidth: 2,
stroke: '#000',
strokeOpacity: 0.8,
labelFontSize: 12,
};
const R_DELTA = 0.05;
const D_DELTA = 0.1;
/**
* <zh/>
*
* <en/> Fisheye Distortion
* @remarks
* <zh/> Fisheye focus+context
*
* <en/> Fisheye is designed for focus+context exploration, it keeps the context and the relationships between context and the focus while magnifying the focus area.
*/
export class Fisheye extends BasePlugin<FisheyeOptions> {
static defaultOptions: Partial<FisheyeOptions> = {
trigger: 'pointermove',
r: 120,
d: 1.5,
maxD: 5,
minD: 0,
showDPercent: true,
style: {},
nodeStyle: { label: true },
preventDefault: true,
};
constructor(context: RuntimeContext, options: FisheyeOptions) {
super(context, Object.assign({}, Fisheye.defaultOptions, options));
this.bindEvents();
}
private lens!: Circle;
private r = this.options.r;
private d = this.options.d;
private get canvas() {
return this.context.canvas.getLayer('transient');
}
private get isLensOn() {
return this.lens && !this.lens.destroyed;
}
protected onCreateFisheye = (event: IPointerEvent) => {
if (this.options.trigger === 'drag' && this.isLensOn) return;
const origin = parsePoint(event.canvas as PointObject);
this.onMagnify(origin);
};
protected onMagnify = (origin: Point) => {
if (origin.some(isNaN)) return;
this.renderLens(origin);
this.renderFocusElements();
};
private renderLens = (origin: Point) => {
const style = Object.assign({}, defaultLensStyle, this.options.style);
if (!this.isLensOn) {
this.lens = new Circle({ style });
this.canvas.appendChild(this.lens);
}
Object.assign(style, toPointObject(origin), {
size: this.r * 2,
label: this.options.showDPercent,
labelText: this.getDPercent(),
});
this.lens.update(style);
};
private getDPercent = () => {
const { minD, maxD } = this.options as Required<FisheyeOptions>;
const percent = Math.round(((this.d - minD) / (maxD - minD)) * 100);
return `${percent}%`;
};
private prevMagnifiedStyleMap = new Map<ID, NodeStyle>();
private prevOriginStyleMap = new Map<ID, NodeStyle>();
private renderFocusElements = () => {
if (!this.isLensOn) return;
const { graph } = this.context;
const origin = this.lens.getCenter();
const molecularParam = (this.d + 1) * this.r;
const magnifiedStyleMap = new Map<ID, NodeStyle>();
const originStyleMap = new Map<ID, NodeStyle>();
const nodeData = graph.getNodeData();
nodeData.forEach((datum) => {
const position = positionOf(datum);
const distanceToOrigin = distance(position, origin);
if (distanceToOrigin > this.r) return;
const magnifiedDistance = (molecularParam * distanceToOrigin) / (this.d * distanceToOrigin + this.r);
const [nodeX, nodeY] = position;
const [originX, originY] = origin;
const cos = (nodeX - originX) / distanceToOrigin;
const sin = (nodeY - originY) / distanceToOrigin;
const newPoint: Point = [originX + magnifiedDistance * cos, originY + magnifiedDistance * sin];
const nodeId = idOf(datum);
const style = this.getNodeStyle(datum);
const originStyle = pick(graph.getElementRenderStyle(nodeId), Object.keys(style));
magnifiedStyleMap.set(nodeId, { ...toPointObject(newPoint), ...style });
originStyleMap.set(nodeId, { ...toPointObject(position), ...originStyle });
});
this.updateStyle(magnifiedStyleMap, originStyleMap);
};
private getNodeStyle = (datum: NodeData) => {
const { nodeStyle } = this.options;
return typeof nodeStyle === 'function' ? nodeStyle(datum) : nodeStyle;
};
private updateStyle = (magnifiedStyleMap: Map<ID, NodeStyle>, originStyleMap: Map<ID, NodeStyle>) => {
const { graph, element } = this.context;
const { enter, exit, keep } = arrayDiff<ID>(
Array.from(this.prevMagnifiedStyleMap.keys()),
Array.from(magnifiedStyleMap.keys()),
(d) => d,
);
const relatedEdges = new Set<ID>();
const update = (nodeId: ID, style: NodeStyle) => {
const node = element!.getElement(nodeId) as Node;
node.update(style);
graph.getRelatedEdgesData(nodeId).forEach((datum) => {
relatedEdges.add(idOf(datum));
});
};
[...enter, ...keep].forEach((nodeId) => {
update(nodeId, magnifiedStyleMap.get(nodeId)!);
});
exit.forEach((nodeId) => {
update(nodeId, this.prevOriginStyleMap.get(nodeId)!);
this.prevOriginStyleMap.delete(nodeId);
});
relatedEdges.forEach((edgeId) => {
const edge = element!.getElement(edgeId);
edge?.update({});
});
this.prevMagnifiedStyleMap = magnifiedStyleMap;
originStyleMap.forEach((style, nodeId) => {
if (!this.prevOriginStyleMap.has(nodeId)) {
this.prevOriginStyleMap.set(nodeId, style);
}
});
};
private isWheelValid = (event: WheelEvent) => {
if (this.options.preventDefault) event.preventDefault();
if (!this.isLensOn) return false;
const { clientX, clientY } = event;
const scaleOrigin = this.context.graph.getCanvasByClient([clientX, clientY]);
const origin = this.lens.getCenter();
if (distance(scaleOrigin, origin) > this.r) return false;
return true;
};
private scaleR = (positive: boolean) => {
const { maxR, minR } = this.options;
const ratio = positive ? 1 / (1 - R_DELTA) : 1 - R_DELTA;
const canvasR = Math.min(...this.context.canvas.getSize()) / 2;
this.r = Math.max(minR || 0, Math.min(maxR || canvasR, this.r * ratio));
};
private scaleD = (positive: boolean) => {
const { maxD, minD } = this.options as Required<FisheyeOptions>;
const newD = positive ? this.d + D_DELTA : this.d - D_DELTA;
this.d = Math.max(minD, Math.min(maxD, newD));
};
private scaleRByWheel = (event: WheelEvent) => {
if (!this.isWheelValid(event)) return;
const { deltaX, deltaY } = event;
this.scaleR(deltaX + deltaY > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
private scaleDByWheel = (event: WheelEvent) => {
if (!this.isWheelValid(event)) return;
const { deltaX, deltaY } = event;
this.scaleD(deltaX + deltaY > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
private isDragValid = (event: IDragEvent) => {
if (this.options.preventDefault) event.preventDefault();
if (!this.isLensOn) return false;
const dragOrigin = parsePoint(event.canvas as PointObject);
const origin = this.lens.getCenter();
if (distance(dragOrigin, origin) > this.r) return false;
return true;
};
private isLensDragging = false;
private onDragStart = (event: IDragEvent) => {
if (!this.isDragValid(event)) return;
this.isLensDragging = true;
};
private onDrag = (event: IDragEvent) => {
if (!this.isLensDragging) return;
const dragOrigin = parsePoint(event.canvas as PointObject);
this.onMagnify(dragOrigin);
};
private onDragEnd = () => {
this.isLensDragging = false;
};
private scaleRByDrag = (event: IDragEvent) => {
if (!this.isLensDragging) return;
const { dx, dy } = event;
this.scaleR(dx - dy > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
private scaleDByDrag = (event: IDragEvent) => {
if (!this.isLensDragging) return;
const { dx, dy } = event;
this.scaleD(dx - dy > 0);
const origin = this.lens.getCenter();
this.onMagnify(origin);
};
get graphDom() {
return this.context.graph.getCanvas().getContextService().getDomElement();
}
private bindEvents() {
const { graph } = this.context;
const { trigger, scaleRBy, scaleDBy } = this.options;
const canvas = graph.getCanvas().getLayer();
if (['click', 'drag'].includes(trigger)) {
canvas.addEventListener(CommonEvent.CLICK, this.onCreateFisheye);
}
if (trigger === 'pointermove') {
canvas.addEventListener(CommonEvent.POINTER_MOVE, this.onCreateFisheye);
}
if (trigger === 'drag' || scaleRBy === 'drag' || scaleDBy === 'drag') {
canvas.addEventListener(CommonEvent.DRAG_START, this.onDragStart);
canvas.addEventListener(CommonEvent.DRAG_END, this.onDragEnd);
const dragFunc = trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag;
canvas.addEventListener(CommonEvent.DRAG, dragFunc);
}
if (scaleRBy === 'wheel' || scaleDBy === 'wheel') {
const wheelFunc = scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel;
this.graphDom?.addEventListener(CommonEvent.WHEEL, wheelFunc, { passive: false });
}
}
private unbindEvents() {
const { graph } = this.context;
const { trigger, scaleRBy, scaleDBy } = this.options;
const canvas = graph.getCanvas().getLayer();
if (['click', 'drag'].includes(trigger)) {
canvas.removeEventListener(CommonEvent.CLICK, this.onCreateFisheye);
}
if (trigger === 'pointermove') {
canvas.removeEventListener(CommonEvent.POINTER_MOVE, this.onCreateFisheye);
}
if (trigger === 'drag' || scaleRBy === 'drag' || scaleDBy === 'drag') {
canvas.removeEventListener(CommonEvent.DRAG_START, this.onDragStart);
canvas.removeEventListener(CommonEvent.DRAG_END, this.onDragEnd);
const dragFunc = trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag;
canvas.removeEventListener(CommonEvent.DRAG, dragFunc);
}
if (scaleRBy === 'wheel' || scaleDBy === 'wheel') {
const wheelFunc = scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel;
this.graphDom?.removeEventListener(CommonEvent.WHEEL, wheelFunc);
}
}
public update(options: Partial<FisheyeOptions>) {
this.unbindEvents();
super.update(options);
this.bindEvents();
}
public destroy() {
this.unbindEvents();
if (this.isLensOn) {
this.lens?.destroy();
}
this.prevMagnifiedStyleMap.clear();
this.prevOriginStyleMap.clear();
super.destroy();
}
}

View File

@ -4,6 +4,7 @@ export { BubbleSets } from './bubble-sets';
export { CameraSetting } from './camera-setting';
export { Contextmenu } from './contextmenu';
export { EdgeFilterLens } from './edge-filter-lens';
export { Fisheye } from './fisheye';
export { Fullscreen } from './fullscreen';
export { GridLine } from './grid-line';
export { History } from './history';
@ -22,6 +23,7 @@ export type { BubbleSetsOptions } from './bubble-sets';
export type { CameraSettingOptions } from './camera-setting';
export type { ContextmenuOptions } from './contextmenu';
export type { EdgeFilterLensOptions } from './edge-filter-lens';
export type { FisheyeOptions } from './fisheye';
export type { FullscreenOptions } from './fullscreen';
export type { GridLineOptions } from './grid-line';
export type { HistoryOptions } from './history';

View File

@ -73,6 +73,7 @@ import {
BubbleSets,
Contextmenu,
EdgeFilterLens,
Fisheye,
Fullscreen,
GridLine,
History,
@ -187,6 +188,7 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
'grid-line': GridLine,
background: Background,
contextmenu: Contextmenu,
fisheye: Fisheye,
fullscreen: Fullscreen,
history: History,
hull: Hull,

View File

@ -79,6 +79,13 @@ export interface BaseElementStyleProps extends BaseShapeStyleProps {
* <en/> Element methods
*/
export interface ElementMethods {
/**
* <zh/>
*
* <en/> Update element attributes
* @param attr - <zh/> | <en/> Attributes
*/
update(attr: any): void;
/**
* <zh/>
*

View File

@ -0,0 +1,39 @@
import { Graph, iconfont } from '@antv/g6';
const style = document.createElement('style');
style.innerHTML = `@import url('${iconfont.css}');`;
document.head.appendChild(style);
fetch('https://assets.antv.antgroup.com/g6/relations.json')
.then((res) => res.json())
.then((data) => {
const graph = new Graph({
container: 'container',
autoFit: 'view',
data,
node: {
style: {
size: (datum) => datum.id.length * 2 + 10,
label: false,
labelText: (datum) => datum.id,
labelBackground: true,
icon: false,
iconFontFamily: 'iconfont',
iconText: '\ue6f6',
iconFill: '#fff',
},
palette: {
type: 'group',
field: (datum) => datum.id,
color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],
},
},
edge: {
style: {
stroke: '#e2e2e2',
},
},
plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],
});
graph.render();
});

View File

@ -0,0 +1,108 @@
import { Graph, iconfont } from '@antv/g6';
const style = document.createElement('style');
style.innerHTML = `@import url('${iconfont.css}');`;
document.head.appendChild(style);
fetch('https://assets.antv.antgroup.com/g6/relations.json')
.then((res) => res.json())
.then((data) => {
const graph = new Graph({
container: 'container',
autoFit: 'view',
data,
node: {
style: {
size: (datum) => datum.id.length * 2 + 10,
label: false,
labelText: (datum) => datum.id,
labelBackground: true,
icon: false,
iconFontFamily: 'iconfont',
iconText: '\ue6f6',
iconFill: '#fff',
},
palette: {
type: 'group',
field: (datum) => datum.id,
color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],
},
},
edge: {
style: {
stroke: '#e2e2e2',
},
},
plugins: [
{
key: 'fisheye',
type: 'fisheye',
trigger: 'click',
scaleRBy: 'wheel',
scaleDBy: 'drag',
style: { fill: 'transparent', stroke: 'transparent' },
nodeStyle: { label: true, icon: true },
},
],
});
graph.render();
const config = {
trigger: 'click',
scaleRBy: 'wheel',
scaleDBy: 'drag',
showDPercent: true,
borderless: true,
};
window.addPanel((gui) => {
gui
.add(config, 'trigger', ['pointermove', 'click', 'drag'])
.name('Trigger')
.onChange((value) => {
graph.updatePlugin({
key: 'fisheye',
trigger: value,
});
});
gui
.add(config, 'scaleRBy', ['wheel', 'drag', 'unset'])
.name('Scale R by')
.onChange((value) => {
graph.updatePlugin({
key: 'fisheye',
scaleRBy: value,
});
});
gui
.add(config, 'scaleDBy', ['wheel', 'drag', 'unset'])
.name('Scale D by')
.onChange((value) => {
graph.updatePlugin({
key: 'fisheye',
scaleDBy: value,
});
});
gui
.add(config, 'showDPercent')
.name('Show D Percent')
.onChange((value) => {
graph.updatePlugin({
key: 'fisheye',
showDPercent: value,
});
});
gui
.add(config, 'borderless')
.name('Borderless')
.onChange((value) => {
const style = value
? { fill: 'transparent', lineDash: 0, stroke: 'transparent' }
: { fill: '#F08F56', lineDash: [5, 5], stroke: '#666' };
graph.updatePlugin({
key: 'fisheye',
style,
});
});
});
});

View File

@ -0,0 +1,24 @@
{
"title": {
"zh": "中文分类",
"en": "Category"
},
"demos": [
{
"filename": "basic.js",
"title": {
"zh": "鱼眼放大镜",
"en": "Fisheye"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SPqiTLwdZ9MAAAAAAAAAAAAADmJ7AQ/original"
},
{
"filename": "custom.js",
"title": {
"zh": "自定义鱼眼放大镜",
"en": "Custom Fisheye"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-VyPRY-2dMIAAAAAAAAAAAAADmJ7AQ/original"
}
]
}

View File

@ -0,0 +1,5 @@
---
title: Fisheye
---
Fisheye is designed for focus+context exploration, it keeps the context and the relationships between context and the focus while magnifying the focus area.

View File

@ -0,0 +1,5 @@
---
title: Fisheye 鱼眼放大镜
---
Fisheye 鱼眼放大镜是为 focus+context 的探索场景设计的,它能够保证在放大关注区域的同时,保证上下文以及上下文与关注中心的关系不丢失。