Preserve the visible range when the content rect is resized (chart jumps sideways after a zoom)#811
Conversation
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.
Yes, it's always good to have tests. Please add an androidTest in advance to see the improvement with this. (If possible) Thank you ! |
Adds two instrumented tests covering the viewport preservation fix in ViewPortHandler.restrainViewPort (PR AppDevNext#811). These must run on a device because the library's JVM unit tests stub android.graphics and cannot exercise the real Matrix math the fix depends on. The first test reproduces a real-world scenario: deeply zoomed/panned viewport, then content rect resize (from y-axis label growth). It verifies the visible x-range is preserved and reconstructs the pre-fix behavior to show it would shift ~83 x-units on a 3600-unit range. The second test asserts the pan ratios (transX/contentWidth, transY/contentHeight) are preserved across the resize. Also updates chartLib/build.gradle.kts to add androidTest dependencies.
|
Thanks! I've pushed an androidTest — commit ae79148 on this branch. What's covered (chartLib/src/androidTest/.../ViewPortHandlerViewportPreservationTest.kt, two tests):
Why instrumented rather than a JVM unit test: the library's unit tests run with unitTests.isReturnDefaultValues = true, which stubs android.graphics.Matrix/RectF out — fine for the pure-data tests, but it means the viewport transform math can't be exercised there. An instrumented test gets the real Matrix. To enable it I added an androidTest source set to chartLib/build.gradle.kts (androidx.test.ext:junit, androidx.test:runner, and the JUnit runner). Verified green on a physical device (moto g stylus 5G, Android 14); ./gradlew :chartLib:lint is clean. One thing to check on your side: if CI doesn't currently run connectedAndroidTest on an emulator, these won't execute there as-is. |
Problem
The chart jumps sideways the moment a zoom gesture ends.
BarLineChartTouchListenerdeferschart.calculateOffsets()toACTION_UP:Zooming the y-axis changes the width of the y-axis labels (e.g.
1000becomes1012.5), so this deferred call resizes the content rect only once the gesture finishes.The pan, however, lives in
matrixTouchas 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: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:
ACTION_UPscaleXandtransXare 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_ZOOMthat 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 callscalculateOffsets()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:I did not add a regression test:
chartLib's JVM tests run withunitTests.isReturnDefaultValues = true, which stubsandroid.graphics.MatrixandRectFto return zeros, so the matrix arithmetic this touches cannot be exercised there, andchartLibhas 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 anandroidTestsource set for the library).