React + ECharts 进阶:优雅实现"带记忆"的多图联动与 Tooltip 点击锁定
前言
在开发复杂的数据看板时,我们经常会遇到这样的痛点:
- 联动难:虽然 ECharts 自带 connect,但它只能同步悬停,无法自定义复杂的交互逻辑。
- 看数难:鼠标一旦移开,Tooltip 消失。想对比两个相距较远的点?甚至想截图保存当前数据状态?难如登天。
- 视觉乱:多图联动时,所有图表同时弹出 Tooltip,画面瞬间被密密麻麻的浮层塞满。
今天我将分享一个基于 React Context + ECharts 手动派发 Action 的方案,实现多表同步悬停、点击锁定数据点、智能 Tooltip 显示等高阶交互功能。
🚀 核心交互预览
- 全场同步:鼠标在 A 图滑过,B、C、D 图的指示线(AxisPointer)精准同步。
- 点击锁定:点击某一数据点,所有图表进入"锁定模式",即便鼠标移走,数据依然停留在那一刻,方便深度对比。
- 智能激活:只有当前鼠标所在的图表(Active Chart)才显示详细浮层,其他图表保持简洁,仅保留指示线。
一、 为什么不直接用 echarts.connect?
ECharts 官方提供的 connect 能够满足 80% 的联动场景。但剩下的 20%(比如我们要实现的"点击锁定"),官方 API 就显得捉襟见肘了。
本方案的优势:
- 高度受控:所有图表的状态(选中索引、激活状态)全部收拢在 React State 中。
- 交互定制:可以自由定义"解锁"逻辑(如点击空白、鼠标再次进入时自动解锁)。
- 性能优异:通过 Ref 和 dispatchAction 精准控制图表更新,避免了因 setOption 导致的频繁重绘。
二、 架构设计:给图表装上"指挥部"
我们采用 Context + Provider 模式。所有图表组件都是"订阅者",指挥部(Context)负责分发指令。
1. 逻辑分发图
这里的核心是单向数据流:

2. 定义通讯协议
codeTypeScript
typescript
interface SyncContext {
lockedIndex: number | null; // "锁定"的索引:一旦确定,全场静止
hoverIndex: number | null; // "悬停"的索引:随鼠标起舞
activeChartId: string | null; // 谁是当前的"指挥官":只有它能显示气泡
}
三、 核心代码实现
1. 夺回控制权:禁用自动触发
要实现手动联动,首先要关掉 ECharts 默认的鼠标响应。
codeJavaScript
arduino
tooltip: {
trigger: "axis",
triggerOn: "none", // 重点:禁用默认的自动触发,改由代码接管
showContent: isActive, // 只有被激活的图表才显示气泡内容
},
2. 时空转换:像素坐标到数据索引
如何知道鼠标指在哪个点?我们需要把屏幕上的像素点"翻译"成数据的 index。
js
// 使用 zr (ZRender) 监听,比普通图表事件更灵敏
zr.on("mousemove", (params) => {
if (lockedIndexRef.current !== null) return; // 锁定状态下不响应悬停
const pointInPixel = [params.offsetX, params.offsetY];
if (chart.containPixel("grid", pointInPixel)) {
// 关键:将像素坐标转换为数据坐标
const pointInGrid = chart.convertFromPixel({ seriesIndex: 0 }, pointInPixel);
const xIndex = Math.round(pointInGrid[0]); // 获取数据索引
setHoverIndex(xIndex);
setActiveChartId(id);
}
});
3. 精准打击:使用 dispatchAction
这是实现联动的"魔法棒"。我们不通过 setOption 改数据,而是直接向图表发送动作指令。
js
useEffect(() => {
const chart = instanceRef.current;
if (!chart) return;
// 优先级:锁定索引 > 悬停索引
const targetIndex = lockedIndex !== null ? lockedIndex : hoverIndex;
const isActive = activeChartId === id;
if (targetIndex !== null) {
// 1. 同步坐标轴指示线 (所有图表同步)
chart.dispatchAction({
type: "updateAxisPointer",
seriesIndex: 0,
dataIndex: targetIndex,
});
// 2. 控制气泡显示 (仅激活图表显示)
if (isActive) {
chart.dispatchAction({ type: "showTip", dataIndex: targetIndex });
} else {
chart.dispatchAction({ type: "hideTip" });
}
}
}, [lockedIndex, hoverIndex, activeChartId]);
四、 进阶优化(避坑指南)
1. 解决 React 闭包陷阱
在 zr.on 的异步回调中,直接访问 lockedIndex 拿到的是组件初次渲染时的旧值。
方案:引入 useRef 实时同步状态,确保回调函数永远能拿到"现在"的值。
js
const lockedIndexRef = useRef(lockedIndex);
useEffect(() => {
lockedIndexRef.current = lockedIndex; // 同步 ref
}, [lockedIndex]);
2. 极致性能:防止配置重载
频繁切换 showContent 可能会触发 ECharts 内部的重绘逻辑。
方案 :在组件内部维护一个 tooltipStateRef,只有当激活状态真正发生切换时,才调用 chart.setOption。
3. 视觉反馈优化
为了增强交互感,我在外层容器使用了 Tailwind 动态类名:
js
// 当锁定发生时,外层边框发光提醒
className={`transition-all duration-300 ${
lockedIndex !== null ? "border-indigo-500 shadow-indigo-500/20" : "border-transparent"
}`}
五、 总结
这套方案的核心思想是:将 ECharts 的内部状态"外部化" 。
通过 React 控制 ECharts 的 dispatchAction,我们不仅实现了多表联动和点击锁定,更重要的是,我们为数据图表赋予了"记忆"和"主从关系"。
这种"受控图表"的思路可以轻易扩展到:
- 多维数据刷选 (Brush) 同步
- 时间轴缩放 (DataZoom) 同步
- 多维度对比 的持久化展示
如果你正在开发高性能的可视化大屏,不妨尝试这套方案,它会让你的看板交互瞬间提升一个档次。
作者注:文中代码片段为核心逻辑,完整实现需要配合 Context.Provider。如果在开发过程中遇到坐标转换偏移、多图同步延迟等细节问题,欢迎在评论区交流讨论!