Patch tail candles without resetting chart viewport.

When auto-follow is off, refresh only updates the latest bars via series.update instead of setData so zoom and pan stay fixed during background polls.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-08 08:39:56 +08:00
parent 5af0cbf286
commit b34aefbcc4
2 changed files with 68 additions and 13 deletions
+67 -12
View File
@@ -1868,6 +1868,16 @@
});
}
function buildVolumeBar(candle) {
const p = chartThemePalette();
const up = Number(candle.close) >= Number(candle.open);
return {
time: candle.time,
value: Number(candle.volume) || 0,
color: up ? p.volUp : p.volDown,
};
}
function ensureChart() {
if (chart && candleSeries && volumeSeries) return true;
if (!window.LightweightCharts) {
@@ -2141,6 +2151,45 @@
return merged;
}
/** 尾部静默刷新:仅 update 变更 K 线,不 setData,避免视口跳动 */
function applyTailCandlePatch(incoming) {
if (!candleSeries || !volumeSeries || !incoming || !incoming.length) return false;
const aligned = alignCandlesToTick(incoming);
const prevLen = lastCandles.length;
const oldestTime = prevLen ? lastCandles[0].time : null;
const merged = mergeCandles(lastCandles, aligned, { prepend: false });
if (
prevLen > 0 &&
merged.length > 0 &&
merged[0].time !== oldestTime &&
merged.length <= prevLen
) {
return false;
}
aligned.forEach(function (bar) {
candleSeries.update(bar);
volumeSeries.update(buildVolumeBar(bar));
});
if (merged.length > prevLen) {
for (let i = prevLen; i < merged.length; i++) {
const bar = merged[i];
candleSeries.update(bar);
volumeSeries.update(buildVolumeBar(bar));
}
}
lastCandles = merged;
indexCandles(lastCandles);
readIndicatorState();
if (indicatorState.ema || indicatorState.macd || indicatorState.rsi) {
try {
updateIndicators();
} catch (indErr) {}
}
updateVisibleRangeMarkers();
showLatestOhlcv();
return true;
}
function applyCandlesToChart(candles, rangeShift, opts) {
opts = opts || {};
let savedRange = null;
@@ -2151,7 +2200,9 @@
indexCandles(lastCandles);
candleSeries.setData(lastCandles);
volumeSeries.setData(buildVolumeData(lastCandles));
applyChartRightGap();
if (!opts.skipRightGap) {
applyChartRightGap();
}
if (rangeShift && chart) {
const range = chart.timeScale().getVisibleLogicalRange();
if (range) {
@@ -2270,21 +2321,25 @@
applyChartPriceFormat();
}
}
applyCandlesToChart(
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
0,
{
const incoming = alignCandlesToTick(data.candles);
if (!autoFollow && applyTailCandlePatch(incoming)) {
/* 手动模式:增量更新,不触碰时间轴 */
} else {
const merged = mergeCandles(lastCandles, incoming, { prepend: false });
applyCandlesToChart(merged, 0, {
preserveRange: false,
skipAutoScale: !autoFollow,
skipRightGap: !autoFollow,
});
if (epochAtStart !== chartViewEpoch) return;
const n = lastCandles.length;
if (autoFollow) {
applyDefaultVisibleRange();
} else if (savedRange) {
applyPreservedVisibleRange(savedRange, n);
}
);
if (epochAtStart !== chartViewEpoch) return;
const n = lastCandles.length;
if (autoFollow) {
applyDefaultVisibleRange();
} else if (savedRange) {
applyPreservedVisibleRange(savedRange, n);
}
if (epochAtStart !== chartViewEpoch) return;
scheduleRangeUiUpdate();
if (posContext) {
updateLivePosPnl();
+1 -1
View File
@@ -349,7 +349,7 @@
<div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260608-auto-viewport"></script>
<script src="/assets/chart.js?v=20260608-tail-patch"></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/app.js?v=20260607-hub-archive-v1"></script>