캔들차트, 이동평균선, 일목균형표, 볼린저밴드 보이기 숨기기 기능적용

This commit is contained in:
Macbook
2026-06-03 00:19:44 +09:00
parent 08fb442d4a
commit 76e5b12d19
15 changed files with 427 additions and 69 deletions
+120 -9
View File
@@ -59,6 +59,12 @@ import {
resolveIchimokuCloudColors,
resolveIchimokuDisplacements,
} from './ichimokuConfig';
import {
type ChartOverlayToggleKey,
type ChartOverlayVisibility,
DEFAULT_CHART_OVERLAY_VISIBILITY,
chartOverlayKeyForType,
} from './chartOverlayVisibility';
import type { PlotDef, HLineStyle } from './indicatorRegistry';
import { mapHistogramSeriesData, histogramBarColor } from './plotColorUtils';
import {
@@ -230,6 +236,10 @@ export class ChartManager {
/** 실시간 전략 체크 마커 */
private liveStrategyMarkers: MarkerData[] = [];
private compareSeriesMap = new Map<string, ISeriesApi<SeriesType>>();
/** 캔들 pane 오버레이 그룹 표시 (config.hidden 과 별도 — in-place 토글용) */
private _chartOverlayVisibility: ChartOverlayVisibility = {
...DEFAULT_CHART_OVERLAY_VISIBILITY,
};
// ── 인디케이터 실시간 갱신 스로틀 ─────────────────────────────────────────
private _indUpdateTimer: ReturnType<typeof setTimeout> | null = null;
@@ -668,7 +678,7 @@ export class ChartManager {
config = enrichIndicatorConfig(config);
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
const dataGenAtStart = this._dataGeneration;
const indicatorHidden = config.hidden === true;
const indicatorHidden = !this._isIndicatorEffectivelyVisible(config);
const def = getIndicatorDef(config.type);
const seriesList: ISeriesApi<SeriesType>[] = [];
const seriesMeta: IndicatorEntry['seriesMeta'] = [];
@@ -855,6 +865,7 @@ export class ChartManager {
if (configs.length > 0) {
this._finalizeIndicatorPaneLayout();
this._refreshAllIchimokuClouds();
this.syncChartOverlayVisibility();
}
}
@@ -930,6 +941,7 @@ export class ChartManager {
this._finalizeIndicatorPaneLayout();
this._refreshAllIchimokuClouds();
this.syncChartOverlayVisibility();
}
removeIndicator(id: string): void {
@@ -1786,7 +1798,7 @@ export class ChartManager {
plugin.setColors(colors.bullishColor, colors.bearishColor);
plugin.setVisibility(effectiveBullish, effectiveBearish);
if (config.hidden === true) {
if (!this._isIndicatorEffectivelyVisible(config)) {
plugin.setCloudData([]);
return;
}
@@ -2343,6 +2355,89 @@ export class ChartManager {
return result;
}
getChartOverlayVisibility(): ChartOverlayVisibility {
return { ...this._chartOverlayVisibility };
}
setChartOverlayVisibility(visibility: ChartOverlayVisibility): void {
this._chartOverlayVisibility = { ...visibility };
this.syncChartOverlayVisibility();
}
setChartOverlayGroupVisible(key: ChartOverlayToggleKey, visible: boolean): void {
this._chartOverlayVisibility = {
...this._chartOverlayVisibility,
[key]: visible,
};
this.syncChartOverlayVisibility();
}
/** 사용자 hidden + 세션 오버레이 토글 — entry.config.hidden 은 사용자 의도만 반영 */
private _isIndicatorEffectivelyVisible(config: IndicatorConfig): boolean {
const overlayKey = chartOverlayKeyForType(config.type);
if (overlayKey && !this._chartOverlayVisibility[overlayKey]) return false;
return config.hidden !== true;
}
/** 오버레이·plotVisibility 반영해 시리즈 visible 만 갱신 (플롯 정의 없어도 전체 시리즈 대상) */
private _applySeriesVisibilityToEntry(entry: IndicatorEntry): void {
const config = entry.config;
if (!config) return;
const indicatorVisible = this._isIndicatorEffectivelyVisible(config);
entry.seriesList.forEach((series, i) => {
const plotId = entry.seriesMeta[i]?.plotId;
const visible = indicatorVisible
&& (plotId == null || config.plotVisibility?.[plotId] !== false);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
series.applyOptions({ visible } as any);
} catch { /* ok */ }
});
}
/** 오버레이 그룹·캔들 visible — 시리즈 재생성 없이 applyOptions 만 사용 */
syncChartOverlayVisibility(): void {
if (this.mainSeries) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.mainSeries as ISeriesApi<any>).applyOptions({
visible: this._chartOverlayVisibility.candle,
});
} catch { /* ok */ }
}
for (const entry of this.indicators.values()) {
const config = entry.config;
if (!config) continue;
const key = chartOverlayKeyForType(config.type);
if (!key) continue;
this._applySeriesVisibilityToEntry(entry);
if (entry.type === 'BollingerBands') {
detachBbBandFill(entry);
if (this._isIndicatorEffectivelyVisible(config)) {
attachBbBandFill(entry, config);
entry.bbFillPrimitive?.requestRefresh();
}
}
if (entry.type === 'IchimokuCloud' && entry.cloudPlugin) {
if (!this._isIndicatorEffectivelyVisible(config)) {
entry.cloudPlugin.setCloudData([]);
} else {
void calculateIndicator(
'IchimokuCloud',
this.rawBars,
config.params ?? {},
).then(({ plots }) => {
this._applyIchimokuCloudPlugin(entry.cloudPlugin!, plots, config);
}).catch(() => { /* ignore */ });
}
}
}
}
/** 인디케이터 전체 가시성 토글 (시리즈 visible 옵션 변경) */
toggleIndicatorVisible(id: string, visible: boolean): void {
const entry = this.indicators.get(id);
@@ -2451,19 +2546,25 @@ export class ChartManager {
config = enrichIndicatorConfig(config);
const entry = this.indicators.get(config.id);
if (!entry) return;
entry.config = config;
const prevUserHidden = entry.config?.hidden === true;
const def = getIndicatorDef(config.type);
const plotDefs = config.plots ?? def?.plots ?? [];
const plotById = Object.fromEntries(plotDefs.map((p: PlotDef) => [p.id, p]));
const indicatorVisible = this._isIndicatorEffectivelyVisible(config);
entry.seriesList.forEach((series, i) => {
let plot: PlotDef | undefined;
const pid = entry.seriesMeta[i]?.plotId;
plot = (pid ? plotById[pid] : undefined) ?? plotDefs[i];
if (!plot) return;
const indicatorVisible = config.hidden !== true;
const visible = indicatorVisible && config.plotVisibility?.[plot.id] !== false;
const visible = indicatorVisible
&& (pid == null || config.plotVisibility?.[pid] !== false);
if (!plot) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
series.applyOptions({ visible } as any);
return;
}
const paneIdx = entry.paneIndex ?? 0;
const indicatorPriceFormat = priceFormatForIndicatorPane(paneIdx);
@@ -2516,7 +2617,7 @@ export class ChartManager {
entry.fillPrimitive = undefined;
}
if (config.hidden !== true) {
if (indicatorVisible) {
for (const hl of hlines) {
if (hl.visible === false) continue;
const pl = firstSeries.createPriceLine({
@@ -2544,7 +2645,7 @@ export class ChartManager {
if (entry.type === 'BollingerBands') {
detachBbBandFill(entry);
if (config.hidden !== true) {
if (indicatorVisible) {
attachBbBandFill(entry, config);
entry.bbFillPrimitive?.requestRefresh();
}
@@ -2560,7 +2661,17 @@ export class ChartManager {
}).catch(() => { /* ignore */ });
}
entry.config = config;
const overlayKey = chartOverlayKeyForType(config.type);
const overlayOn = overlayKey == null || this._chartOverlayVisibility[overlayKey];
const toStore = { ...config };
if (overlayKey) {
if (overlayOn && config.hidden !== true) {
delete toStore.hidden;
} else if (!overlayOn && toStore.hidden === true && !prevUserHidden && config.hidden !== true) {
delete toStore.hidden;
}
}
entry.config = toStore;
if (entry.seriesMeta.some(m => m.isHistogram)) {
void this._repaintHistogramSeries(config.id, config);
}