Skip to content

Preserve the visible range when the content rect is resized (chart jumps sideways after a zoom)#811

Open
oliverClimbs wants to merge 1 commit into
AppDevNext:masterfrom
oliverClimbs:fix/viewport-shift-on-offset-recalc
Open

Preserve the visible range when the content rect is resized (chart jumps sideways after a zoom)#811
oliverClimbs wants to merge 1 commit into
AppDevNext:masterfrom
oliverClimbs:fix/viewport-shift-on-offset-recalc

Conversation

@oliverClimbs

Copy link
Copy Markdown
Contributor

Problem

The chart jumps sideways the moment a zoom gesture ends.

BarLineChartTouchListener defers chart.calculateOffsets() to ACTION_UP:

if (mTouchMode == X_ZOOM || Y_ZOOM || PINCH_ZOOM || POST_ZOOM) {
    // Range might have changed, which means that Y-axis labels
    // could have changed in size, affecting Y-axis size.
    chart.calculateOffsets()

Zooming the y-axis changes the width of the y-axis labels (e.g. 1000 becomes 1012.5), so this deferred call resizes the content rect only once the gesture finishes.

The pan, however, lives in matrixTouch as a pixel translation, while the value-to-pixel scale is derived from the content rect width. restrainViewPort() resizes the rect without re-mapping that translation, so the same pixel offset suddenly denotes a different x-value and the visible range slides:

Δx ≈ totalXRange × (W − W') / W'

The displacement is a fraction of the entire x-range, independent of zoom depth, and it grows with transX — so the further you are zoomed in and panned, the more violent it gets.

Measured on device

A chart whose x-axis spans a ~30-minute recording, zoomed to 112×, panned right:

offsetLeft contentW scaleX transX lowestVisibleX
during the pinch 64.0 951.0 122.12 −101209.2 25,884,092
after ACTION_UP 69.0 948.0 122.12 −101209.2 25,966,004

scaleX and transX are unchanged, but a 3px content-rect change moved the viewport by 81,912 ms — about 82 seconds of data. In another capture, a 2px change at 112× zoom displaced the chart by 143 pixels.

An X_ZOOM that happened not to change the label widths produced no offset change and no jump — which isolates the cause precisely.

The same applies to the fling path: computeScroll() also calls calculateOffsets() when the deceleration settles.

Fix

Rescale the touch matrix's translation by the same ratio as the content rect whenever the rect is resized, then re-clamp. This keeps the visible range where the user left it.

The visible extent itself is unaffected: it is axis range / touch scale, which does not depend on the content width — so preserving the left edge preserves the whole window.

It is a no-op before the first layout (previousWidth == 0), and a no-op when unzoomed (transX == 0).

Verification

Verified on a physical device (moto g stylus 5G, Android 14) against tag 3.1.0.31, built as an AAR and consumed by a real app whose chart spans a long time range:

  • Pinch-zoom on the y-axis while panned right — no jump (previously ~82s / 143px).
  • Fling — no jump when the deceleration settles.
  • The y-position the user zoomed to is preserved.

I did not add a regression test: chartLib's JVM tests run with unitTests.isReturnDefaultValues = true, which stubs android.graphics.Matrix and RectF to return zeros, so the matrix arithmetic this touches cannot be exercised there, and chartLib has no instrumented test source set. Happy to add coverage if you'd like to point me at the approach you'd prefer (e.g. Robolectric, or an androidTest source set for the library).

The pan is stored in the touch matrix as a pixel translation, while the
value-to-pixel scale is derived from the content rect. Resizing the content
rect therefore makes the same pixel translation denote a different value,
sliding the visible range.

This is most visible after a zoom gesture: BarLineChartTouchListener defers
chart.calculateOffsets() to ACTION_UP, so a y-zoom that widens the y-axis
labels resizes the content rect only once the gesture ends, and the chart
jumps sideways. The same happens when a fling settles, via computeScroll().

Scale the translation by the same ratio as the content rect so the visible
range survives the resize. The visible extent is unaffected: it is
(axis range / touch scale), which does not depend on the content width.
@hannesa2 hannesa2 added the bugfix Something isn't working label Jul 12, 2026
@hannesa2

Copy link
Copy Markdown
Collaborator

Happy to add coverage if you'd like to point me at the approach you'd prefer (e.g. Robolectric, or an androidTest source set for the library).

Yes, it's always good to have tests. Please add an androidTest in advance to see the improvement with this. (If possible)
So I wait for that, before I merge this

Thank you !

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

Labels

bugfix Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants