실시간 차트 보조지표명 겹침오류 수정

This commit is contained in:
Macbook
2026-05-29 01:59:55 +09:00
parent 3db7e85e2c
commit db1c22d217
4 changed files with 302 additions and 142 deletions
+173 -3
View File
@@ -643,13 +643,14 @@ export class ChartManager {
} else {
const isPlotVisible = !indicatorHidden && config.plotVisibility?.[plotDef.id] !== false;
const showPriceLabel = this.shouldShowSeriesPriceLabel(config);
const showSeriesTitle = pane < 2 && showPriceLabel;
series = this.chart.addSeries(LineSeries, {
color: plotDef.color,
lineWidth: (plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4,
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
priceLineVisible: false,
lastValueVisible: showPriceLabel,
title: this.seriesTitleForPlot(config, plotDef),
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
visible: isPlotVisible,
}, pane);
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
@@ -732,6 +733,7 @@ export class ChartManager {
}
if (configs.length > 0 && this._activeIndicatorPaneIndices().size > 0) {
this._removeOrphanSubPanes();
this._resyncIndicatorPaneIndices();
this.resetPaneHeights(this._lastLayoutAvailableHeight);
this._notifyPaneLayout();
}
@@ -828,7 +830,10 @@ export class ChartManager {
for (const c of configs) {
if (this._detachIndicatorEntry(c.id)) any = true;
}
if (any) this._trimTrailingEmptySubPanes();
if (any) {
this._removeOrphanSubPanes();
this._trimTrailingEmptySubPanes();
}
if (this._indicatorLoadStale(loadGen)) return;
for (const config of configs) {
@@ -838,12 +843,173 @@ export class ChartManager {
if (this._indicatorLoadStale(loadGen)) return;
this._removeOrphanSubPanes();
this._resyncIndicatorPaneIndices();
if (this._activeIndicatorPaneIndices().size > 0) {
this.resetPaneHeights(this._lastLayoutAvailableHeight);
this._notifyPaneLayout();
}
}
/**
* 파라미터만 변경된 지표 — pane·시리즈를 유지하고 setData 로 갱신.
* (detach/re-add 시 orphan pane·레이블 좌표 어긋남·이중 표시 방지)
*/
async refreshIndicatorParamsInPlace(configs: IndicatorConfig[]): Promise<void> {
if (configs.length === 0) return;
this.cancelPendingIndicatorUpdates();
const loadGen = ++this._dataGeneration;
const fallback: IndicatorConfig[] = [];
for (const raw of configs) {
const config = enrichIndicatorConfig(raw);
const def = getIndicatorDef(config.type);
if (def?.returnsMarkers) {
fallback.push(config);
continue;
}
const entry = this.indicators.get(config.id);
if (!entry || entry.seriesList.length === 0 || entry.type !== config.type) {
fallback.push(config);
continue;
}
try {
const result = await calculateIndicator(
config.type, this.rawBars.slice(), config.params ?? {},
);
if (this._indicatorLoadStale(loadGen)) return;
if (!this._canApplyPlotsInPlace(entry, result.plots)) {
fallback.push(config);
continue;
}
entry.config = config;
if (config.type === 'IchimokuCloud') {
this._setIchimokuPlotsFullData(entry, result.plots, config);
if (entry.cloudPlugin) {
this._applyIchimokuCloudPlugin(entry.cloudPlugin, result.plots, config);
}
} else {
this._setIndicatorPlotsFullData(entry, result.plots, config);
}
this.applyIndicatorStyle(config);
} catch (e) {
console.error(`[Indicator in-place] ${config.type}:`, e);
fallback.push(config);
}
}
if (this._indicatorLoadStale(loadGen)) return;
if (fallback.length > 0) {
await this.refreshIndicators(fallback);
return;
}
this._resyncIndicatorPaneIndices();
this._notifyPaneLayout();
}
private _canApplyPlotsInPlace(
entry: IndicatorEntry,
plots: Record<string, PlotData>,
): boolean {
if (entry.seriesMeta.length === 0) return false;
for (const meta of entry.seriesMeta) {
const data = plots[meta.plotId] as PlotData | undefined;
if (!data?.length) return false;
const filtered = data.filter(p => p.value !== null && !isNaN(p.value));
if (filtered.length === 0) return false;
}
return true;
}
/** 기존 시리즈에 전체 plot 데이터 재적용 (pane 유지) */
private _setIndicatorPlotsFullData(
entry: IndicatorEntry,
plots: Record<string, PlotData>,
config: IndicatorConfig,
): void {
const plotDefs = config.plots ?? getIndicatorDef(config.type)?.plots ?? [];
const plotById = Object.fromEntries(plotDefs.map((p: PlotDef) => [p.id, p]));
for (let i = 0; i < entry.seriesList.length; i++) {
const meta = entry.seriesMeta[i];
if (!meta) continue;
const plot = plotById[meta.plotId];
const plotData = plots[meta.plotId] as PlotData | undefined;
if (!plot || !plotData?.length) continue;
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
if (filtered.length === 0) continue;
const series = entry.seriesList[i];
if (meta.isHistogram) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(series as ISeriesApi<any>).setData(mapHistogramSeriesData(filtered, plot));
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(series as ISeriesApi<any>).setData(
filtered.map(p => ({ time: p.time as Time, value: p.value })),
);
}
}
if (config.type === 'BollingerBands') {
detachBbBandFill(entry);
attachBbBandFill(entry, config);
entry.bbFillPrimitive?.requestRefresh();
}
}
private _setIchimokuPlotsFullData(
entry: IndicatorEntry,
plots: Record<string, PlotData>,
config: IndicatorConfig,
): void {
const { senkou: displacement } = resolveIchimokuDisplacements(config.params);
const laggingPeriod = (config.params?.laggingSpan2Periods as number) ?? 52;
const convPlot = (plots['plot0'] as PlotData) ?? [];
const basePlot = (plots['plot1'] as PlotData) ?? [];
for (let i = 0; i < entry.seriesList.length; i++) {
const meta = entry.seriesMeta[i];
if (!meta) continue;
const plotData = plots[meta.plotId] as PlotData | undefined;
if (!plotData?.length) continue;
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
if (filtered.length === 0) continue;
let seriesData: Array<{ time: Time; value: number }> =
filtered.map(p => ({ time: p.time as Time, value: p.value }));
if (meta.plotId === 'plot3') {
seriesData = seriesData.concat(
this._ichimokuFutureSpanA(convPlot, basePlot, displacement),
);
} else if (meta.plotId === 'plot4') {
seriesData = seriesData.concat(
this._ichimokuFutureSpanB(displacement, laggingPeriod),
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(entry.seriesList[i] as ISeriesApi<any>).setData(seriesData);
}
}
/** LWC pane index ↔ entry.paneIndex 동기화 (removePane 후 어긋남 보정) */
private _resyncIndicatorPaneIndices(): void {
for (const entry of this.indicators.values()) {
const first = entry.seriesList[0];
if (!first) continue;
try {
const pi = first.getPane().paneIndex();
if (pi >= 0) entry.paneIndex = pi;
} catch { /* ok */ }
}
}
/** 메인 시리즈 마커 플러그인 해제 (setData 등 시리즈 교체 전 호출) */
private _disposeMainMarkersPlugin(): void {
if (!this.mainMarkersPlugin) return;
@@ -1993,6 +2159,7 @@ export class ChartManager {
config = enrichIndicatorConfig(config);
const entry = this.indicators.get(config.id);
if (!entry) return;
entry.config = config;
const def = getIndicatorDef(config.type);
const plotDefs = config.plots ?? def?.plots ?? [];
@@ -2016,8 +2183,11 @@ export class ChartManager {
plotAllows = entry.seriesMeta[i]?.plotId !== 'plot2';
}
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, plotAllows);
const paneIdx = entry.paneIndex ?? 0;
opts['lastValueVisible'] = showPriceLabel;
opts['title'] = this.seriesTitleForPlot(config, plot, plotAllows);
opts['title'] = paneIdx < 2 && showPriceLabel
? this.seriesTitleForPlot(config, plot, plotAllows)
: '';
} else {
opts['color'] = plot.color ?? '#aaa';
}