실시간 차트 보조지표 탭 문제 수정
This commit is contained in:
@@ -715,6 +715,7 @@ export class ChartManager {
|
||||
await this.addIndicator(config, { skipLayout: true });
|
||||
}
|
||||
if (configs.length > 0 && this._activeIndicatorPaneIndices().size > 0) {
|
||||
this._removeOrphanSubPanes();
|
||||
this.resetPaneHeights(this._lastLayoutAvailableHeight);
|
||||
this._notifyPaneLayout();
|
||||
}
|
||||
@@ -778,6 +779,8 @@ export class ChartManager {
|
||||
|
||||
if (this._indicatorLoadStale(loadGen)) return;
|
||||
|
||||
// 시리즈 제거 후 남은 빈 sub-pane 제거 (상단 orphan pane → 레이블·그래프 어긋남 방지)
|
||||
this._removeOrphanSubPanes();
|
||||
// 빈 pane stretch 정리 + 캔들 pane 비율 복구
|
||||
this.resetPaneHeights();
|
||||
this._notifyPaneLayout();
|
||||
@@ -1723,6 +1726,11 @@ export class ChartManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** PaneLegend 등 — 보조지표 pane DOM 배치가 끝난 뒤 레이블 위치 재동기화 */
|
||||
notifyPaneLayoutChanged(): void {
|
||||
this._notifyPaneLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 컨테이너 기준 각 pane 의 topY 와 height 배열 반환.
|
||||
* IPaneApi.getHTMLElement() 로 pane index ↔ 화면 위치를 직접 매칭한다.
|
||||
@@ -1805,22 +1813,25 @@ export class ChartManager {
|
||||
getIndicatorPaneScreenLayout(id: string): { topY: number; height: number } | null {
|
||||
const entry = this.indicators.get(id);
|
||||
if (!entry || entry.paneIndex == null || entry.paneIndex < 2) return null;
|
||||
const series = entry.seriesList[0];
|
||||
if (!series) return null;
|
||||
try {
|
||||
const el = series.getPane().getHTMLElement();
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
const height = Math.round(rect.height);
|
||||
if (height < 12) return null;
|
||||
return {
|
||||
topY: Math.round(rect.top - containerRect.top),
|
||||
height,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
const MIN_H = 4;
|
||||
|
||||
for (const series of entry.seriesList) {
|
||||
try {
|
||||
const el = series.getPane().getHTMLElement();
|
||||
if (!el) continue;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const height = Math.round(rect.height);
|
||||
if (height < MIN_H) continue;
|
||||
return {
|
||||
topY: Math.round(rect.top - containerRect.top),
|
||||
height,
|
||||
};
|
||||
} catch {
|
||||
/* try next series */
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2373,6 +2384,22 @@ export class ChartManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 활성 지표가 없는 sub-pane 제거 (중간·상단 orphan pane 포함) */
|
||||
private _removeOrphanSubPanes(): void {
|
||||
const active = this._activeIndicatorPaneIndices();
|
||||
for (let i = this.chart.panes().length - 1; i >= 2; i--) {
|
||||
const panes = this.chart.panes();
|
||||
if (i >= panes.length) continue;
|
||||
const pi = panes[i].paneIndex();
|
||||
if (active.has(pi)) continue;
|
||||
try {
|
||||
this.chart.removePane(i);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 활성 지표가 없는 맨 끝 sub-pane 만 제거 — 단일 지표 제거 시 나머지 pane 유지 */
|
||||
private _trimTrailingEmptySubPanes(): void {
|
||||
for (;;) {
|
||||
|
||||
@@ -19,6 +19,21 @@ export function singleIndParamKey(i: IndicatorConfig): string {
|
||||
return `${JSON.stringify(i.params)}|${(i.plots ?? []).map(p => p.id).sort().join(',')}|${i.mergedWith ?? ''}`;
|
||||
}
|
||||
|
||||
/** 차트에 로드된 지표 id 중 현재 설정에 없는(이전 탭 잔존) 항목이 있는지 */
|
||||
export function chartHasStaleIndicators(
|
||||
loadedIds: Set<string>,
|
||||
indicators: IndicatorConfig[],
|
||||
): boolean {
|
||||
if (loadedIds.size === 0) return false;
|
||||
const expected = new Set(
|
||||
indicators.filter(i => i.hidden !== true).map(i => i.id),
|
||||
);
|
||||
for (const id of loadedIds) {
|
||||
if (!expected.has(id)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 이전·현재 지표 목록 차이를 분류해 최소 갱신 경로를 선택 */
|
||||
export function classifyIndicatorChartChange(
|
||||
prev: IndicatorConfig[],
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 보조지표 설정 목록 표시·차트 pane 순서 (localStorage)
|
||||
*/
|
||||
import { getSettingsIndicatorTypes } from './indicatorSettingsList';
|
||||
import type { IndicatorConfig } from '../types';
|
||||
import { getPaneHostId } from './indicatorPaneMerge';
|
||||
|
||||
const STORAGE_KEY = 'gc_indicator_settings_list_order';
|
||||
|
||||
export function loadIndicatorListOrder(): string[] | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed.filter((t): t is string => typeof t === 'string' && t.length > 0);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveIndicatorListOrder(types: string[]): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(types));
|
||||
} catch {
|
||||
/* quota */
|
||||
}
|
||||
}
|
||||
|
||||
/** 저장 순서 + registry 신규 type 병합 */
|
||||
export function mergeIndicatorListOrder(saved: string[] | null, canonical: readonly string[]): string[] {
|
||||
const canonSet = new Set(canonical);
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
if (saved) {
|
||||
for (const t of saved) {
|
||||
if (canonSet.has(t) && !seen.has(t)) {
|
||||
seen.add(t);
|
||||
out.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of canonical) {
|
||||
if (!seen.has(t)) {
|
||||
seen.add(t);
|
||||
out.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getOrderedSettingsIndicatorTypes(): string[] {
|
||||
const canonical = getSettingsIndicatorTypes();
|
||||
return mergeIndicatorListOrder(loadIndicatorListOrder(), canonical);
|
||||
}
|
||||
|
||||
export function compareIndicatorTypeOrder(
|
||||
a: string,
|
||||
b: string,
|
||||
order: readonly string[],
|
||||
): number {
|
||||
const rank = new Map(order.map((t, i) => [t, i]));
|
||||
const ra = rank.get(a) ?? 1_000_000;
|
||||
const rb = rank.get(b) ?? 1_000_000;
|
||||
if (ra !== rb) return ra - rb;
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
/** 차트 인스턴스 배열을 설정 목록 type 순서로 정렬 (병합 pane 은 호스트와 함께 이동) */
|
||||
export function sortIndicatorsByTypeOrder(
|
||||
inds: IndicatorConfig[],
|
||||
order: readonly string[] = getOrderedSettingsIndicatorTypes(),
|
||||
): IndicatorConfig[] {
|
||||
if (inds.length <= 1) return inds;
|
||||
|
||||
const rankOfInd = (ind: IndicatorConfig): number => {
|
||||
const hostId = getPaneHostId(ind.id, inds);
|
||||
const host = inds.find(i => i.id === hostId) ?? ind;
|
||||
const idx = order.indexOf(host.type);
|
||||
const hostRank = idx >= 0 ? idx : 1_000_000;
|
||||
if (ind.id === hostId) return hostRank;
|
||||
const selfIdx = order.indexOf(ind.type);
|
||||
return hostRank + (selfIdx >= 0 ? selfIdx * 0.0001 : 0.0005);
|
||||
};
|
||||
|
||||
return [...inds].sort((a, b) => {
|
||||
const d = rankOfInd(a) - rankOfInd(b);
|
||||
if (d !== 0) return d;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function reorderIndicatorListType(
|
||||
order: string[],
|
||||
draggedType: string,
|
||||
targetType: string,
|
||||
place: 'before' | 'after' = 'before',
|
||||
): string[] {
|
||||
if (draggedType === targetType) return order;
|
||||
const without = order.filter(t => t !== draggedType);
|
||||
let idx = without.indexOf(targetType);
|
||||
if (idx < 0) return order;
|
||||
if (place === 'after') idx += 1;
|
||||
without.splice(idx, 0, draggedType);
|
||||
return without;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 지표 추가 팝업 — Main(주요지표) 탭 표시·적용 순서
|
||||
*/
|
||||
import { MAIN_INDICATOR_TYPES } from './indicatorMainTab';
|
||||
import { mergeIndicatorListOrder } from './indicatorListOrder';
|
||||
|
||||
const STORAGE_KEY = 'gc_indicator_main_tab_order';
|
||||
|
||||
export function loadMainTabOrder(): string[] | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed.filter((t): t is string => typeof t === 'string' && t.length > 0);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveMainTabOrder(types: string[]): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(types));
|
||||
} catch {
|
||||
/* quota */
|
||||
}
|
||||
}
|
||||
|
||||
export function getOrderedMainIndicatorTypes(): string[] {
|
||||
return mergeIndicatorListOrder(loadMainTabOrder(), MAIN_INDICATOR_TYPES);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { INDICATOR_REGISTRY } from './indicatorRegistry';
|
||||
import { MAIN_INDICATOR_TYPES } from './indicatorMainTab';
|
||||
import { getOrderedMainIndicatorTypes } from './indicatorMainTabOrder';
|
||||
import type { IndicatorCustomTab } from './indicatorCustomTabsStorage';
|
||||
|
||||
export type IndicatorTabApplySource =
|
||||
@@ -8,7 +8,7 @@ export type IndicatorTabApplySource =
|
||||
|
||||
export function resolveIndicatorTabApplyTypes(source: IndicatorTabApplySource): string[] {
|
||||
if (source.kind === 'main') {
|
||||
return [...MAIN_INDICATOR_TYPES].filter(
|
||||
return getOrderedMainIndicatorTypes().filter(
|
||||
type => INDICATOR_REGISTRY.some(d => d.type === type),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user