Skip to content

fix(roam): proper touch pinch support for zoom & pan - #21655

Open
NightRa wants to merge 4 commits into
apache:masterfrom
NightRa:master
Open

fix(roam): proper touch pinch support for zoom & pan#21655
NightRa wants to merge 4 commits into
apache:masterfrom
NightRa:master

Conversation

@NightRa

@NightRa NightRa commented Jun 19, 2026

Copy link
Copy Markdown

Brief Information

This pull request is in the type of:

  • bug fixing
  • new feature
  • others

What does this PR do?

Improve RoamController pinch handling by using the real touch pinch scale, supporting two-finger pan, and capturing pinch gestures consistently.

Fixed issues

Fixes #18949
Fixes #18113
Related #21417
Related #20939

Details

Before: What was the problem?

Touch pinch zoom used a fixed zoom step:

e.pinchScale > 1 ? 1.1 : 1 / 1.1

This made small frame-to-frame pinch changes to zoom exponentially instead of according to the pinch distance.

Pinch also emitted only zoom events. When the pinch center moved, roamable views did not pan with the fingers. Additionally, pinch hit-testing was repeated on every frame, so an active pinch could stop when moving outside the roam area, or be picked up mid-gesture by another overlapping RoamController.

After: How does it behave after the fixing?

RoamController now uses the actual e.pinchScale reported by ZRender.

Pinch is treated as a two-finger roam gesture:

  • roam: true: pinch can pan and zoom.
  • roam: 'move' / 'pan': pinch can pan without zooming.
  • roam: 'scale' / 'zoom': pinch can zoom without panning.

A pinch is captured only when it starts from a native touchstart inside the valid roam area. Once captured, it continues outside the area like mouse drag, and another RoamController cannot pick up the already-moving pinch.

Document Info

  • This PR doesn't relate to document changes
  • The document should be updated later
  • The document changes have been made in apache/echarts-doc#xxx

Misc

Security Checking

  • This PR uses security-sensitive Web APIs.

ZRender Changes

  • This PR depends on ZRender changes (ecomfe/zrender#xxx).

Related test cases or examples to use the new APIs

Manual examples:

  • test/dataZoom-timeAxis.html
  • dataZoom-rainfall-inside.html
  • test/geo-map-roam.html
  • test/graph-layout-roam.html
  • test/tree-roam.html
  • test/sankey-roam.html
  • test/treemap-simple.html
  • test/treemap-scaleLimit.html
  • test/graph-thumbnail.html

I've tested the changes in all the mentioned test examples on an Android phone in Chrome.

Merging options

  • Please squash the commits into a single one when merging.

Before & After videos

dataZoom-Before.mp4
dataZoom-After.mp4
Geo-Before.mp4
Geo-After.mp4

@echarts-bot

echarts-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

Thanks for your contribution!
The community will review it ASAP. In the meanwhile, please checkout the coding standard and Wiki about How to make a pull request.

Please DO NOT commit the files in dist, i18n, and ssr/client/dist folders in a non-release pull request. These folders are for release use only.

@NightRa NightRa changed the title Fix exponential zoom for pinch zoom in RoamController Proper touch pinch support for Zoom & Pan Jun 20, 2026
@NightRa

NightRa commented Jun 22, 2026

Copy link
Copy Markdown
Author

@100pah I'd love a review by you or by anyone you see fit.
This fixes a core issue in data history graphs in Home Assistant on mobile.
Thanks!

@NightRa

NightRa commented Jul 28, 2026

Copy link
Copy Markdown
Author

@plainheart Can you take a look please? I would like to get this merged :)

@plainheart plainheart added this to the 6.2.0 milestone Jul 29, 2026
@plainheart
plainheart requested review from 100pah and Copilot July 29, 2026 23:58
@plainheart plainheart changed the title Proper touch pinch support for Zoom & Pan fix(roam): proper touch pinch support for zoom & pan Jul 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates RoamController’s touch pinch handling to use the real ZRender-reported pinchScale, add two-finger pan during pinch (by tracking pinch-center movement), and make pinch capture behave more like mouse drag (capture once, then continue handling outside the roam area).

Changes:

  • Use e.pinchScale directly for zoom instead of a fixed 1.1 step.
  • Track pinch center (_pinchX/_pinchY) to emit pan during a pinch when the center moves.
  • Split roam capability into _moveEnabled / _zoomEnabled and only attach the pinch listener when either is enabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +441 to +451
// Gate only the beginning of a pinch capture.
// Once captured, keep handling movement outside the roam area.
// Requiring a native touchstart for a new capture also
// prevents another RoamController from picking up an already-moving pinch.
const isTouchStart = e.event.type === 'touchstart';
const isPinchStart = !this._pinching || isTouchStart;
if (isPinchStart) {
if (!isTouchStart || !this._checkPointer(e, originX, originY)) {
return;
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZRender does emit a pinch during touchstart once the event contains two touches. GestureMgr falls back to using the current sample as pinchPre, producing pinchScale === 1, and Handler.processGesture dispatches that event while the native event type remains touchstart.

I verified this against ZRender 6.1.0 and the current ZRender source.

Requiring touchstart here is intentional: it ensures pinch ownership is decided only when the gesture begins. Allowing an uncaptured controller to start on touchmove would let a pinch be acquired after entering another roam area, potentially causing unexpected mid-gesture zoom or transferring ownership between overlapping controllers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And empirically this works well too - you may take a look at the attached videos

Comment on lines 219 to +221
}
this._moveEnabled = moveEnabled;
this._zoomEnabled = zoomEnabled;

@NightRa NightRa Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, you’re correct that _controlType is never assigned, so the existing idempotency check always removes and re-adds the listeners, but this behavior predates this PR.

Assigning _controlType alone may not be safe, however. Listener registration also captures component, zlevel, z, and z2, which determine precedence between overlapping controllers. These values are recalculated by every enable() call; skipping registration solely because controlType is unchanged could leave stale ordering metadata when a component’s z-order changes.

A complete fix should invalidate or update the registration when either controlType or that z-order metadata changes. Since this is an existing issue rather than a regression from the pinch changes, I suggest handling it separately.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@plainheart / @100pah - do you want me to implement this as well here? It's a bit out of scope and will make the PR quite larger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] pinch-to-zoom is too responsive on mobile [Bug] Pinch on maps has too high scale factor, thus feeling unnatural

3 participants