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 <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 07:46:14 +08:00
parent 06897c59f1
commit 4afea6bb97
+45 -13
View File
@@ -178,6 +178,7 @@
let exhaustedLeft = false; let exhaustedLeft = false;
let loadingLeft = false; let loadingLeft = false;
let chartDataLoading = false; let chartDataLoading = false;
let chartViewEpoch = 0;
let priceTagTimer = null; let priceTagTimer = null;
let tfDigitBuf = ""; let tfDigitBuf = "";
let tfDigitTimer = null; let tfDigitTimer = null;
@@ -2025,6 +2026,22 @@
return true; 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() { function clearChartSeriesData() {
lastCandles = []; lastCandles = [];
candleByTime = {}; candleByTime = {};
@@ -2149,9 +2166,12 @@
if (!exKey || !sym || !lastCandles.length || chartDataLoading) return; if (!exKey || !sym || !lastCandles.length || chartDataLoading) return;
if (!lastViewKey || vKey !== lastViewKey) return; if (!lastViewKey || vKey !== lastViewKey) return;
const myToken = loadToken; const myToken = loadToken;
const epochAtStart = chartViewEpoch;
const candleCountBefore = lastCandles.length; const candleCountBefore = lastCandles.length;
let savedRange = null; let savedRange = null;
if (chart) savedRange = chart.timeScale().getVisibleLogicalRange(); if (chart) savedRange = chart.timeScale().getVisibleLogicalRange();
const wasViewingTail =
!savedRange || isViewingChartTail(savedRange, candleCountBefore);
try { try {
const data = await fetchChartChunk({ const data = await fetchChartChunk({
exchange_key: exKey, exchange_key: exKey,
@@ -2161,6 +2181,7 @@
}); });
if (myToken !== loadToken) return; if (myToken !== loadToken) return;
if (vKey !== lastViewKey) return; if (vKey !== lastViewKey) return;
if (epochAtStart !== chartViewEpoch) return;
if (!data.ok || !data.candles || !data.candles.length) return; if (!data.ok || !data.candles || !data.candles.length) return;
if (data.price_tick != null) { if (data.price_tick != null) {
priceTick = data.price_tick; priceTick = data.price_tick;
@@ -2172,13 +2193,23 @@
} }
} }
applyCandlesToChart(mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }), 0); 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 && savedRange &&
isVisibleRangeValidForCandles(savedRange, lastCandles.length) && isVisibleRangeValidForCandles(savedRange, n) &&
Math.abs(lastCandles.length - candleCountBefore) < chartChunkLimit(tf) Math.abs(n - candleCountBefore) < chartChunkLimit(tf)
) { ) {
chart.timeScale().setVisibleLogicalRange(savedRange); chart.timeScale().setVisibleLogicalRange(savedRange);
} else if (!isVisibleRangeValidForCandles(curRange, n)) {
const tailRange = tailVisibleLogicalRange(n);
if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange);
} }
updateVisibleRangeMarkers();
if (posContext) { if (posContext) {
updateLivePosPnl(); updateLivePosPnl();
refreshPosPnlFromBoard(); refreshPosPnlFromBoard();
@@ -2200,18 +2231,17 @@
function applyDefaultVisibleRange() { function applyDefaultVisibleRange() {
if (!chart || !lastCandles.length) return; if (!chart || !lastCandles.length) return;
const n = lastCandles.length; function applyOnce() {
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; if (!chart || !lastCandles.length) return;
chart.timeScale().setVisibleLogicalRange({ from: from, to: to }); const r = tailVisibleLogicalRange(lastCandles.length);
if (!r) return;
applyChartRightGap();
chart.timeScale().setVisibleLogicalRange(r);
updateVisibleRangeMarkers(); updateVisibleRangeMarkers();
}); }
}); applyOnce();
requestAnimationFrame(applyOnce);
setTimeout(applyOnce, 0);
} }
function updateVisibleRangeMarkers() { function updateVisibleRangeMarkers() {
@@ -2429,6 +2459,7 @@
const resetView = !!force || vKey !== lastViewKey; const resetView = !!force || vKey !== lastViewKey;
chartDataLoading = true; chartDataLoading = true;
if (resetView) { if (resetView) {
chartViewEpoch += 1;
resetChartHistoryState(); resetChartHistoryState();
lastViewKey = ""; lastViewKey = "";
clearChartSeriesData(); clearChartSeriesData();
@@ -2592,6 +2623,7 @@
if (elFsTf) { if (elFsTf) {
elFsTf.addEventListener("change", function () { elFsTf.addEventListener("change", function () {
currentTf = elFsTf.value || "1d"; currentTf = elFsTf.value || "1d";
lastViewKey = "";
syncMainFromFsToolbar(); syncMainFromFsToolbar();
tickLiveClock(); tickLiveClock();
loadChart(false); loadChart(false);