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