Skip to content

feat(dataZoom): add per-axis wheel pan/zoom options for 'inside' dataZoom#21588

Open
takaebato wants to merge 2 commits intoapache:masterfrom
takaebato:feat/datazoom-xy-wheel
Open

feat(dataZoom): add per-axis wheel pan/zoom options for 'inside' dataZoom#21588
takaebato wants to merge 2 commits intoapache:masterfrom
takaebato:feat/datazoom-xy-wheel

Conversation

@takaebato
Copy link
Copy Markdown

@takaebato takaebato commented Apr 19, 2026

Brief Information

This pull request is in the type of:

  • bug fixing
  • new feature
  • others

What does this PR do?

Adds moveOnMouseWheelAxis and zoomOnMouseWheelAxis options that restrict an inside dataZoom to a single wheel axis, so horizontal and vertical wheel components can drive different dataZooms independently.

Fixed issues

Details

Before: What was the problem?

  1. No per-axis wheel routing. zrender's polyfill folds deltaX and deltaY into a single wheelDelta
    scalar, so the horizontal and vertical components of a wheel event can't be directed to different dataZooms
    separately.
  2. No axis restriction. When two inside dataZooms share a coord sys (one on each cartesian axis), any wheel direction pans / zooms both — there was no way to split them.

After: How does it behave after the fixing?

  1. RoamController's 'zoom' and 'scrollMove' events now carry per-axis fields (scaleX / scaleY,
    scrollDeltaX / scrollDeltaY) alongside the existing scale / scrollDelta scalars. Per-axis values are
    polyfilled from the native WheelEvent when deltaX / deltaY are available; pre-2013 IE falls back to
    caller-supplied defaults. Touch pinch mirrors the scalar into both per-axis fields since it has no distinct
    direction.
  2. Two new options on dataZoom.inside:
  • moveOnMouseWheelAxis?: 'horizontal' | 'vertical' — pin the pan to a single wheel axis.
  • zoomOnMouseWheelAxis?: 'horizontal' | 'vertical' — pin the zoom to a single wheel axis.
  1. With both options unset, existing behavior is preserved.

Document Info

One of the following should be checked.

Misc

Security Checking

  • This PR uses security-sensitive Web APIs.

ZRender Changes

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

The per-axis values are read directly from the native WheelEvent (via e.event.deltaX / .deltaY, with a fallback to
the legacy WebKit wheelDeltaX / wheelDeltaY), so zrender's scalar wheelDelta API stays untouched and no companion zrender PR is needed.

Related test cases or examples to use the new APIs

test/dataZoom-inside-wheel-axis.html (added in this PR) — 11 manual panels covering the default fallback,
moveOnMouseWheelAxis / zoomOnMouseWheelAxis with various axis configs, zoom / pan composition, shift-based
horizontal scroll, and polar coordinate systems.

Merging options

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

Other information

Relation to #18365. Same underlying problem (zrender's wheelDelta collapses both wheel axes into a single scalar, so horizontal and vertical inputs can't be routed separately), different API. Rather than extending moveOnMouseWheel /
zoomOnMouseWheel with composite values like 'x+shift' / 'y+none', this PR splits the concern in two:

  • moveOnMouseWheelAxis / zoomOnMouseWheelAxiswhich wheel axis drives the dataZoom.
  • moveOnMouseWheel / zoomOnMouseWheelunder which modifier state the action fires.

Keeping the two orthogonal means axis restriction composes cleanly with any existing modifier mode, without a
combinatorial explosion of composite strings. The 'none' modifier value also mentioned in that issue is out of
scope here and can follow separately.

Touch pinch. Axis restriction applies only to wheel-driven pan / zoom. Touch pinch keeps zooming on every dataZoom
regardless of the axis restriction setting — a touch pinch is a 2D gesture with no distinct horizontal / vertical
component.

Future work. This is independent of the axis API introduced in this PR, but a follow-up is planned to tackle
the magnitude side. The FIXME in RoamController sits on a 3-tier heuristic that ignores deltaMode, still carries the IE-era / 120 division, and feels a bit fast on some setups. The plan is to derive the magnitude from the native WheelEvent directly, retire the FIXME, and add a wheelSensitivity option.

