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:
@@ -652,6 +652,7 @@ def api_chart_ohlcv(
|
|||||||
symbol: str = "",
|
symbol: str = "",
|
||||||
timeframe: str = "1d",
|
timeframe: str = "1d",
|
||||||
refresh: str = "",
|
refresh: str = "",
|
||||||
|
tail: str = "",
|
||||||
limit: int = 0,
|
limit: int = 0,
|
||||||
before_ms: str = "",
|
before_ms: str = "",
|
||||||
):
|
):
|
||||||
@@ -665,6 +666,7 @@ def api_chart_ohlcv(
|
|||||||
raise HTTPException(status_code=400, detail="请输入币种")
|
raise HTTPException(status_code=400, detail="请输入币种")
|
||||||
ex_key = str(ex.get("key") or "").strip().lower()
|
ex_key = str(ex.get("key") or "").strip().lower()
|
||||||
force = (refresh or "").strip().lower() in ("1", "true", "yes", "on")
|
force = (refresh or "").strip().lower() in ("1", "true", "yes", "on")
|
||||||
|
tail_refresh = (tail or "").strip().lower() in ("1", "true", "yes", "on")
|
||||||
lim = int(limit) if int(limit or 0) > 0 else None
|
lim = int(limit) if int(limit or 0) > 0 else None
|
||||||
bms_raw = (before_ms or "").strip()
|
bms_raw = (before_ms or "").strip()
|
||||||
bms = None
|
bms = None
|
||||||
@@ -690,6 +692,7 @@ def api_chart_ohlcv(
|
|||||||
timeframe,
|
timeframe,
|
||||||
remote_fetch,
|
remote_fetch,
|
||||||
force_refresh=force,
|
force_refresh=force,
|
||||||
|
tail_refresh=tail_refresh,
|
||||||
limit=lim,
|
limit=lim,
|
||||||
before_ms=bms,
|
before_ms=bms,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
let chartRangeUserLocked = false;
|
let chartRangeUserLocked = false;
|
||||||
let chartRangeLockTimer = null;
|
let chartRangeLockTimer = null;
|
||||||
let suppressRangeUserLock = false;
|
let suppressRangeUserLock = false;
|
||||||
let pendingViewportEpoch = -1;
|
const CHART_TAIL_REFRESH_LIMIT = 30;
|
||||||
let priceTagTimer = null;
|
let priceTagTimer = null;
|
||||||
let tfDigitBuf = "";
|
let tfDigitBuf = "";
|
||||||
let tfDigitTimer = null;
|
let tfDigitTimer = null;
|
||||||
@@ -2033,7 +2033,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markChartRangeUserAdjusted() {
|
function markChartRangeUserAdjusted() {
|
||||||
pendingViewportEpoch = -1;
|
|
||||||
chartRangeUserLocked = true;
|
chartRangeUserLocked = true;
|
||||||
if (chartRangeLockTimer) clearTimeout(chartRangeLockTimer);
|
if (chartRangeLockTimer) clearTimeout(chartRangeLockTimer);
|
||||||
chartRangeLockTimer = setTimeout(function () {
|
chartRangeLockTimer = setTimeout(function () {
|
||||||
@@ -2168,6 +2167,7 @@
|
|||||||
});
|
});
|
||||||
if (params.before_ms) qs.set("before_ms", String(params.before_ms));
|
if (params.before_ms) qs.set("before_ms", String(params.before_ms));
|
||||||
if (params.refresh) qs.set("refresh", "1");
|
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 r = await fetch("/api/chart/ohlcv?" + qs.toString(), { credentials: "same-origin" });
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -2197,8 +2197,10 @@
|
|||||||
if (data.exhausted) exhaustedLeft = true;
|
if (data.exhausted) exhaustedLeft = true;
|
||||||
const incoming = alignCandlesToTick(data.candles || []);
|
const incoming = alignCandlesToTick(data.candles || []);
|
||||||
if (!incoming.length) return;
|
if (!incoming.length) return;
|
||||||
const shift = incoming.length;
|
const prevLen = lastCandles.length;
|
||||||
applyCandlesToChart(mergeCandles(lastCandles, incoming, { prepend: true }), shift);
|
const merged = mergeCandles(lastCandles, incoming, { prepend: true });
|
||||||
|
const shift = merged.length - prevLen;
|
||||||
|
applyCandlesToChart(merged, shift);
|
||||||
if (elStatus && !elStatus.classList.contains("err")) {
|
if (elStatus && !elStatus.classList.contains("err")) {
|
||||||
elStatus.textContent =
|
elStatus.textContent =
|
||||||
"已加载 " +
|
"已加载 " +
|
||||||
@@ -2230,12 +2232,15 @@
|
|||||||
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,
|
||||||
symbol: sym,
|
symbol: sym,
|
||||||
timeframe: tf,
|
timeframe: tf,
|
||||||
limit: chartChunkLimit(tf),
|
limit: CHART_TAIL_REFRESH_LIMIT,
|
||||||
|
tail: true,
|
||||||
});
|
});
|
||||||
if (myToken !== loadToken) return;
|
if (myToken !== loadToken) return;
|
||||||
if (vKey !== lastViewKey) return;
|
if (vKey !== lastViewKey) return;
|
||||||
@@ -2250,25 +2255,28 @@
|
|||||||
applyChartPriceFormat();
|
applyChartPriceFormat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const keepViewport =
|
const shouldPreserve =
|
||||||
pendingViewportEpoch !== chartViewEpoch &&
|
|
||||||
savedRange &&
|
savedRange &&
|
||||||
isVisibleRangeValidForCandles(savedRange, candleCountBefore);
|
isVisibleRangeValidForCandles(savedRange, candleCountBefore) &&
|
||||||
|
(chartRangeUserLocked || !wasViewingTail);
|
||||||
applyCandlesToChart(
|
applyCandlesToChart(
|
||||||
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
|
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
|
||||||
0,
|
0,
|
||||||
{
|
{
|
||||||
preserveRange: !!keepViewport,
|
preserveRange: !!shouldPreserve,
|
||||||
skipAutoScale: chartRangeUserLocked,
|
skipAutoScale: chartRangeUserLocked,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (epochAtStart !== chartViewEpoch) return;
|
if (epochAtStart !== chartViewEpoch) return;
|
||||||
const n = lastCandles.length;
|
const n = lastCandles.length;
|
||||||
if (pendingViewportEpoch === chartViewEpoch) {
|
const minorTailUpdate = Math.abs(n - candleCountBefore) <= CHART_TAIL_REFRESH_LIMIT + 5;
|
||||||
applyDefaultVisibleRange();
|
if (wasViewingTail && !chartRangeUserLocked) {
|
||||||
} else if (savedRange && isVisibleRangeValidForCandles(savedRange, n)) {
|
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
|
||||||
restoreVisibleLogicalRange(savedRange, n);
|
} else if (shouldPreserve && minorTailUpdate) {
|
||||||
} else if (!chartRangeUserLocked) {
|
if (!restoreVisibleLogicalRange(savedRange, n) && !chartRangeUserLocked) {
|
||||||
|
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
|
||||||
|
}
|
||||||
|
} else if (!restoreVisibleLogicalRange(savedRange, n) && !chartRangeUserLocked) {
|
||||||
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
|
const curRange = chart && chart.timeScale().getVisibleLogicalRange();
|
||||||
if (!curRange || !isVisibleRangeValidForCandles(curRange, n)) {
|
if (!curRange || !isVisibleRangeValidForCandles(curRange, n)) {
|
||||||
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
|
restoreVisibleLogicalRange(tailVisibleLogicalRange(n), n);
|
||||||
@@ -2525,7 +2533,6 @@
|
|||||||
chartDataLoading = true;
|
chartDataLoading = true;
|
||||||
if (resetView) {
|
if (resetView) {
|
||||||
chartViewEpoch += 1;
|
chartViewEpoch += 1;
|
||||||
pendingViewportEpoch = chartViewEpoch;
|
|
||||||
chartRangeUserLocked = false;
|
chartRangeUserLocked = false;
|
||||||
if (chartRangeLockTimer) {
|
if (chartRangeLockTimer) {
|
||||||
clearTimeout(chartRangeLockTimer);
|
clearTimeout(chartRangeLockTimer);
|
||||||
|
|||||||
Reference in New Issue
Block a user