Fix chart viewport regressions from tail refresh and period switch.

Remove pendingViewportEpoch, fetch only 30 tail bars on poll, restore wasViewingTail logic, and fix left-scroll range shift from actual merge delta.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 08:18:06 +08:00
parent 7ea51818f1
commit 35088be097
2 changed files with 25 additions and 15 deletions
+22 -15
View File
@@ -178,7 +178,7 @@
let chartRangeUserLocked = false;
let chartRangeLockTimer = null;
let suppressRangeUserLock = false;
let pendingViewportEpoch = -1;
const CHART_TAIL_REFRESH_LIMIT = 30;
let priceTagTimer = null;
let tfDigitBuf = "";
let tfDigitTimer = null;
@@ -2033,7 +2033,6 @@
}
function markChartRangeUserAdjusted() {
pendingViewportEpoch = -1;
chartRangeUserLocked = true;
if (chartRangeLockTimer) clearTimeout(chartRangeLockTimer);
chartRangeLockTimer = setTimeout(function () {
@@ -2168,6 +2167,7 @@
});
if (params.before_ms) qs.set("before_ms", String(params.before_ms));
if (params.refresh) qs.set("refresh", "1");
if (params.tail) qs.set("tail", "1");
const r = await fetch("/api/chart/ohlcv?" + qs.toString(), { credentials: "same-origin" });
const data = await r.json();
if (!r.ok) {
@@ -2197,8 +2197,10 @@
if (data.exhausted) exhaustedLeft = true;
const incoming = alignCandlesToTick(data.candles || []);
if (!incoming.length) return;
const shift = incoming.length;
applyCandlesToChart(mergeCandles(lastCandles, incoming, { prepend: true }), shift);
const prevLen = lastCandles.length;
const merged = mergeCandles(lastCandles, incoming, { prepend: true });
const shift = merged.length - prevLen;
applyCandlesToChart(merged, shift);
if (elStatus && !elStatus.classList.contains("err")) {
elStatus.textContent =
"已加载 " +
@@ -2230,12 +2232,15 @@
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,
symbol: sym,
timeframe: tf,
limit: chartChunkLimit(tf),
limit: CHART_TAIL_REFRESH_LIMIT,
tail: true,
});
if (myToken !== loadToken) return;
if (vKey !== lastViewKey) return;
@@ -2250,25 +2255,28 @@
applyChartPriceFormat();
}
}
const keepViewport =
pendingViewportEpoch !== chartViewEpoch &&
const shouldPreserve =
savedRange &&
isVisibleRangeValidForCandles(savedRange, candleCountBefore);
isVisibleRangeValidForCandles(savedRange, candleCountBefore) &&
(chartRangeUserLocked || !wasViewingTail);
applyCandlesToChart(
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
0,
{
preserveRange: !!keepViewport,
preserveRange: !!shouldPreserve,
skipAutoScale: chartRangeUserLocked,
}
);
if (epochAtStart !== chartViewEpoch) return;
const n = lastCandles.length;
if (pendingViewportEpoch === chartViewEpoch) {
applyDefaultVisibleRange();
} else if (savedRange && isVisibleRangeValidForCandles(savedRange, n)) {
restoreVisibleLogicalRange(savedRange, n);
} else if (!chartRangeUserLocked) {
const minorTailUpdate = Math.abs(n - candleCountBefore) <= CHART_TAIL_REFRESH_LIMIT + 5;
if (wasViewingTail && !chartRangeUserLocked) {
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
} else if (shouldPreserve && minorTailUpdate) {
if (!restoreVisibleLogicalRange(savedRange, n) && !chartRangeUserLocked) {
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
}
} else if (!restoreVisibleLogicalRange(savedRange, n) && !chartRangeUserLocked) {
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
if (!curRange || !isVisibleRangeValidForCandles(curRange, n)) {
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
@@ -2525,7 +2533,6 @@
chartDataLoading = true;
if (resetView) {
chartViewEpoch += 1;
pendingViewportEpoch = chartViewEpoch;
chartRangeUserLocked = false;
if (chartRangeLockTimer) {
clearTimeout(chartRangeLockTimer);