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:
@@ -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();
|
||||||
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() {
|
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);
|
||||||
|
lastViewKey = vKey;
|
||||||
if (resetView) {
|
if (resetView) {
|
||||||
lastViewKey = vKey;
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user