CM-2 UI: Chart viewport resets to full-history view on every 30s candle refresh; user zoom/scroll position is not preserved #336

Closed
opened 2026-06-05 15:46:36 +00:00 by totdking · 17 comments
totdking commented 2026-06-05 15:46:36 +00:00 (Migrated from gitlab.com)
No description provided.
totdking commented 2026-06-05 16:07:57 +00:00 (Migrated from gitlab.com)

Summary

Every 30 seconds the candle query refetches and PriceChartLightweightCanvas calls timeScale().fitContent() immediately after writing the new data. This snaps the chart viewport back to "show all candles" regardless of where the user has scrolled or zoomed. A user inspecting a specific time window (e.g. zoomed into the last 4 hours on a 1h chart) will be kicked back to the full-history view 30 seconds later with no warning.

The headline "Last" price updates every 15 seconds via the trades tape and does not cause any visual flicker — that part of CM-2 passes.


Root cause (code verified)

PriceChartLightweightCanvas.tsx:227–234 — data-refresh useEffect:

useEffect(() => {
  if (!candleSeriesRef.current || !volumeSeriesRef.current) return
  candleSeriesRef.current.setData(candlePoints)       // full replace, not series.update()
  volumeSeriesRef.current.setData(volumePoints)
  if (indicatorRefs.current.sma7) indicatorRefs.current.sma7.setData(sma7Points)
  if (indicatorRefs.current.sma25) indicatorRefs.current.sma25.setData(sma25Points)
  if (indicatorRefs.current.rsi) indicatorRefs.current.rsi.setData(rsiPoints)
  chartRef.current?.timeScale().fitContent()          // ← resets viewport every refresh
}, [candlePoints, volumePoints, sma7Points, sma25Points, rsiPoints])

This effect runs whenever candlePoints changes — which happens on every candlesQuery refetch (refetchInterval: 30_000 in PriceChart.tsx:60). Because setData replaces all candle data and fitContent() fits the time scale to the full dataset, the user's viewport is unconditionally reset to the full candle range on each refresh cycle.

The correct pattern for live chart updates is:

  1. Use series.update(latestCandle) to append or update only the most recent bar (no viewport side effect)
  2. Call fitContent() only on initial mount or on deliberate interval/pair switch — not on every background refetch

fitContent() is also called at:

  • Line 149: during chart initialization — correct ✓
  • Line 224: when indicator overlays are toggled — correct ✓
  • Line 234: on every live data refresh — incorrect, this is the bug

Steps to reproduce

  1. Open /trade/:pairAddr or /charts, connect Keplr wallet
  2. Select a pair and wait for the chart to load (1h interval)
  3. Scroll or zoom the chart to focus on a specific time range (e.g. zoom into recent 10 candles)
  4. Wait 30 seconds (the candle refetch interval)
  5. Observe: the chart viewport snaps back to fit all candles — the custom zoom is lost

Repeatable every 30 seconds as long as the chart is on screen.


Expected behavior

Background candle data refreshes should update the chart data without affecting the user's viewport. If a new candle is appended, series.update() should be used instead of series.setData(). timeScale().fitContent() should only be called on initial mount, pair switch, or interval switch — not during routine background refetches.


Actual behavior

Every 30-second background refetch calls setData on all series and then fitContent(), resetting the viewport to show the full candle history and discarding any user-set zoom or scroll position.


Environment

  • Chain: localterra
  • LCD: http://localhost:1317
  • Wallet: Keplr (Terra Classic)
  • Browser: Chromium (DevTools open)
  • Pages affected: /trade/:pairAddr (embedded chart), /charts (standalone chart page)
  • Chart interval: any (30s refetch applies to all intervals)
  • Network throttle applied: No

Severity: P2(Polish) : chart data updates correctly; the viewport reset is a UX regression that makes the chart unusable for any analysis that requires holding a zoom level for more than 30 seconds.

cc: @PlasticDigits

