실시간 차트 갱신시 화면떨림 문제 해결

This commit is contained in:
Macbook
2026-05-27 14:54:50 +09:00
parent f7aac535d8
commit 7a0af36b9b
10 changed files with 517 additions and 130 deletions
+88 -26
View File
@@ -532,7 +532,10 @@ export class ChartManager {
}
// ─── Indicators ─────────────────────────────────────────────────────────
async addIndicator(config: IndicatorConfig): Promise<void> {
async addIndicator(
config: IndicatorConfig,
options?: { skipLayout?: boolean },
): Promise<void> {
config = enrichIndicatorConfig(config);
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
const dataGenAtStart = this._dataGeneration;
@@ -664,12 +667,50 @@ export class ChartManager {
} catch (e) {
console.error(`[Indicator] ${config.type} error:`, e);
} finally {
if (this._activeIndicatorPaneIndices().size > 0) {
if (
!options?.skipLayout
&& this._activeIndicatorPaneIndices().size > 0
) {
this.resetPaneHeights(this._lastLayoutAvailableHeight);
}
}
}
/** 여러 지표 추가 후 pane 레이아웃을 한 번만 갱신 */
async addIndicatorsBatch(configs: IndicatorConfig[]): Promise<void> {
for (const config of configs) {
await this.addIndicator(config, { skipLayout: true });
}
if (configs.length > 0 && this._activeIndicatorPaneIndices().size > 0) {
this.resetPaneHeights(this._lastLayoutAvailableHeight);
this._notifyPaneLayout();
}
}
private _detachIndicatorEntry(id: string): boolean {
const entry = this.indicators.get(id);
if (!entry) return false;
if (entry.seriesList.length > 0) {
for (const pl of entry.hlineRefs) {
try { entry.seriesList[0].removePriceLine(pl); } catch { /* ok */ }
}
if (entry.fillPrimitive) {
try { entry.seriesList[0].detachPrimitive(entry.fillPrimitive); } catch { /* ok */ }
}
}
if (entry.cloudPlugin) {
for (const s of entry.seriesList) {
try { s.detachPrimitive(entry.cloudPlugin); } catch { /* ok */ }
}
}
detachBbBandFill(entry);
for (const s of entry.seriesList) { try { this.chart.removeSeries(s); } catch { /* ok */ } }
this.patternMarkers = this.patternMarkers.filter(p => p.id !== id);
this._reapplyAllPatternMarkers();
this.indicators.delete(id);
return true;
}
/**
* 순서 변경 전용 재로드 — 메인 차트·볼륨 시리즈는 유지하고 보조지표만 제거 후 재추가.
* reloadAll 과 달리 setData()를 호출하지 않으므로 메인 캔들 차트가 깜빡이지 않는다.
@@ -708,31 +749,32 @@ export class ChartManager {
}
removeIndicator(id: string): void {
const entry = this.indicators.get(id);
if (!entry) return;
// hlines price line 및 fill primitive 정리
if (entry.seriesList.length > 0) {
for (const pl of entry.hlineRefs) {
try { entry.seriesList[0].removePriceLine(pl); } catch { /* ok */ }
}
if (entry.fillPrimitive) {
try { entry.seriesList[0].detachPrimitive(entry.fillPrimitive); } catch { /* ok */ }
}
this.removeIndicators([id]);
}
/** 여러 지표 제거 후 pane 레이아웃을 한 번만 갱신 (줌·스크롤 유지에 유리) */
removeIndicators(ids: string[]): void {
let any = false;
for (const id of ids) {
if (this._detachIndicatorEntry(id)) any = true;
}
if (entry.cloudPlugin) {
for (const s of entry.seriesList) {
try { s.detachPrimitive(entry.cloudPlugin); } catch { /* ok */ }
}
}
for (const s of entry.seriesList) { try { this.chart.removeSeries(s); } catch { /* ok */ } }
this.patternMarkers = this.patternMarkers.filter(p => p.id !== id);
this._reapplyAllPatternMarkers();
this.indicators.delete(id);
this._removeExtraSubPanes();
if (!any) return;
this._trimTrailingEmptySubPanes();
this.resetPaneHeights(this._lastLayoutAvailableHeight);
this._notifyPaneLayout();
}
/** 파라미터 변경된 지표만 제거 후 재추가 (전체 보조지표 재로드 회피) */
async refreshIndicators(configs: IndicatorConfig[]): Promise<void> {
if (configs.length === 0) return;
let any = false;
for (const c of configs) {
if (this._detachIndicatorEntry(c.id)) any = true;
}
if (any) this._trimTrailingEmptySubPanes();
await this.addIndicatorsBatch(configs);
}
/** 메인 시리즈 마커 플러그인 해제 (setData 등 시리즈 교체 전 호출) */
private _disposeMainMarkersPlugin(): void {
if (!this.mainMarkersPlugin) return;
@@ -2254,7 +2296,7 @@ export class ChartManager {
return indices;
}
/** pane 2+ 빈 sub-pane 제거 — 제거·재로드 후 고아 pane 이 높이를 나눠 가져가는 것 방지 */
/** pane 2+ 빈 sub-pane 제거 — reloadIndicatorsOnly 전용 (모든 지표 시리즈 제거 후) */
private _removeExtraSubPanes(): void {
for (;;) {
const panes = this.chart.panes();
@@ -2268,6 +2310,21 @@ export class ChartManager {
}
}
/** 활성 지표가 없는 맨 끝 sub-pane 만 제거 — 단일 지표 제거 시 나머지 pane 유지 */
private _trimTrailingEmptySubPanes(): void {
for (;;) {
const panes = this.chart.panes();
if (panes.length <= 2) return;
const last = panes.length - 1;
if (this._activeIndicatorPaneIndices().has(last)) return;
try {
this.chart.removePane(last);
} catch {
return;
}
}
}
/** chart-container 기준 Y(px) → pane index (0=캔들, 1=거래량, 2+=보조지표) */
getPaneIndexAtChartY(chartY: number): number {
const layouts = this.getPaneLayouts();
@@ -2578,12 +2635,17 @@ export class ChartManager {
/** 실제 시리즈·구름이 붙은 보조지표 수 (부분 로드·중단 감지용) */
getLoadedIndicatorCount(): number {
let n = 0;
return this.getLoadedIndicatorIds().size;
}
/** 차트에 실제로 그려진 보조지표 id (부분 로드·증분 복구용) */
getLoadedIndicatorIds(): Set<string> {
const ids = new Set<string>();
for (const entry of this.indicators.values()) {
if (entry.config?.hidden === true) continue;
if (entry.cloudPlugin || entry.seriesList.length > 0) n += 1;
if (entry.cloudPlugin || entry.seriesList.length > 0) ids.add(entry.id);
}
return n;
return ids;
}
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */