실시간 차트 갱신시 화면떨림 문제 해결
This commit is contained in:
@@ -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 개수 반환 (데이터 로드 여부 확인용) */
|
||||
|
||||
@@ -29,8 +29,8 @@ export const ICHIMOKU_LINE_ROWS: IchimokuLineRowDef[] = [
|
||||
{ plotIndex: 0, plotId: 'plot0', paramKey: 'conversionPeriods' },
|
||||
{ plotIndex: 1, plotId: 'plot1', paramKey: 'basePeriods' },
|
||||
{ plotIndex: 2, plotId: 'plot2', paramKey: 'displacement' },
|
||||
{ plotIndex: 3, plotId: 'plot3', paramKey: 'laggingSpan2Periods' },
|
||||
{ plotIndex: 4, plotId: 'plot4', paramKey: 'displacement' },
|
||||
{ plotIndex: 3, plotId: 'plot3' },
|
||||
{ plotIndex: 4, plotId: 'plot4', paramKey: 'laggingSpan2Periods' },
|
||||
];
|
||||
|
||||
export const ICHIMOKU_DEFAULT_PARAMS: Record<string, number> = {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { IndicatorConfig } from '../types';
|
||||
|
||||
export type IndicatorChartSyncMode =
|
||||
| 'none'
|
||||
| 'remove-only'
|
||||
| 'add-only'
|
||||
| 'params-changed'
|
||||
| 'reorder-only'
|
||||
| 'full';
|
||||
|
||||
export interface IndicatorChartChange {
|
||||
mode: IndicatorChartSyncMode;
|
||||
removedIds: string[];
|
||||
added: IndicatorConfig[];
|
||||
paramsChangedIds: string[];
|
||||
}
|
||||
|
||||
export function singleIndParamKey(i: IndicatorConfig): string {
|
||||
return `${JSON.stringify(i.params)}|${(i.plots ?? []).map(p => p.id).sort().join(',')}|${i.mergedWith ?? ''}`;
|
||||
}
|
||||
|
||||
/** 이전·현재 지표 목록 차이를 분류해 최소 갱신 경로를 선택 */
|
||||
export function classifyIndicatorChartChange(
|
||||
prev: IndicatorConfig[],
|
||||
curr: IndicatorConfig[],
|
||||
): IndicatorChartChange {
|
||||
const prevMap = new Map(prev.map(i => [i.id, i]));
|
||||
const currMap = new Map(curr.map(i => [i.id, i]));
|
||||
|
||||
const removedIds = prev.filter(i => !currMap.has(i.id)).map(i => i.id);
|
||||
const added = curr.filter(i => !prevMap.has(i.id));
|
||||
|
||||
const paramsChangedIds: string[] = [];
|
||||
for (const c of curr) {
|
||||
const p = prevMap.get(c.id);
|
||||
if (!p) continue;
|
||||
if (singleIndParamKey(p) !== singleIndParamKey(c)) {
|
||||
paramsChangedIds.push(c.id);
|
||||
}
|
||||
}
|
||||
|
||||
const empty: IndicatorChartChange = {
|
||||
mode: 'none',
|
||||
removedIds,
|
||||
added,
|
||||
paramsChangedIds,
|
||||
};
|
||||
|
||||
if (removedIds.length === 0 && added.length === 0 && paramsChangedIds.length === 0) {
|
||||
const sameSet = prev.length === curr.length
|
||||
&& prev.every(p => currMap.has(p.id));
|
||||
if (sameSet && prev.map(i => i.id).join(',') !== curr.map(i => i.id).join(',')) {
|
||||
return { ...empty, mode: 'reorder-only' };
|
||||
}
|
||||
return empty;
|
||||
}
|
||||
|
||||
if (removedIds.length > 0 && added.length === 0 && paramsChangedIds.length === 0) {
|
||||
return { ...empty, mode: 'remove-only' };
|
||||
}
|
||||
|
||||
if (added.length > 0 && removedIds.length === 0 && paramsChangedIds.length === 0) {
|
||||
return { ...empty, mode: 'add-only' };
|
||||
}
|
||||
|
||||
if (
|
||||
paramsChangedIds.length > 0
|
||||
&& removedIds.length === 0
|
||||
&& added.length === 0
|
||||
) {
|
||||
return { ...empty, mode: 'params-changed' };
|
||||
}
|
||||
|
||||
return { ...empty, mode: 'full' };
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { normalizeIchimokuConfig } from './ichimokuConfig';
|
||||
import type { IchimokuCloudColors } from './ichimokuConfig';
|
||||
import { isMainIndicatorType } from './indicatorMainTab';
|
||||
import { indicatorDefaultsFingerprint } from './indicatorDefaultsFingerprint';
|
||||
|
||||
export type MainIndicatorDraftMap = Record<string, IndicatorConfig>;
|
||||
|
||||
@@ -93,6 +94,42 @@ function finalizeMainIndicatorConfig(cfg: IndicatorConfig, isNew: boolean): Indi
|
||||
return out;
|
||||
}
|
||||
|
||||
/** DB에 저장된 전역 기본값 → 차트 인스턴스에 반영 (id·hidden·pane 병합 상태 유지) */
|
||||
export function mergeGlobalDefaultsOntoChartIndicators(
|
||||
activeIndicators: IndicatorConfig[],
|
||||
getParams: (type: string, defaults: Record<string, number | string | boolean>) => Record<string, number | string | boolean>,
|
||||
getVisualConfig: (
|
||||
type: string,
|
||||
defaultPlots?: PlotDef[],
|
||||
defaultHlines?: HLineDef[],
|
||||
) => { plots: PlotDef[]; hlines: HLineDef[]; cloudColors?: IchimokuCloudColors },
|
||||
): IndicatorConfig[] {
|
||||
return activeIndicators.map(ind => {
|
||||
const cfg = buildMainIndicatorConfig(ind.type, [ind], getParams, getVisualConfig);
|
||||
if (!cfg) return enrichIndicatorConfig(ind);
|
||||
return finalizeMainIndicatorConfig(
|
||||
{
|
||||
...cfg,
|
||||
id: ind.id,
|
||||
hidden: ind.hidden,
|
||||
mergedWith: ind.mergedWith,
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** 전역 기본값 병합 후 실제 렌더 설정이 바뀌었는지 */
|
||||
export function chartIndicatorsNeedDefaultsRefresh(
|
||||
before: IndicatorConfig[],
|
||||
after: IndicatorConfig[],
|
||||
): boolean {
|
||||
if (before.length !== after.length) return true;
|
||||
return before.some((ind, i) =>
|
||||
indicatorDefaultsFingerprint(ind) !== indicatorDefaultsFingerprint(after[i]),
|
||||
);
|
||||
}
|
||||
|
||||
/** Main 탭 설정 → 현재 차트에 올라간 동일 type 인스턴스에 반영 (id·hidden 유지) */
|
||||
export function mergeMainDraftOntoChart(
|
||||
activeIndicators: IndicatorConfig[],
|
||||
|
||||
@@ -202,3 +202,22 @@ export function buildIndicatorConfigsFromTypes(
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** id 제거 — 병합 호스트 제거 시 해당 pane 멤버도 함께 제거 */
|
||||
export function removeIndicatorFromList(
|
||||
prev: IndicatorConfig[],
|
||||
id: string,
|
||||
): IndicatorConfig[] {
|
||||
return prev.filter(i => i.id !== id && i.mergedWith !== id);
|
||||
}
|
||||
|
||||
/** type 제거 — 해당 type 인스턴스 및 병합 멤버 함께 제거 */
|
||||
export function removeIndicatorTypeFromList(
|
||||
prev: IndicatorConfig[],
|
||||
type: string,
|
||||
): IndicatorConfig[] {
|
||||
const removedIds = new Set(prev.filter(i => i.type === type).map(i => i.id));
|
||||
return prev.filter(
|
||||
i => i.type !== type && (!i.mergedWith || !removedIds.has(i.mergedWith)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,11 +46,12 @@ export const INDICATOR_PLOT_PARAMS: Record<string, Record<string, string[]>> = {
|
||||
IchimokuCloud: {
|
||||
plot0: ['conversionPeriods'],
|
||||
plot1: ['basePeriods'],
|
||||
/** 선행스팬1 — 전환·기준 평균선의 미래 이동(shift) 기간 */
|
||||
/** 후행스팬 — chikou 이동(displacement) */
|
||||
plot2: ['displacement'],
|
||||
plot3: ['laggingSpan2Periods'],
|
||||
/** 후행스팬 — chikou 이동(선행스팬1과 동일 displacement) */
|
||||
plot4: ['displacement'],
|
||||
/** 선행스팬1 — 전환·기준선에서 산출, 별도 기간 없음 */
|
||||
plot3: [],
|
||||
/** 선행스팬2 — Donchian 기간 */
|
||||
plot4: ['laggingSpan2Periods'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user