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:
@@ -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() {
|
function ensureChart() {
|
||||||
if (chart && candleSeries && volumeSeries) return true;
|
if (chart && candleSeries && volumeSeries) return true;
|
||||||
if (!window.LightweightCharts) {
|
if (!window.LightweightCharts) {
|
||||||
@@ -2141,6 +2151,45 @@
|
|||||||
return merged;
|
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) {
|
function applyCandlesToChart(candles, rangeShift, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
let savedRange = null;
|
let savedRange = null;
|
||||||
@@ -2151,7 +2200,9 @@
|
|||||||
indexCandles(lastCandles);
|
indexCandles(lastCandles);
|
||||||
candleSeries.setData(lastCandles);
|
candleSeries.setData(lastCandles);
|
||||||
volumeSeries.setData(buildVolumeData(lastCandles));
|
volumeSeries.setData(buildVolumeData(lastCandles));
|
||||||
applyChartRightGap();
|
if (!opts.skipRightGap) {
|
||||||
|
applyChartRightGap();
|
||||||
|
}
|
||||||
if (rangeShift && chart) {
|
if (rangeShift && chart) {
|
||||||
const range = chart.timeScale().getVisibleLogicalRange();
|
const range = chart.timeScale().getVisibleLogicalRange();
|
||||||
if (range) {
|
if (range) {
|
||||||
@@ -2270,21 +2321,25 @@
|
|||||||
applyChartPriceFormat();
|
applyChartPriceFormat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyCandlesToChart(
|
const incoming = alignCandlesToTick(data.candles);
|
||||||
mergeCandles(lastCandles, alignCandlesToTick(data.candles), { prepend: false }),
|
if (!autoFollow && applyTailCandlePatch(incoming)) {
|
||||||
0,
|
/* 手动模式:增量更新,不触碰时间轴 */
|
||||||
{
|
} else {
|
||||||
|
const merged = mergeCandles(lastCandles, incoming, { prepend: false });
|
||||||
|
applyCandlesToChart(merged, 0, {
|
||||||
preserveRange: false,
|
preserveRange: false,
|
||||||
skipAutoScale: !autoFollow,
|
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();
|
scheduleRangeUiUpdate();
|
||||||
if (posContext) {
|
if (posContext) {
|
||||||
updateLivePosPnl();
|
updateLivePosPnl();
|
||||||
|
|||||||
@@ -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=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/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>
|
||||||
|
|||||||
Reference in New Issue
Block a user