Preserve chart zoom and pan across tail refresh updates.
Keep the user viewport when SSE updates candles so zooming out to see the full series is not reset to the recent 200-bar view. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -175,6 +175,9 @@
|
|||||||
let chartViewEpoch = 0;
|
let chartViewEpoch = 0;
|
||||||
let rangeUiTimer = null;
|
let rangeUiTimer = null;
|
||||||
let loadOlderTimer = null;
|
let loadOlderTimer = null;
|
||||||
|
let chartRangeUserLocked = false;
|
||||||
|
let chartRangeLockTimer = null;
|
||||||
|
let suppressRangeUserLock = false;
|
||||||
let priceTagTimer = null;
|
let priceTagTimer = null;
|
||||||
let tfDigitBuf = "";
|
let tfDigitBuf = "";
|
||||||
let tfDigitTimer = null;
|
let tfDigitTimer = null;
|
||||||
@@ -1950,6 +1953,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
chart.timeScale().subscribeVisibleLogicalRangeChange(function (range) {
|
chart.timeScale().subscribeVisibleLogicalRangeChange(function (range) {
|
||||||
|
if (!chartDataLoading && range && !suppressRangeUserLock) {
|
||||||
|
markChartRangeUserAdjusted();
|
||||||
|
}
|
||||||
scheduleRangeUiUpdate();
|
scheduleRangeUiUpdate();
|
||||||
if (
|
if (
|
||||||
!range ||
|
!range ||
|
||||||
@@ -2025,6 +2031,23 @@
|
|||||||
return range.to >= maxTo - 24;
|
return range.to >= maxTo - 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markChartRangeUserAdjusted() {
|
||||||
|
chartRangeUserLocked = true;
|
||||||
|
if (chartRangeLockTimer) clearTimeout(chartRangeLockTimer);
|
||||||
|
chartRangeLockTimer = setTimeout(function () {
|
||||||
|
chartRangeLockTimer = null;
|
||||||
|
chartRangeUserLocked = false;
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreVisibleLogicalRange(range, candleCount) {
|
||||||
|
if (!chart || !range || !isVisibleRangeValidForCandles(range, candleCount)) return false;
|
||||||
|
suppressRangeUserLock = true;
|
||||||
|
chart.timeScale().setVisibleLogicalRange(range);
|
||||||
|
suppressRangeUserLock = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -2100,7 +2123,12 @@
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCandlesToChart(candles, rangeShift) {
|
function applyCandlesToChart(candles, rangeShift, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
let savedRange = null;
|
||||||
|
if (opts.preserveRange && chart) {
|
||||||
|
savedRange = chart.timeScale().getVisibleLogicalRange();
|
||||||
|
}
|
||||||
lastCandles = alignCandlesToTick(candles);
|
lastCandles = alignCandlesToTick(candles);
|
||||||
indexCandles(lastCandles);
|
indexCandles(lastCandles);
|
||||||
candleSeries.setData(lastCandles);
|
candleSeries.setData(lastCandles);
|
||||||
@@ -2109,13 +2137,19 @@
|
|||||||
if (rangeShift && chart) {
|
if (rangeShift && chart) {
|
||||||
const range = chart.timeScale().getVisibleLogicalRange();
|
const range = chart.timeScale().getVisibleLogicalRange();
|
||||||
if (range) {
|
if (range) {
|
||||||
|
suppressRangeUserLock = true;
|
||||||
chart.timeScale().setVisibleLogicalRange({
|
chart.timeScale().setVisibleLogicalRange({
|
||||||
from: range.from + rangeShift,
|
from: range.from + rangeShift,
|
||||||
to: range.to + rangeShift,
|
to: range.to + rangeShift,
|
||||||
});
|
});
|
||||||
|
suppressRangeUserLock = false;
|
||||||
}
|
}
|
||||||
|
} else if (savedRange) {
|
||||||
|
restoreVisibleLogicalRange(savedRange, lastCandles.length);
|
||||||
|
}
|
||||||
|
if (!opts.skipAutoScale) {
|
||||||
|
applyPriceAutoScale();
|
||||||
}
|
}
|
||||||
applyPriceAutoScale();
|
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
try {
|
try {
|
||||||
updateIndicators();
|
updateIndicators();
|
||||||
@@ -2194,8 +2228,6 @@
|
|||||||
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,
|
||||||
@@ -2216,20 +2248,18 @@
|
|||||||
applyChartPriceFormat();
|
applyChartPriceFormat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyCandlesToChart(mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }), 0);
|
applyCandlesToChart(
|
||||||
|
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
|
||||||
|
0,
|
||||||
|
{ preserveRange: true, skipAutoScale: chartRangeUserLocked }
|
||||||
|
);
|
||||||
if (epochAtStart !== chartViewEpoch) return;
|
if (epochAtStart !== chartViewEpoch) return;
|
||||||
const n = lastCandles.length;
|
const n = lastCandles.length;
|
||||||
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
|
if (!restoreVisibleLogicalRange(savedRange, n)) {
|
||||||
const minorTailUpdate = Math.abs(n - candleCountBefore) <= 5;
|
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
|
||||||
if (
|
if (!chartRangeUserLocked && curRange && !isVisibleRangeValidForCandles(curRange, n)) {
|
||||||
savedRange &&
|
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
|
||||||
isVisibleRangeValidForCandles(savedRange, n) &&
|
}
|
||||||
(minorTailUpdate || !wasViewingTail)
|
|
||||||
) {
|
|
||||||
chart.timeScale().setVisibleLogicalRange(savedRange);
|
|
||||||
} else if (!isVisibleRangeValidForCandles(curRange, n)) {
|
|
||||||
const tailRange = tailVisibleLogicalRange(n);
|
|
||||||
if (tailRange) chart.timeScale().setVisibleLogicalRange(tailRange);
|
|
||||||
}
|
}
|
||||||
scheduleRangeUiUpdate();
|
scheduleRangeUiUpdate();
|
||||||
if (posContext) {
|
if (posContext) {
|
||||||
@@ -2258,7 +2288,7 @@
|
|||||||
const r = tailVisibleLogicalRange(lastCandles.length);
|
const r = tailVisibleLogicalRange(lastCandles.length);
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
applyChartRightGap();
|
applyChartRightGap();
|
||||||
chart.timeScale().setVisibleLogicalRange(r);
|
restoreVisibleLogicalRange(r, lastCandles.length);
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
}
|
}
|
||||||
applyOnce();
|
applyOnce();
|
||||||
@@ -2482,6 +2512,11 @@
|
|||||||
chartDataLoading = true;
|
chartDataLoading = true;
|
||||||
if (resetView) {
|
if (resetView) {
|
||||||
chartViewEpoch += 1;
|
chartViewEpoch += 1;
|
||||||
|
chartRangeUserLocked = false;
|
||||||
|
if (chartRangeLockTimer) {
|
||||||
|
clearTimeout(chartRangeLockTimer);
|
||||||
|
chartRangeLockTimer = null;
|
||||||
|
}
|
||||||
resetChartHistoryState();
|
resetChartHistoryState();
|
||||||
lastViewKey = "";
|
lastViewKey = "";
|
||||||
clearChartSeriesData();
|
clearChartSeriesData();
|
||||||
|
|||||||
Reference in New Issue
Block a user