From 4afea6bb973dab6b9f57ef36fdea4576ee2bace6 Mon Sep 17 00:00:00 2001 From: dekun Date: Mon, 8 Jun 2026 07:46:14 +0800 Subject: [PATCH] Fix chart tail viewport after 5m/15m timeframe switches. Snap to latest candles on period change and only preserve scroll position when viewing history. Co-authored-by: Cursor --- manual_trading_hub/static/chart.js | 62 ++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index 8774c13..bae3e74 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -178,6 +178,7 @@ let exhaustedLeft = false; let loadingLeft = false; let chartDataLoading = false; + let chartViewEpoch = 0; let priceTagTimer = null; let tfDigitBuf = ""; let tfDigitTimer = null; @@ -2025,6 +2026,22 @@ return true; } + function isViewingChartTail(range, candleCount) { + if (!range || candleCount <= 0) return true; + const maxTo = candleCount - 1 + RIGHT_OFFSET_BARS; + return range.to >= maxTo - 24; + } + + function tailVisibleLogicalRange(candleCount) { + const n = Math.max(0, Number(candleCount) || 0); + if (n <= 0) return null; + const visible = Math.min(DEFAULT_VISIBLE_BARS, n); + return { + from: Math.max(0, n - visible), + to: n - 1 + RIGHT_OFFSET_BARS, + }; + } + function clearChartSeriesData() { lastCandles = []; candleByTime = {}; @@ -2149,9 +2166,12 @@ if (!exKey || !sym || !lastCandles.length || chartDataLoading) return; if (!lastViewKey || vKey !== lastViewKey) return; const myToken = loadToken; + const epochAtStart = chartViewEpoch; const candleCountBefore = lastCandles.length; let savedRange = null; if (chart) savedRange = chart.timeScale().getVisibleLogicalRange(); + const wasViewingTail = + !savedRange || isViewingChartTail(savedRange, candleCountBefore); try { const data = await fetchChartChunk({ exchange_key: exKey, @@ -2161,6 +2181,7 @@ }); if (myToken !== loadToken) return; if (vKey !== lastViewKey) return; + if (epochAtStart !== chartViewEpoch) return; if (!data.ok || !data.candles || !data.candles.length) return; if (data.price_tick != null) { priceTick = data.price_tick; @@ -2172,13 +2193,23 @@ } } applyCandlesToChart(mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }), 0); - if ( + if (epochAtStart !== chartViewEpoch) return; + const n = lastCandles.length; + const curRange = chart && chart.timeScale().getVisibleLogicalRange(); + if (wasViewingTail) { + const tailRange = tailVisibleLogicalRange(n); + if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange); + } else if ( savedRange && - isVisibleRangeValidForCandles(savedRange, lastCandles.length) && - Math.abs(lastCandles.length - candleCountBefore) < chartChunkLimit(tf) + isVisibleRangeValidForCandles(savedRange, n) && + Math.abs(n - candleCountBefore) < chartChunkLimit(tf) ) { chart.timeScale().setVisibleLogicalRange(savedRange); + } else if (!isVisibleRangeValidForCandles(curRange, n)) { + const tailRange = tailVisibleLogicalRange(n); + if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange); } + updateVisibleRangeMarkers(); if (posContext) { updateLivePosPnl(); refreshPosPnlFromBoard(); @@ -2200,18 +2231,17 @@ function applyDefaultVisibleRange() { if (!chart || !lastCandles.length) return; - const n = lastCandles.length; - const visible = Math.min(DEFAULT_VISIBLE_BARS, n); - const from = Math.max(0, n - visible); - const to = n - 1 + RIGHT_OFFSET_BARS; - applyChartRightGap(); - requestAnimationFrame(function () { - requestAnimationFrame(function () { - if (!chart || !lastCandles.length) return; - chart.timeScale().setVisibleLogicalRange({ from: from, to: to }); - updateVisibleRangeMarkers(); - }); - }); + function applyOnce() { + if (!chart || !lastCandles.length) return; + const r = tailVisibleLogicalRange(lastCandles.length); + if (!r) return; + applyChartRightGap(); + chart.timeScale().setVisibleLogicalRange(r); + updateVisibleRangeMarkers(); + } + applyOnce(); + requestAnimationFrame(applyOnce); + setTimeout(applyOnce, 0); } function updateVisibleRangeMarkers() { @@ -2429,6 +2459,7 @@ const resetView = !!force || vKey !== lastViewKey; chartDataLoading = true; if (resetView) { + chartViewEpoch += 1; resetChartHistoryState(); lastViewKey = ""; clearChartSeriesData(); @@ -2592,6 +2623,7 @@ if (elFsTf) { elFsTf.addEventListener("change", function () { currentTf = elFsTf.value || "1d"; + lastViewKey = ""; syncMainFromFsToolbar(); tickLiveClock(); loadChart(false);