fix(web): 分型框竖边 canvas 绘制,换币对强制全量刷新

LWC 折线无法画真竖线;增量刷新时用坐标采样补刷竖边,避免与横边脱节。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-11 17:32:18 +08:00
co-authored by Cursor
parent 9cf625c413
commit 340676bfbd
4 changed files with 188 additions and 72 deletions
+9
View File
@@ -365,6 +365,15 @@ function updateTradingViewData() {
applyPosition('');
setTimeout(function () { applyPosition('@0'); }, 0);
setTimeout(function () { applyPosition('@50'); }, 50);
// 增量 setData 常不触发可见时间范围回调,但价格轴会变:补刷分型竖边
var bumpFxVert = function () {
if (typeof window._redrawFxBoxVerticalOverlay === 'function') {
window._redrawFxBoxVerticalOverlay();
}
};
bumpFxVert();
setTimeout(bumpFxVert, 0);
setTimeout(bumpFxVert, 50);
}
console.log('增量更新图表完成');
+158 -64
View File
@@ -58,10 +58,8 @@ function safeOverlayLineSetData(series, points) {
var v0 = Number(a.value);
var v1 = Number(b.value);
if (!isFinite(t0) || !isFinite(t1) || !isFinite(v0) || !isFinite(v1)) return;
if (t0 === t1) {
// 竖边:同 time 两点 LWC 不接受,跳过(横边仍保留)
return;
}
// 竖边不用折线(任意时间差都会斜),改走 canvas
if (t0 === t1) return;
if (t0 > t1) {
series.setData([{ time: t1, value: v1 }, { time: t0, value: v0 }]);
} else {
@@ -72,7 +70,147 @@ function safeOverlayLineSetData(series, points) {
}
}
function pushFxBoxVertical(time, lo, hi, color) {
if (!window._fxBoxVerticals) window._fxBoxVerticals = [];
var t = Math.floor(Number(time));
var a = Number(lo), b = Number(hi);
if (!isFinite(t) || !isFinite(a) || !isFinite(b) || a === b) return;
window._fxBoxVerticals.push({
time: t,
lo: Math.min(a, b),
hi: Math.max(a, b),
color: color || '#888'
});
}
function getMainPriceSeries() {
if (!window.tvWidget || !tvWidget.series) return null;
var s = tvWidget.series;
return s.candleSeries || s.klcSeries || s.barSeries || s.heikinSeries || s.renkoSeries ||
s.lineSeries || s.areaSeries || s.baselineSeries || null;
}
function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
if (!mainChart || !mainChartContainer) return;
if (typeof window._fxBoxOverlayCleanup === 'function') {
try { window._fxBoxOverlayCleanup(); } catch (e) {}
window._fxBoxOverlayCleanup = null;
}
var canvas = mainChartContainer.querySelector('.fx-box-vert-overlay');
if (!canvas) {
canvas = document.createElement('canvas');
canvas.className = 'fx-box-vert-overlay';
canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:6;';
if (getComputedStyle(mainChartContainer).position === 'static') {
mainChartContainer.style.position = 'relative';
}
mainChartContainer.appendChild(canvas);
}
var lastSig = '';
var watchRaf = null;
var cleaned = false;
var redrawPending = false;
var quant = function (v) {
if (v == null || !isFinite(Number(v))) return 'n';
return String(Math.round(Number(v)));
};
// LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放)
var sampleSig = function () {
var boxes = window._fxBoxVerticals || [];
var series = getMainPriceSeries();
if (!series || !boxes.length) return '0';
var ts = mainChart.timeScale();
var a = boxes[0];
var b = boxes[boxes.length - 1];
return [
boxes.length,
quant(ts.timeToCoordinate(a.time)),
quant(series.priceToCoordinate(a.hi)),
quant(series.priceToCoordinate(a.lo)),
quant(ts.timeToCoordinate(b.time)),
quant(series.priceToCoordinate(b.hi)),
quant(series.priceToCoordinate(b.lo))
].join('|');
};
var redraw = function () {
var boxes = window._fxBoxVerticals || [];
var series = getMainPriceSeries();
var rect = mainChartContainer.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
var ctx2 = canvas.getContext('2d');
if (!ctx2) return;
ctx2.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx2.clearRect(0, 0, rect.width, rect.height);
if (!series || !boxes.length) {
lastSig = sampleSig();
return;
}
var ts = mainChart.timeScale();
for (var i = 0; i < boxes.length; i++) {
var box = boxes[i];
var x = ts.timeToCoordinate(box.time);
var y1 = series.priceToCoordinate(box.hi);
var y2 = series.priceToCoordinate(box.lo);
if (x == null || y1 == null || y2 == null) continue;
ctx2.beginPath();
ctx2.strokeStyle = box.color;
ctx2.lineWidth = 1;
ctx2.setLineDash([4, 3]);
ctx2.moveTo(Math.round(x) + 0.5, y1);
ctx2.lineTo(Math.round(x) + 0.5, y2);
ctx2.stroke();
}
ctx2.setLineDash([]);
lastSig = sampleSig();
};
var scheduleRedraw = function () {
if (cleaned || redrawPending) return;
redrawPending = true;
requestAnimationFrame(function () {
redrawPending = false;
if (!cleaned) redraw();
});
};
var watch = function () {
if (cleaned) return;
watchRaf = requestAnimationFrame(watch);
var sig = sampleSig();
if (sig !== lastSig) scheduleRedraw();
};
try { mainChart.timeScale().subscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {}
try { mainChart.timeScale().subscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {}
var ro = null;
if (typeof ResizeObserver !== 'undefined') {
ro = new ResizeObserver(scheduleRedraw);
ro.observe(mainChartContainer);
}
window._redrawFxBoxVerticalOverlay = scheduleRedraw;
window._fxBoxOverlayCleanup = function () {
if (cleaned) return;
cleaned = true;
if (watchRaf != null) {
try { cancelAnimationFrame(watchRaf); } catch (e) {}
watchRaf = null;
}
window._redrawFxBoxVerticalOverlay = null;
try { mainChart.timeScale().unsubscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {}
try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {}
if (ro) try { ro.disconnect(); } catch (e) {}
try { if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); } catch (e) {}
};
if (!window._tvInitCleanups) window._tvInitCleanups = [];
window._tvInitCleanups.push(window._fxBoxOverlayCleanup);
scheduleRedraw();
setTimeout(scheduleRedraw, 50);
watchRaf = requestAnimationFrame(watch);
}
function chartTvRenderOverlays(ctx) {
window._fxBoxVerticals = [];
var symbol = ctx.symbol;
var timeframe = ctx.timeframe;
var symbolConfig = ctx.symbolConfig;
@@ -1726,28 +1864,11 @@ function chartTvRenderOverlays(ctx) {
});
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2, // 虚线
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = [];
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries);
}
}
@@ -1932,28 +2053,11 @@ function chartTvRenderOverlays(ctx) {
});
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = [];
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries);
}
}
@@ -2085,28 +2189,11 @@ function chartTvRenderOverlays(ctx) {
});
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
const leftSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
const rightSeries = mainChart.addLineSeries({
color: boxColor,
lineWidth: 1,
lineStyle: 2,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
});
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries);
}
}
} catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); }
@@ -2409,4 +2496,11 @@ function chartTvRenderOverlays(ctx) {
}
}
}
// KLC 分型框竖边:canvas 真竖线(LWC 折线做不到不斜)
try {
syncFxBoxVerticalOverlay(mainChart, mainChartContainer);
} catch (e) {
console.warn('分型竖边 overlay 失败:', e);
}
}
+17 -4
View File
@@ -110,8 +110,17 @@ function updateChart(options) {
const requestId = ++lastRequestId;
const chartsReady = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
const hasBaseline = !!(currentData && Array.isArray(currentData.kline_data) && currentData.kline_data.length);
// 自动刷新常态:只拉最近 2 根;fullAnalyze(约每 1 分钟)走全量 analyze 更新缠论
const useRecentTail = !!(options.fromAutoRefresh && !options.fullAnalyze && chartsReady && hasBaseline);
const baselineSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || '';
// 自动刷新常态:只拉最近 2 根;换币对后基线不一致则禁止尾部合并(否则会叠旧缠论)
// fullAnalyze(约每 1 分钟)走全量 analyze 更新缠论
const useRecentTail = !!(
options.fromAutoRefresh &&
!options.fullAnalyze &&
chartsReady &&
hasBaseline &&
baselineSymbol &&
baselineSymbol === symbol
);
if (useRecentTail) {
console.log('自动刷新 → /api/klines/recent limit=2');
@@ -187,24 +196,28 @@ function updateChart(options) {
}
// 保存当前数据
const prevSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || '';
if (currentData) {
// 覆盖前断开旧引用,帮助GC尽快回收
delete currentData.original_kline_data;
delete currentData.original_macd;
}
currentData = data;
window._lastChartSymbol = symbol;
window._lastFullAnalyzeAt = Date.now();
if (typeof renderWyckoffCycleSummary === 'function') {
renderWyckoffCycleSummary();
}
// 有图则增量;笔/段/中枢/结构区只在全量 init 绘制fullAnalyze 必须重建
// 有图则增量;笔/段/中枢/结构区只在全量 init 绘制
// 换币对 / 手动分析 / 结构区:必须全量重建,否则会残留旧币对叠层
const ready = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
const structureZonesOn = $('#showMainStructureZone').is(':checked');
const symbolChanged = !!(prevSymbol && prevSymbol !== symbol);
let wantIncremental = options.incremental !== undefined
? !!options.incremental
: (ready || !!options.fromAutoRefresh);
if (structureZonesOn || options.fullAnalyze) {
if (structureZonesOn || options.fullAnalyze || symbolChanged || options.incremental === false) {
wantIncremental = false;
}
refreshChart(data, { incremental: wantIncremental });