### Summary Every 30 seconds the candle query refetches and `PriceChartLightweightCanvas` calls `timeScale().fitContent()` immediately after writing the new data. This snaps the chart viewport back to "show all candles" regardless of where the user has scrolled or zoomed. A user inspecting a specific time window (e.g. zoomed into the last 4 hours on a 1h chart) will be kicked back to the full-history view 30 seconds later with no warning. The headline "Last" price updates every 15 seconds via the trades tape and does not cause any visual flicker — that part of CM-2 passes. --- ### Root cause (code verified) **`PriceChartLightweightCanvas.tsx:227–234` — data-refresh `useEffect`:** ```ts useEffect(() => { if (!candleSeriesRef.current || !volumeSeriesRef.current) return candleSeriesRef.current.setData(candlePoints) // full replace, not series.update() volumeSeriesRef.current.setData(volumePoints) if (indicatorRefs.current.sma7) indicatorRefs.current.sma7.setData(sma7Points) if (indicatorRefs.current.sma25) indicatorRefs.current.sma25.setData(sma25Points) if (indicatorRefs.current.rsi) indicatorRefs.current.rsi.setData(rsiPoints) chartRef.current?.timeScale().fitContent() // ← resets viewport every refresh }, [candlePoints, volumePoints, sma7Points, sma25Points, rsiPoints]) ``` This effect runs whenever `candlePoints` changes — which happens on every `candlesQuery` refetch (`refetchInterval: 30_000` in `PriceChart.tsx:60`). Because `setData` replaces all candle data and `fitContent()` fits the time scale to the full dataset, the user's viewport is unconditionally reset to the full candle range on each refresh cycle. The correct pattern for live chart updates is: 1. Use `series.update(latestCandle)` to append or update only the most recent bar (no viewport side effect) 2. Call `fitContent()` only on initial mount or on deliberate interval/pair switch — not on every background refetch `fitContent()` is also called at: - Line 149: during chart initialization — correct ✓ - Line 224: when indicator overlays are toggled — correct ✓ - Line 234: on every live data refresh — **incorrect**, this is the bug --- ### Steps to reproduce 1. Open `/trade/:pairAddr` or `/charts`, connect Keplr wallet 2. Select a pair and wait for the chart to load (1h interval) 3. Scroll or zoom the chart to focus on a specific time range (e.g. zoom into recent 10 candles) 4. Wait 30 seconds (the candle refetch interval) 5. Observe: the chart viewport snaps back to fit all candles — the custom zoom is lost Repeatable every 30 seconds as long as the chart is on screen. --- ### Expected behavior Background candle data refreshes should update the chart data without affecting the user's viewport. If a new candle is appended, `series.update()` should be used instead of `series.setData()`. `timeScale().fitContent()` should only be called on initial mount, pair switch, or interval switch — not during routine background refetches. --- ### Actual behavior Every 30-second background refetch calls `setData` on all series and then `fitContent()`, resetting the viewport to show the full candle history and discarding any user-set zoom or scroll position. --- ### Environment - Chain: localterra - LCD: [http://localhost:1317](http://localhost:1317) - Wallet: Keplr (Terra Classic) - Browser: Chromium (DevTools open) - Pages affected: `/trade/:pairAddr` (embedded chart), `/charts` (standalone chart page) - Chart interval: any (30s refetch applies to all intervals) - Network throttle applied: No --- **Severity:** P2(Polish) : chart data updates correctly; the viewport reset is a UX regression that makes the chart unusable for any analysis that requires holding a zoom level for more than 30 seconds. cc: @PlasticDigits
ghost1 commented 2026-06-06 06:53:09 +00:00 (Migrated from gitlab.com)

mentioned in commit 9c27c35016

mentioned in commit 9c27c35016e52e61b3484a2c574f6e95c66da3fe
PlasticDigits commented 2026-06-06 06:53:47 +00:00 (Migrated from gitlab.com)

mentioned in merge request !822

mentioned in merge request !822
PlasticDigits commented 2026-06-06 06:53:58 +00:00 (Migrated from gitlab.com)

Implementation complete — MR !822: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/104

Root cause: PriceChartLightweightCanvas called timeScale().fitContent() on every candle data refresh (30s refetchInterval), resetting zoom/scroll.

