G6 鱼眼放大镜(Fisheye)插件完全指南:focus+context 交互式局部放大实战
【免费下载链接】G6♾ A Graph Visualization Framework in JavaScript.项目地址: https://gitcode.com/gh_mirrors/g6/G6
Fisheye 鱼眼放大镜是 G6 图可视化框架内置的交互插件,专为 focus+context(焦点+上下文)探索场景设计:它用一个可移动的圆形透镜放大关注区域内的节点,同时保持周边上下文以及上下文与关注中心的关系不丢失。本文基于 G6 官方文档与源码实现,系统讲解 Fisheye 插件的配置项、交互触发方式、畸变算法原理、样式定制与动态更新 API,并给出可直接运行的完整代码示例。读完本文,你将能够在自己的 G6 应用中一键接入鱼眼放大镜,并针对半径、畸变因子、外观样式和节点样式做精细化定制。
概述:为什么需要鱼眼放大镜
在大规模图可视化场景中,节点数量多、布局密集,直接放大画布往往导致查看区域外的重要上下文被裁剪。鱼眼放大镜(Fisheye)采用的是一种“非线性放大”策略:以透镜圆心为中心,对半径范围内的节点做距离相关的放大变换——越靠近中心放大倍数越大,越靠近边缘越接近原始尺寸,从而在突出关注区域的同时保留整体视图的连续感。
从源码注释(packages/g6/src/plugins/fisheye/index.ts)可以看到其设计目标:
Fisheye 鱼眼放大镜是为 focus+context 的探索场景设计的,它能够保证在放大关注区域的同时,保证上下文以及上下文与关注中心的关系不丢失。
典型使用场景
- 演示与汇报:在演示过程中需要突出展示某些区域内容,引导观众视线聚焦;
- 局部细节审查:需要局部放大查看密集区域的细节(如节点标签、连边走向)时,同时不想失去整体视图;
- 大图探索:在包含大量节点和边的大型关系图中,通过透镜扫视不同区域,快速定位感兴趣的子图。
基本用法:一行配置接入鱼眼
Fisheye 是 G6 内置插件,无需单独安装扩展包,直接在Graph的plugins数组中声明即可。最简单的配置方式只需要插件类型字符串:
const graph = new Graph({ plugins: ['fisheye'], });带完整参数的最基本初始化示例:
const graph = new Graph({ plugins: [ { type: 'fisheye', trigger: 'drag', // 通过拖拽移动鱼眼 d: 1.5, // 设置畸变因子 r: 120, // 设置鱼眼半径 showDPercent: true, // 显示畸变程度 }, ], });执行graph.render()后,将鼠标移动到画布上即可看到圆形透镜跟随出现,透镜内的节点被放大并重新分布。
配置项全解
Fisheye 插件的全部配置项定义在源码接口FisheyeOptions中(packages/g6/src/plugins/fisheye/index.ts),默认值见Fisheye.defaultOptions(同文件第 156-166 行)。下表为完整配置项说明:
| 属性 | 描述 | 类型 | 默认值 | 必选 |
|---|---|---|---|---|
| type | 插件类型 | string | fisheye | ✓ |
| key | 插件的唯一标识,可用于获取插件实例或更新插件选项 | string | - | |
| trigger | 控制鱼眼放大镜的移动方式,支持三种配置:pointermove(始终跟随鼠标移动)、click(点击画布时移动到点击位置)、drag(通过拖拽移动) | pointermove|drag|click | pointermove | |
| r | 鱼眼放大镜半径 | number | 120 | |
| maxR | 鱼眼放大镜可调整的最大半径(配合scaleRBy使用) | number | 画布宽高的最小值的一半 | |
| minR | 鱼眼放大镜可调整的最小半径 | number | 0 | |
| d | 畸变因子 | number | 1.5 | |
| maxD | 鱼眼放大镜可调整的最大畸变因子 | number | 5 | |
| minD | 鱼眼放大镜可调整的最小畸变因子 | number | 0 | |
| scaleRBy | 调整鱼眼放大镜范围半径的方式:wheel(滚轮)或drag(拖拽) | wheel|drag | - | |
| scaleDBy | 调整鱼眼放大镜畸变因子的方式:wheel(滚轮)或drag(拖拽) | wheel|drag | - | |
| showDPercent | 是否在鱼眼放大镜中显示畸变因子数值 | boolean | true | |
| style | 鱼眼放大镜(圆形透镜)样式,详见下文 style 小节 | object | - | |
| nodeStyle | 在鱼眼放大镜中的节点样式 | NodeStyle | ((datum: NodeData) => NodeStyle) | { label: true } | |
| preventDefault | 是否阻止默认事件 | boolean | true |
关键参数详解
r(半径):透镜的物理作用范围,单位为画布坐标像素。源码中透镜实际渲染为一个圆形元素,其直径等于r * 2(见 renderLens)。默认值 120,适合中等画布;画布较大时可适当调大。d(畸变因子):决定透镜内节点被拉伸的“强度”。d越大,靠近中心的节点被放得越大,边缘到中心的尺寸过渡越陡峭。默认值 1.5,取值区间建议在 0~5 之间(minD/maxD默认即为此范围)。trigger:透镜移动方式。默认为pointermove,即透镜始终跟随鼠标;click模式适合触摸屏或需要精确落点的场景;drag模式适合在探索大图时“抓住”透镜拖动浏览。nodeStyle:透镜内节点的渲染样式,默认{ label: true },即进入透镜的节点自动显示标签。它既可以是普通样式对象,也可以是接收NodeData返回样式的函数,便于按节点数据动态定制。preventDefault:默认true,会阻止滚轮等事件触发的浏览器默认行为(如页面滚动),避免与透镜缩放操作冲突。
style:透镜圆形样式
style对应 G6 圆形元素(Circle)的样式属性CircleStyleProps,用于配置鱼眼放大镜本身的外观(填充、描边、透明度、阴影、线段端点等)。源码中的默认透镜样式(defaultLensStyle)为:
{ fill: '#ccc', // 填充颜色 fillOpacity: 0.1, // 填充透明度 lineWidth: 2, // 线宽 stroke: '#000', // 描边颜色 strokeOpacity: 0.8, // 描边透明度 labelFontSize: 12, // 畸变百分比标签字号 }完整可配置属性如下:
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| fill | 填充颜色 | string | Pattern | null | #ccc |
| stroke | 描边颜色 | string | Pattern | null | #000 |
| opacity | 整体透明度 | number | string | - |
| fillOpacity | 填充透明度 | number | string | 0.1 |
| strokeOpacity | 描边透明度 | number | string | - |
| lineWidth | 线宽度 | number | string | 2 |
| lineCap | 线段端点样式 | butt|round|square | - |
| lineJoin | 线段连接处样式 | miter|round|bevel | - |
| shadowColor | 阴影颜色 | string | - |
| shadowBlur | 阴影模糊程度 | number | - |
| shadowOffsetX | 阴影 X 方向偏移 | number | - |
| shadowOffsetY | 阴影 Y 方向偏移 | number | - |
完整样式属性参考 元素 - 节点 - 内置节点 - 通用样式属性 - style。
交互缩放控制:半径与畸变因子的动态调整
通过scaleRBy和scaleDBy可以分别控制透镜半径与畸变因子的实时调整方式,支持滚轮(wheel)和拖拽(drag)两种手势。示例:
const graph = new Graph({ plugins: [ { type: 'fisheye', // 通过滚轮调整半径 scaleRBy: 'wheel', // 通过拖拽调整畸变因子 scaleDBy: 'drag', // 设置半径和畸变因子的范围 minR: 50, maxR: 200, minD: 1, maxD: 3, }, ], });手势与优先级规则(重要)
当trigger、scaleRBy、scaleDBy三者可能同时使用拖拽(drag)手势时,同一手势只能绑定给一个配置项,优先级顺序为:
trigger>scaleRBy>scaleDBy
即如果三者都设为'drag',只会为trigger绑定拖拽事件,移动透镜优先;同理,如果scaleRBy和scaleDBy同时设为'wheel',只会为scaleRBy绑定滚轮事件,此时滚轮只调节半径。
这一优先级逻辑与源码 bindEvents 中的事件绑定分支完全一致:代码依次判断trigger、scaleRBy、scaleDBy,三者都要求drag时按三目运算符链trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag取优先级最高者;滚轮同理取scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel。
调整步长与范围约束
源码定义了两个增量常量(index.ts):
const R_DELTA = 0.05; // 半径每次缩放的比例系数 const D_DELTA = 0.1; // 畸变因子每次调整的步长- 半径:滚轮上滚/拖拽正方向时按
r / (1 - R_DELTA)放大,下滚时按r * (1 - R_DELTA)缩小,并始终被约束在[minR, maxR]内;maxR未设置时取画布宽高最小值的二分之一(见 scaleR)。 - 畸变因子:每次
+0.1或-0.1,被约束在[minD, maxD]内(见 scaleD)。
代码示例:从基础到深度定制
基础用法
最简配置方式:
const graph = new Graph({ plugins: ['fisheye'], });自定义样式:透镜外观 + 节点样式
可以同时定制透镜本身的外观,以及透镜内节点的样式(包括基础样式、标签、图标等):
const graph = new Graph({ plugins: [ { type: 'fisheye', r: 150, d: 2, style: { fill: '#2f54eb', // 鱼眼区域的填充颜色 fillOpacity: 0.2, // 填充区域的透明度 stroke: '#1d39c4', // 鱼眼边框的颜色 strokeOpacity: 0.8, // 边框的透明度 lineWidth: 1.5, // 边框的线宽 shadowColor: '#1d39c4', // 阴影颜色 shadowBlur: 10, // 阴影的模糊半径 shadowOffsetX: 0, // 阴影的水平偏移 shadowOffsetY: 0, // 阴影的垂直偏移 cursor: 'pointer', // 鼠标悬停时的指针样式 }, nodeStyle: { // 节点基础样式 size: 40, // 节点大小 fill: '#d6e4ff', // 节点填充颜色 stroke: '#2f54eb', // 节点边框颜色 lineWidth: 2, // 节点边框宽度 shadowColor: '#2f54eb', // 节点阴影颜色 shadowBlur: 5, // 节点阴影模糊半径 cursor: 'pointer', // 鼠标悬停时的指针样式 // 标签样式 label: true, // 是否显示标签 labelFontSize: 14, // 标签字体大小 labelFontWeight: 'bold', // 标签字体粗细 labelFill: '#1d39c4', // 标签文字颜色 labelBackground: true, // 是否显示标签背景 labelBackgroundFill: '#fff', // 标签背景填充颜色 labelBackgroundStroke: '#1d39c4', // 标签背景边框颜色 labelBackgroundOpacity: 0.8, // 标签背景透明度 labelBackgroundPadding: [4, 8, 4, 8], // 标签背景内边距 [上,右,下,左] // 图标样式 icon: true, // 是否显示图标 iconFontFamily: 'iconfont', // 图标字体 iconText: '\ue6f6', // 图标的 Unicode 编码 iconFill: '#1d39c4', // 图标颜色 iconSize: 16, // 图标大小 iconFontWeight: 'normal', // 图标字体粗细 }, }, ], });官方文档中该示例对应的完整可运行版本(含 5 个节点、5 条边的图数据与graph.render()):
import { Graph } from '@antv/g6'; const graph = new Graph({ container: 'container', width: 400, height: 300, data: { nodes: [ { id: 'node-1', style: { x: 150, y: 100 } }, { id: 'node-2', style: { x: 250, y: 100 } }, { id: 'node-3', style: { x: 200, y: 180 } }, { id: 'node-4', style: { x: 120, y: 180 } }, { id: 'node-5', style: { x: 280, y: 180 } }, ], edges: [ { id: 'edge-1', source: 'node-1', target: 'node-2' }, { id: 'edge-2', source: 'node-1', target: 'node-3' }, { id: 'edge-3', source: 'node-2', target: 'node-3' }, { id: 'edge-4', source: 'node-3', target: 'node-4' }, { id: 'edge-5', source: 'node-3', target: 'node-5' }, ], }, node: { style: { size: 30, fill: '#e6f7ff', stroke: '#1890ff', lineWidth: 1, label: false, icon: false, }, }, edge: { style: { stroke: '#91d5ff', lineWidth: 1, }, }, plugins: [ { type: 'fisheye', key: 'fisheye', r: 100, d: 2, style: { fill: '#2f54eb', fillOpacity: 0.2, stroke: '#1d39c4', strokeOpacity: 0.8, lineWidth: 1.5, shadowColor: '#1d39c4', shadowBlur: 10, shadowOffsetX: 0, shadowOffsetY: 0, cursor: 'pointer', }, nodeStyle: { size: 40, fill: '#d6e4ff', stroke: '#2f54eb', lineWidth: 2, shadowColor: '#2f54eb', shadowBlur: 5, cursor: 'pointer', label: true, labelFontSize: 14, labelFontWeight: 'bold', labelFill: '#1d39c4', labelBackground: true, labelBackgroundFill: '#fff', labelBackgroundStroke: '#1d39c4', labelBackgroundOpacity: 0.8, labelBackgroundPadding: [4, 8, 4, 8], icon: true, iconFontFamily: 'iconfont', iconText: '\ue6f6', iconFill: '#1d39c4', iconSize: 16, iconFontWeight: 'normal', }, }, ], }); graph.render();实际案例:在关系大图上使用鱼眼
官方文档提供了一个真实场景案例——基于relations.json关系数据,节点大小由id长度动态计算,使用分组调色板着色,并接入drag-canvas行为与鱼眼插件(透镜内显示标签和图标):
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(); });说明:
iconfont是 G6 内置的图标字体资源,为节点图标提供字体定义;关系数据也可替换为仓库自带的 packages/g6/tests/dataset/relations.json 进行本地实验。
底层原理:鱼眼畸变算法与渲染流程
G6 的 Fisheye 插件本质是一个继承自BasePlugin的运行时插件类(export class Fisheye extends BasePlugin<FisheyeOptions>,见 packages/g6/src/plugins/fisheye/index.ts),并在 packages/g6/src/plugins/index.ts 统一导出注册。
透镜渲染
透镜是一个绘制在transient(瞬态)图层上的Circle圆形元素(index.ts):首次创建时new Circle({ style })并appendChild到瞬态层,之后每次移动只更新位置(size: r * 2)与畸变百分比标签。这样透镜本身不会污染主画布的元素树,销毁时随插件destroy()一并清除。
畸变映射公式
renderFocusElements(index.ts)对半径r内的每个节点执行非线性映射:
分子 = (d + 1) * r 放大后的距离 = 分子 * 原始距离 / (d * 原始距离 + r) 新位置 = 圆心 + 放大后的距离 * (节点相对圆心的单位方向向量)- 当节点恰好在圆心(距离为 0)时,映射后仍在圆心;
- 当节点在透镜边缘(距离 = r)时,放大后的距离 = r,即边缘节点保持原位,与透镜外上下文无缝衔接;
d越大,靠近圆心的节点被拉伸得越远,形成更明显的“放大镜”效果。
这也是鱼眼透镜能与外部上下文平滑过渡、不产生视觉断裂的根本原因。
样式更新与差异计算
updateStyle(index.ts)利用arrayDiff对比上一次与当前放大节点集合的enter / keep / exit差异:进入或保持的节点应用放大后的样式;退出的节点恢复为进入透镜前记录的原始样式(prevOriginStyleMap),随后更新这些节点关联的边,保证边端点与节点新位置同步,实现拖拽过程中的平滑过渡。
事件系统与手势绑定
事件绑定(bindEvents)遵循“同一手势只绑定一个功能”的原则:
trigger为click/drag时监听画布CLICK;为pointermove时监听POINTER_MOVE;- 需要拖拽时监听
DRAG_START/DRAG_END,并按优先级选择DRAG处理函数(移动/调半径/调畸变); - 滚轮缩放监听原生
WHEEL事件并设置{ passive: false },以便preventDefault生效; - 滚轮/拖拽的有效性校验要求鼠标位置位于透镜半径
r内(见isWheelValid、isDragValid),避免误操作。
插件还实现了update(options)与destroy()(index.ts):更新时先解绑旧事件、合并新配置、再重新绑定,运行时动态调整参数无需重建实例。
运行时动态更新与参数调试
Fisheye 插件配置了key后,即可通过 G6 的插件管理能力在运行时获取实例或更新参数:
const graph = new Graph({ plugins: [{ type: 'fisheye', key: 'fisheye' }], }); // 运行时更新:切换移动方式为 click,调整畸变因子 graph.updatePlugin({ key: 'fisheye', trigger: 'click', d: 2.5, }); graph.render(); // 获取插件实例(类型为 Fisheye) const fisheye = graph.getPluginInstance('fisheye');官方文档配套的交互演示(packages/site/common/api/plugins/fisheye.md)正是利用graph.updatePlugin({ key: 'fisheye', [property]: value })配合 GUI 面板,实时调节trigger、r、d、scaleRBy、scaleDBy、showDPercent、preventDefault等参数,非常适合作为理解各配置项手感差异的调试入口。
测试验证与快照保障
仓库为 Fisheye 插件提供了完整的单元测试 packages/g6/tests/unit/plugins/fisheye.spec.ts,覆盖了:
pointermove/drag/click三种方式移动透镜的快照;- 滚轮与拖拽调节半径(
scaleRBy)与畸变因子(scaleDBy)的增大/缩小行为; showDPercent开关对畸变百分比标签的影响;- 自定义透镜样式(如虚线
lineDash: [5, 5])与节点样式(如halo: true)的渲染效果。
测试通过dispatchCanvasEvent模拟鼠标与滚轮事件、通过toMatchSnapshot对比渲染结果,是验证插件行为、防止回归的重要参考。对应的 demo 入口为 packages/g6/tests/demos/plugin-fisheye.ts,可在本地pnpm dev环境下直接体验。
小结
Fisheye 鱼眼放大镜是 G6 内置插件中实现 focus+context 交互最直接的方案:通过trigger控制透镜移动方式,r/d控制放大范围与强度,scaleRBy/scaleDBy支持运行时交互微调,style/nodeStyle提供视觉定制,updatePlugin支持运行时动态改参。结合本文给出的完整代码示例与源码原理分析,你可以快速在关系图、知识图谱、组织架构等可视化场景中落地局部聚焦能力。
【免费下载链接】G6♾ A Graph Visualization Framework in JavaScript.项目地址: https://gitcode.com/gh_mirrors/g6/G6
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考