Fix chart auto toggle to clearly split follow vs manual zoom.

On tail refresh, auto-on always snaps to latest candles while auto-off preserves the saved viewport with clamped range restore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 08:35:56 +08:00
parent bae78d8368
commit 5af0cbf286
2 changed files with 32 additions and 34 deletions
+31 -33
View File
@@ -2026,12 +2026,6 @@
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 markChartRangeUserAdjusted() { function markChartRangeUserAdjusted() {
chartRangeUserLocked = true; chartRangeUserLocked = true;
if (chartRangeLockTimer) clearTimeout(chartRangeLockTimer); if (chartRangeLockTimer) clearTimeout(chartRangeLockTimer);
@@ -2041,14 +2035,37 @@
}, 30000); }, 30000);
} }
function clampVisibleLogicalRange(range, candleCount) {
if (!range || candleCount <= 0) return null;
const maxTo = candleCount - 1 + RIGHT_OFFSET_BARS;
const from = Math.max(-2, Math.min(range.from, candleCount - 1));
const to = Math.max(0, Math.min(range.to, maxTo + 8));
if (to <= from) return null;
return { from: from, to: to };
}
function restoreVisibleLogicalRange(range, candleCount) { function restoreVisibleLogicalRange(range, candleCount) {
if (!chart || !range || !isVisibleRangeValidForCandles(range, candleCount)) return false; const clamped = clampVisibleLogicalRange(range, candleCount);
if (!chart || !clamped || !isVisibleRangeValidForCandles(clamped, candleCount)) return false;
suppressRangeUserLock = true; suppressRangeUserLock = true;
chart.timeScale().setVisibleLogicalRange(range); chart.timeScale().setVisibleLogicalRange(clamped);
suppressRangeUserLock = false; suppressRangeUserLock = false;
return true; return true;
} }
function applyPreservedVisibleRange(range, candleCount) {
if (!chart || !range || !candleCount) return;
function applyOnce() {
if (!chart || !lastCandles.length) return;
applyChartRightGap();
restoreVisibleLogicalRange(range, lastCandles.length);
updateVisibleRangeMarkers();
}
applyOnce();
requestAnimationFrame(applyOnce);
setTimeout(applyOnce, 0);
}
function shouldLoadOlderOnRange(range) { function shouldLoadOlderOnRange(range) {
if (!range || !lastCandles.length) return false; if (!range || !lastCandles.length) return false;
const n = lastCandles.length; const n = lastCandles.length;
@@ -2229,11 +2246,9 @@
if (!lastViewKey || vKey !== lastViewKey) return; if (!lastViewKey || vKey !== lastViewKey) return;
const myToken = loadToken; const myToken = loadToken;
const epochAtStart = chartViewEpoch; const epochAtStart = chartViewEpoch;
const candleCountBefore = lastCandles.length; const autoFollow = priceAutoScale;
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,
@@ -2255,37 +2270,20 @@
applyChartPriceFormat(); applyChartPriceFormat();
} }
} }
const autoFollow = priceAutoScale;
const shouldPreserve =
savedRange &&
isVisibleRangeValidForCandles(savedRange, candleCountBefore) &&
(!autoFollow || chartRangeUserLocked || !wasViewingTail);
applyCandlesToChart( applyCandlesToChart(
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }), mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
0, 0,
{ {
preserveRange: !!shouldPreserve, preserveRange: false,
skipAutoScale: !autoFollow, skipAutoScale: !autoFollow,
} }
); );
if (epochAtStart !== chartViewEpoch) return; if (epochAtStart !== chartViewEpoch) return;
const n = lastCandles.length; const n = lastCandles.length;
const minorTailUpdate = Math.abs(n - candleCountBefore) <= CHART_TAIL_REFRESH_LIMIT + 5; if (autoFollow) {
if (!autoFollow) { applyDefaultVisibleRange();
if (savedRange && isVisibleRangeValidForCandles(savedRange, n)) { } else if (savedRange) {
restoreVisibleLogicalRange(savedRange, n); applyPreservedVisibleRange(savedRange, n);
}
} else 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);
}
} }
scheduleRangeUiUpdate(); scheduleRangeUiUpdate();
if (posContext) { if (posContext) {
+1 -1
View File
@@ -349,7 +349,7 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script> <script src="/assets/chart.js?v=20260608-auto-viewport"></script>
<script src="/assets/archive.js?v=20260607-hub-archive-v6"></script> <script src="/assets/archive.js?v=20260607-hub-archive-v6"></script>
<script src="/assets/ai_review_render.js?v=2"></script> <script src="/assets/ai_review_render.js?v=2"></script>
<script src="/assets/app.js?v=20260607-hub-archive-v1"></script> <script src="/assets/app.js?v=20260607-hub-archive-v1"></script>