@echarts-bot echarts-bot Bot added PR: awaiting doc Document changes is required for this PR. PR: awaiting review labels Apr 19, 2026
@echarts-bot
Copy link
Copy Markdown

echarts-bot Bot commented Apr 19, 2026

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.

Document changes are required in this PR. Please also make a PR to apache/echarts-doc for document changes and update the issue id in the PR description. When the doc PR is merged, the maintainers will remove the PR: awaiting doc label.

Copy link
Copy Markdown
Contributor

@Justin-ZS Justin-ZS left a comment

Choose a reason for hiding this comment

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

Thanks, the API split between modifier gating and wheel-axis gating is clean, and the manual coverage is thoughtful. I found one blocking interaction issue, though: when the wheel direction does not match moveOnMouseWheelAxis / zoomOnMouseWheelAxis, the dataZoom becomes a no-op but the wheel event is still consumed in RoamController, so ordinary page/container scrolling is blocked. I think that axis filtering needs to happen before eventTool.stop() so non-matching wheel input can fall through.

this._checkTriggerMoveZoom(this, 'zoom', 'zoomOnMouseWheel', e, {
scale: scale, originX: originX, originY: originY, isAvailableBehavior: null
scale: scale,
scaleX: axisZoomScale(nativeEvent, 'horizontal', 1),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With moveOnMouseWheelAxis: 'horizontal' / zoomOnMouseWheelAxis: 'horizontal', a plain vertical wheel becomes a no-op in InsideZoomView (effectiveDelta === 0 / effectiveScale === 1). But the event is still consumed later in _checkTriggerMoveZoom(), so the page/container can no longer scroll. That turns an axis mismatch from "ignored" into "blocked". Could we move the axis check earlier so non-matching wheel directions fall through like the existing modifier-gated cases do?

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.

@Justin-ZS

Thanks! I hadn't considered that the event is consumed even when the axis doesn't match. Let me think through the right place to hoist the check and I'll follow up.

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.

@Justin-ZS

Fixed in 80bda33.

The axis check is now hoisted into RoamController._mousewheelHandler, so non-matching wheel directions fall through to the browser.

I also tightened mergeControllerParams to forward each dataZoom's actual zoomOnMouseWheel / moveOnMouseWheel truthiness instead of fixing both to true.
As a side effect, this also resolves a long-standing case where wheel events were stop()ped even when every inside dataZoom had zoomOnMouseWheel: false and moveOnMouseWheel: false — i.e. the wheel was being captured even when no dataZoom would do anything with it.

Note: modifier-mismatched wheel events (e.g. zoomOnMouseWheel: 'shift' with shift not held) exhibit the same shape of issue and are out of scope here.
I'm planning a follow-up PR that extends the modifier API into a more flexible form (possibly an object form like { shift: true, ctrl: false }) to address the requests in #18365 — that change should naturally resolve the modifier-mismatch fall-through too.

Panel E in test/dataZoom-inside-wheel-axis.html is a quick visual check: a plain vertical wheel over the chart should now scroll the page.

Copy link
Copy Markdown
Author

@takaebato takaebato Apr 26, 2026

Choose a reason for hiding this comment

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

A separate API design question: would it make sense to let callers express the "no restriction" state while keeping the field present?

There seem to be a couple of independent dimensions to consider:

  1. Whether to add an explicit literal (e.g. 'all', or 'both')
  2. Whether to admit null / undefined in the type

Some existing precedents in echarts I found:

So some candidate shapes might be:

// (a) literal only
moveOnMouseWheelAxis?: 'horizontal' | 'vertical' | 'all'

// (b) NullUndefined only
moveOnMouseWheelAxis?: 'horizontal' | 'vertical' | NullUndefined

// (c) both (matches outerBoundsMode / ModelFinderIndexQuery shape)
moveOnMouseWheelAxis?: 'horizontal' | 'vertical' | 'all' | NullUndefined

// (d) keep as-is
moveOnMouseWheelAxis?: 'horizontal' | 'vertical'

(b) is the minimal way to let callers keep the field present in the no-restriction state.
That said, (c) might also be a good fit since it aligns with the established outerBoundsMode / ModelFinderIndexQuery shape.
Happy with whichever direction.

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.

2 participants