fix(web): 分型框竖边 canvas 绘制,换币对强制全量刷新
LWC 折线无法画真竖线;增量刷新时用坐标采样补刷竖边,避免与横边脱节。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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('增量更新图表完成');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user