Fix market chart viewport jump when switching timeframes.

Reset visible range and block stale left-pan/tail refresh from applying the previous period logical range to new candles.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 07:40:45 +08:00
parent 11cc482599
commit 06897c59f1
+69 -7
View File
@@ -177,6 +177,7 @@
let currentTf = "1d"; let currentTf = "1d";
let exhaustedLeft = false; let exhaustedLeft = false;
let loadingLeft = false; let loadingLeft = false;
let chartDataLoading = false;
let priceTagTimer = null; let priceTagTimer = null;
let tfDigitBuf = ""; let tfDigitBuf = "";
let tfDigitTimer = null; let tfDigitTimer = null;
@@ -809,6 +810,7 @@
if (elTf) elTf.value = tf; if (elTf) elTf.value = tf;
if (elFsTf) elFsTf.value = tf; if (elFsTf) elFsTf.value = tf;
currentTf = tf; currentTf = tf;
lastViewKey = "";
tickLiveClock(); tickLiveClock();
updateHeaderLabels( updateHeaderLabels(
elSymbol && elSymbol.value.trim().toUpperCase(), elSymbol && elSymbol.value.trim().toUpperCase(),
@@ -1953,7 +1955,17 @@
chart.timeScale().subscribeVisibleLogicalRangeChange(function (range) { chart.timeScale().subscribeVisibleLogicalRangeChange(function (range) {
updateVisibleRangeMarkers(); updateVisibleRangeMarkers();
updatePriceTag(); updatePriceTag();
if (!range || loadingLeft || exhaustedLeft || !lastCandles.length) return; if (
!range ||
chartDataLoading ||
loadingLeft ||
exhaustedLeft ||
!lastCandles.length ||
!lastViewKey
) {
return;
}
if (currentChartViewKey() !== lastViewKey) return;
if (range.from < CHART_LOAD_LEFT_THRESHOLD) { if (range.from < CHART_LOAD_LEFT_THRESHOLD) {
void loadOlderCandles(); void loadOlderCandles();
} }
@@ -1996,6 +2008,30 @@
loadingLeft = false; loadingLeft = false;
} }
function currentChartViewKey() {
const exKey = (elExchange && elExchange.value) || "";
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
const tf = (elTf && elTf.value) || currentTf || "1d";
if (!exKey || !sym) return "";
return viewKey(exKey, sym, tf);
}
function isVisibleRangeValidForCandles(range, candleCount) {
if (!range || candleCount <= 0) return false;
const maxTo = candleCount - 1 + RIGHT_OFFSET_BARS;
if (range.from < -2 || range.to < 0) return false;
if (range.to > maxTo + 8) return false;
if (range.from > candleCount - 1) return false;
return true;
}
function clearChartSeriesData() {
lastCandles = [];
candleByTime = {};
if (candleSeries) candleSeries.setData([]);
if (volumeSeries) volumeSeries.setData([]);
}
function mergeCandles(existing, incoming, opts) { function mergeCandles(existing, incoming, opts) {
opts = opts || {}; opts = opts || {};
const prepend = !!opts.prepend; const prepend = !!opts.prepend;
@@ -2064,11 +2100,13 @@
} }
async function loadOlderCandles() { async function loadOlderCandles() {
if (loadingLeft || exhaustedLeft || !lastCandles.length) return; if (chartDataLoading || loadingLeft || exhaustedLeft || !lastCandles.length) return;
const exKey = (elExchange && elExchange.value) || ""; const exKey = (elExchange && elExchange.value) || "";
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || ""; const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
const tf = (elTf && elTf.value) || "1d"; const tf = (elTf && elTf.value) || "1d";
if (!exKey || !sym) return; if (!exKey || !sym) return;
const vKey = viewKey(exKey, sym, tf);
if (!lastViewKey || vKey !== lastViewKey) return;
loadingLeft = true; loadingLeft = true;
const beforeMs = Number(lastCandles[0].time) * 1000; const beforeMs = Number(lastCandles[0].time) * 1000;
try { try {
@@ -2107,8 +2145,11 @@
const exKey = (elExchange && elExchange.value) || ""; const exKey = (elExchange && elExchange.value) || "";
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || ""; const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
const tf = (elTf && elTf.value) || "1d"; const tf = (elTf && elTf.value) || "1d";
if (!exKey || !sym || !lastCandles.length) return; const vKey = viewKey(exKey, sym, tf);
if (!exKey || !sym || !lastCandles.length || chartDataLoading) return;
if (!lastViewKey || vKey !== lastViewKey) return;
const myToken = loadToken; const myToken = loadToken;
const candleCountBefore = lastCandles.length;
let savedRange = null; let savedRange = null;
if (chart) savedRange = chart.timeScale().getVisibleLogicalRange(); if (chart) savedRange = chart.timeScale().getVisibleLogicalRange();
try { try {
@@ -2119,6 +2160,7 @@
limit: chartChunkLimit(tf), limit: chartChunkLimit(tf),
}); });
if (myToken !== loadToken) return; if (myToken !== loadToken) return;
if (vKey !== lastViewKey) 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;
@@ -2130,7 +2172,13 @@
} }
} }
applyCandlesToChart(mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }), 0); applyCandlesToChart(mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }), 0);
if (savedRange) chart.timeScale().setVisibleLogicalRange(savedRange); if (
savedRange &&
isVisibleRangeValidForCandles(savedRange, lastCandles.length) &&
Math.abs(lastCandles.length - candleCountBefore) < chartChunkLimit(tf)
) {
chart.timeScale().setVisibleLogicalRange(savedRange);
}
if (posContext) { if (posContext) {
updateLivePosPnl(); updateLivePosPnl();
refreshPosPnlFromBoard(); refreshPosPnlFromBoard();
@@ -2155,10 +2203,15 @@
const n = lastCandles.length; const n = lastCandles.length;
const visible = Math.min(DEFAULT_VISIBLE_BARS, n); const visible = Math.min(DEFAULT_VISIBLE_BARS, n);
const from = Math.max(0, n - visible); const from = Math.max(0, n - visible);
// to 延伸到最后一根 K 线之后,留出 RIGHT_OFFSET_BARS 根空白(K 线与价格轴间距)
const to = n - 1 + RIGHT_OFFSET_BARS; const to = n - 1 + RIGHT_OFFSET_BARS;
applyChartRightGap(); applyChartRightGap();
requestAnimationFrame(function () {
requestAnimationFrame(function () {
if (!chart || !lastCandles.length) return;
chart.timeScale().setVisibleLogicalRange({ from: from, to: to }); chart.timeScale().setVisibleLogicalRange({ from: from, to: to });
updateVisibleRangeMarkers();
});
});
} }
function updateVisibleRangeMarkers() { function updateVisibleRangeMarkers() {
@@ -2374,7 +2427,12 @@
const myToken = ++loadToken; const myToken = ++loadToken;
const vKey = viewKey(exKey, sym, tf); const vKey = viewKey(exKey, sym, tf);
const resetView = !!force || vKey !== lastViewKey; const resetView = !!force || vKey !== lastViewKey;
if (resetView) resetChartHistoryState(); chartDataLoading = true;
if (resetView) {
resetChartHistoryState();
lastViewKey = "";
clearChartSeriesData();
}
if (elStatus) { if (elStatus) {
elStatus.className = "market-status"; elStatus.className = "market-status";
elStatus.textContent = "加载中…"; elStatus.textContent = "加载中…";
@@ -2402,8 +2460,8 @@
applyChartPriceFormat(); applyChartPriceFormat();
} }
applyCandlesToChart(alignCandlesToTick(data.candles), 0); applyCandlesToChart(alignCandlesToTick(data.candles), 0);
if (resetView) {
lastViewKey = vKey; lastViewKey = vKey;
if (resetView) {
applyDefaultVisibleRange(); applyDefaultVisibleRange();
} }
syncPosContextForView(exKey, sym); syncPosContextForView(exKey, sym);
@@ -2443,6 +2501,8 @@
elStatus.className = "market-status err"; elStatus.className = "market-status err";
elStatus.textContent = String(e.message || e); elStatus.textContent = String(e.message || e);
} }
} finally {
if (myToken === loadToken) chartDataLoading = false;
} }
} }
@@ -2461,6 +2521,7 @@
tfDigitTimer = null; tfDigitTimer = null;
} }
currentTf = (elTf && elTf.value) || "1d"; currentTf = (elTf && elTf.value) || "1d";
lastViewKey = "";
tickLiveClock(); tickLiveClock();
syncFsToolbarFromMain(); syncFsToolbarFromMain();
loadChart(false); loadChart(false);
@@ -2470,6 +2531,7 @@
elExchange.addEventListener("change", function () { elExchange.addEventListener("change", function () {
updateExchangeDisplay(); updateExchangeDisplay();
syncFsToolbarFromMain(); syncFsToolbarFromMain();
lastViewKey = "";
loadChart(false); loadChart(false);
}); });
} }