Fix: New priceChartLightweightSeriesSync.ts uses series.update() for incremental refreshes; fitContent() limited to initial mount and indicator toggles.

Acceptance

Item Result How verified
Background refetch preserves viewport PASS PriceChartLightweightCanvas.test.tsx — background data refresh does not call fitContent
Live updates use series.update() PASS priceChartLightweightSeriesSync.test.ts (8 cases)
Interval switch still uses setData PASS PriceChart.test.tsx — reuses one chart instance across many interval switches
Manual 30s zoom hold SKIP Needs browser QA on local stack

Automated commands (all PASS):

npm run test:run -- src/components/charts/__tests__/priceChartLightweightSeriesSync.test.ts
npm run test:run -- src/components/charts/__tests__/PriceChartLightweightCanvas.test.tsx
npm run test:run -- src/components/charts/__tests__/PriceChart.test.tsx
Implementation complete — MR !822: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/104 **Root cause:** `PriceChartLightweightCanvas` called `timeScale().fitContent()` on every candle data refresh (30s `refetchInterval`), resetting zoom/scroll. **Fix:** New `priceChartLightweightSeriesSync.ts` uses `series.update()` for incremental refreshes; `fitContent()` limited to initial mount and indicator toggles. ### Acceptance | Item | Result | How verified | |------|--------|--------------| | Background refetch preserves viewport | PASS | `PriceChartLightweightCanvas.test.tsx` — *background data refresh does not call fitContent* | | Live updates use `series.update()` | PASS | `priceChartLightweightSeriesSync.test.ts` (8 cases) | | Interval switch still uses `setData` | PASS | `PriceChart.test.tsx` — *reuses one chart instance across many interval switches* | | Manual 30s zoom hold | SKIP | Needs browser QA on local stack | Automated commands (all PASS): ``` npm run test:run -- src/components/charts/__tests__/priceChartLightweightSeriesSync.test.ts npm run test:run -- src/components/charts/__tests__/PriceChartLightweightCanvas.test.tsx npm run test:run -- src/components/charts/__tests__/PriceChart.test.tsx ```
PlasticDigits commented 2026-06-06 07:07:46 +00:00 (Migrated from gitlab.com)

mentioned in commit f95ffd174a

mentioned in commit f95ffd174a2b6c7a276f71303c55dfc38e8609ba
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-06 07:07:47 +00:00
Brouie commented 2026-06-08 00:20:36 +00:00 (Migrated from gitlab.com)

mentioned in merge request !834

mentioned in merge request !834
Brouie commented 2026-06-08 00:32:39 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
ghost1 commented 2026-06-08 05:24:17 +00:00 (Migrated from gitlab.com)

mentioned in commit f875d5388a

mentioned in commit f875d5388a17e2467de35f7dc805ee7d77e6cea7
PlasticDigits commented 2026-06-08 08:43:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 50cf53e2cf

mentioned in commit 50cf53e2cf602ff994b274e3812e180aa20a0eb9
PlasticDigits commented 2026-06-08 08:43:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 0e3afcaef3

mentioned in commit 0e3afcaef332b416792e99676407524ae3be2ef3
PlasticDigits commented 2026-06-08 13:42:28 +00:00 (Migrated from gitlab.com)

mentioned in commit 5a961085c1

mentioned in commit 5a961085c1e783e5506661f684197414ead933c0
PlasticDigits commented 2026-06-08 13:42:29 +00:00 (Migrated from gitlab.com)

mentioned in commit 9377f88b68

mentioned in commit 9377f88b680054da5b92d3ea4a4927c6b253e326
PlasticDigits commented 2026-06-08 13:42:30 +00:00 (Migrated from gitlab.com)

mentioned in commit 65876e17c7

mentioned in commit 65876e17c74fed89111b3928c9e2229ded5eb4de
PlasticDigits commented 2026-08-17 03:51:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #543

mentioned in issue #543
PlasticDigits commented 2026-08-29 14:16:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #705

mentioned in issue #705
PlasticDigits commented 2026-08-29 15:21:04 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1197

mentioned in merge request !1197
PlasticDigits commented 2026-09-01 08:14:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #717

mentioned in issue #717
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#336
No description provided.