Plotly可视化实战:从入门到性能优化的完整指南

皇甫含含 交互 阅读 1,668
赞 20 收藏
二维码
手机扫码查看
反馈

Plotly 图表在移动端拖拽缩放失效,折腾半天才找到原因

上周在做一个数据看板项目,用 Plotly.js 渲染几个关键指标的趋势图。本地开发时一切正常,鼠标拖拽、滚轮缩放都好使。结果一上真机测试,发现 iOS 和 Android 上图表完全不能拖拽缩放——手指滑动直接被页面滚动“吃掉”了,图表纹丝不动。

Plotly可视化实战:从入门到性能优化的完整指南

我一开始以为是 Plotly 的移动端支持问题,赶紧去查文档。官方文档里明明写着 touch events 是默认启用的,还专门有个 scrollZoom 配置项。但试了各种组合,包括显式设置 dragmode: 'zoom'scrollZoom: true,都没用。手指一碰图表,页面就上下滚动,根本没法操作图表。

这里我踩了个坑:以为是 Plotly 本身的问题,结果浪费了快两个小时在它的配置选项里打转。后来灵光一闪,是不是页面的其他 CSS 或 JS 干扰了?毕竟我们项目里用了不少全局的 touch 事件处理逻辑。

排查过程:从禁用页面滚动开始

我先试着在图表容器上加了个 touch-action: none,这是浏览器层面禁止默认触摸行为的 CSS 属性。结果……还是没反应。这就奇怪了。

接着我打开 Chrome DevTools 的设备模拟器,手动触发 touch 事件,发现控制台里居然报错:Unable to preventDefault inside passive event listener due to target being treated as passive。哦!原来如此——现代浏览器为了提升滚动性能,默认把 touchstart/touchmove 事件监听器设为 passive(被动),这时候你调用 event.preventDefault() 就会报错,而且不会生效。

而 Plotly 内部在处理拖拽缩放时,肯定需要调用 preventDefault() 来阻止页面滚动。如果事件监听器是 passive 的,这个调用就无效,导致页面照常滚动,图表无法响应。

那为什么桌面端没事?因为桌面端用的是 mouse 事件,不受 passive 规则影响。只有移动端的 touch 事件才受这个限制。

解决方案:强制非 passive 事件监听

知道了原因,解决方向就明确了:必须让 Plotly 注册的 touch 事件监听器是非 passive 的。

但 Plotly 本身没有提供配置项来控制这个。翻了下它的源码,发现它确实是用标准的 addEventListener 添加事件,但没传第三个参数 { passive: false }

于是我想了个 hack 办法:在 Plotly 初始化之后,手动给图表容器重新绑定 touch 事件,并显式设置 passive: false,同时阻止默认行为。但这样容易和 Plotly 内部逻辑冲突,不太稳妥。

后来试了下发现,其实更简单的办法是:**在图表容器的父级元素上,阻止 touchmove 的默认行为**,但只在用户实际在图表区域内操作时才阻止。

具体做法是:监听图表容器的 touchstart,记录是否在图表内;然后在 document 上监听 touchmove,如果当前处于图表操作状态,就 preventDefault()

但这样写有点重。再想想,其实只要确保图表容器本身能“吃掉” touchmove 事件就行。

最终我采用的方式是:给 Plotly 图表的容器加一个 wrapper,然后在这个 wrapper 上绑定 touch 事件,显式设置 passive: false,并阻止默认行为。

核心代码就这几行:

<div id="chart-wrapper" style="width: 100%; height: 400px;">
  <div id="my-chart"></div>
</div>
// 先初始化 Plotly 图表
Plotly.newPlot('my-chart', data, layout, {
  responsive: true,
  scrollZoom: true,
  displayModeBar: false
});

// 然后处理 touch 事件
const wrapper = document.getElementById('chart-wrapper');

// 关键:添加非 passive 的 touchmove 监听器
wrapper.addEventListener('touchmove', (e) => {
  // 阻止页面滚动
  e.preventDefault();
}, { passive: false });

// 注意:touchstart 也需要同样处理,否则某些浏览器可能不触发 touchmove
wrapper.addEventListener('touchstart', (e) => {
  // 这里可以加一些逻辑,比如判断是否在图表区域
  // 但简单场景下直接 preventDefault 也行
}, { passive: false });

这段代码亲测有效。加上之后,iOS 和 Android 上都能正常拖拽缩放了。手指滑动图表,页面不再滚动,Plotly 的 zoom 模式完美工作。

踩坑提醒:这三点一定注意

  • 不要只加 touchmove,touchstart 也要处理。有些浏览器(特别是旧版 Safari)要求 touchstart 也是非 passive 的,否则 touchmove 可能不会触发。
  • 作用域要精确。上面的代码是直接在整个 wrapper 上阻止滚动,如果你的图表区域很小,或者页面还有其他可滚动区域,可能会误伤。更严谨的做法是判断 touch 事件的坐标是否在图表绘图区域内,但那样复杂度高很多。我这个项目图表占满整个区域,所以简单处理就够了。
  • 记得移除事件监听器。如果图表是动态创建/销毁的(比如 React/Vue 组件),一定要在组件卸载时 removeEventListener,否则会内存泄漏。

另外,如果你用的是 React,可以用 useRef 拿到容器 DOM,然后在 useEffect 里绑定事件,记得 return 一个 cleanup 函数。Vue 也是类似,在 beforeUnmount 里清理。

有没有更优雅的方案?

其实 Plotly 社区里有人提过 issue,希望官方支持配置 passive 选项。但目前还没实现。也有 PR 提议加,但似乎还没合并。

另一个思路是:用 CSS 的 overscroll-behavior: contain。这个属性可以让容器内部的滚动不会传播到父级。但问题是,Plotly 的缩放不是靠滚动实现的,而是靠拖拽,所以这个 CSS 对 touchmove 无效。我试过,没用。

所以目前来看,手动加非 passive 事件监听器是最直接有效的办法。虽然有点 hack,但胜在简单可靠。

改完之后,大部分机型都正常了。不过有个小问题:在某些安卓机上,快速滑动时偶尔还是会触发一点点页面滚动,但不影响主要操作,属于可接受范围。毕竟移动端浏览器的 touch 行为本身就有点玄学,能 work 就行。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。这个技巧的拓展用法还有很多,比如用在其他需要精细控制 touch 事件的 canvas 或 SVG 库上,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论