실시간 차트 스크롤시 날짜 동기화 문제
This commit is contained in:
@@ -418,6 +418,7 @@ export class ChartManager {
|
||||
} else {
|
||||
ts.fitContent();
|
||||
}
|
||||
this._notifyViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -430,6 +431,7 @@ export class ChartManager {
|
||||
from: bars[startIdx].time as Time,
|
||||
to: to as Time,
|
||||
});
|
||||
this._notifyViewport();
|
||||
}
|
||||
|
||||
private _createMainSeries(chartType: ChartType, t: ThemeTokens): ISeriesApi<SeriesType> {
|
||||
@@ -1929,11 +1931,83 @@ export class ChartManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 가시 시간 범위 변경 알림 (커스텀 패닝·줌 등 LWC 이벤트 미발생 경로 포함) */
|
||||
private readonly _viewportListeners = new Set<() => void>();
|
||||
private _viewportLwcHooked = false;
|
||||
private readonly _lwcViewportHandler = (): void => { this._notifyViewport(); };
|
||||
|
||||
/** 캔들 pane 시간축 — 드래그 패닝 중 transform 동기화용 */
|
||||
private _candleAxisPanActive = false;
|
||||
private _candleAxisPanOffsetPx = 0;
|
||||
|
||||
private _notifyViewport(): void {
|
||||
for (const cb of this._viewportListeners) {
|
||||
try { cb(); } catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
|
||||
private _hookViewportLwc(): void {
|
||||
if (this._viewportLwcHooked) return;
|
||||
this._viewportLwcHooked = true;
|
||||
this.chart.timeScale().subscribeVisibleTimeRangeChange(this._lwcViewportHandler);
|
||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this._lwcViewportHandler);
|
||||
}
|
||||
|
||||
private _unhookViewportLwc(): void {
|
||||
if (!this._viewportLwcHooked) return;
|
||||
this._viewportLwcHooked = false;
|
||||
try { this.chart.timeScale().unsubscribeVisibleTimeRangeChange(this._lwcViewportHandler); } catch { /* ok */ }
|
||||
try { this.chart.timeScale().unsubscribeVisibleLogicalRangeChange(this._lwcViewportHandler); } catch { /* ok */ }
|
||||
}
|
||||
|
||||
/** 논리 범위 변경 + 오버레이(시간축·드로잉) 동기화 */
|
||||
private _setVisibleLogicalRange(range: { from: number; to: number }): void {
|
||||
try {
|
||||
this.chart.timeScale().setVisibleLogicalRange(range);
|
||||
this._notifyViewport();
|
||||
} catch { /* 데이터 로드 전 */ }
|
||||
}
|
||||
|
||||
/** PaneLegend 등 — 보조지표 pane DOM 배치가 끝난 뒤 레이블 위치 재동기화 */
|
||||
notifyPaneLayoutChanged(): void {
|
||||
this._notifyPaneLayout();
|
||||
}
|
||||
|
||||
/** 시간축 오버레이 등 — 뷰포트 동기화 강제 (패닝 종료 시 등) */
|
||||
notifyViewportChanged(): void {
|
||||
this._notifyViewport();
|
||||
}
|
||||
|
||||
/** 캔들 pane 시간축 드래그 패닝 종료 — 라벨 위치를 최종 논리 범위로 재정렬 */
|
||||
endCandleAxisPan(): void {
|
||||
if (!this._candleAxisPanActive) return;
|
||||
this._candleAxisPanActive = false;
|
||||
this._candleAxisPanOffsetPx = 0;
|
||||
this._notifyViewport();
|
||||
}
|
||||
|
||||
isCandleAxisPanActive(): boolean {
|
||||
return this._candleAxisPanActive;
|
||||
}
|
||||
|
||||
getCandleAxisPanOffsetPx(): number {
|
||||
return this._candleAxisPanOffsetPx;
|
||||
}
|
||||
|
||||
private _ensureCandleAxisPan(): void {
|
||||
if (this._candleAxisPanActive) return;
|
||||
this._candleAxisPanActive = true;
|
||||
this._candleAxisPanOffsetPx = 0;
|
||||
this._notifyViewport();
|
||||
}
|
||||
|
||||
/** visible logical range → 플롯 X (LWC 좌표 API와 동일한 선형 매핑) */
|
||||
private _logicalToAxisX(logical: number, lr: { from: number; to: number }, plotWidth: number): number {
|
||||
const span = lr.to - lr.from;
|
||||
if (!Number.isFinite(span) || span <= 0) return 0;
|
||||
return ((logical - lr.from) / span) * plotWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 차트 컨테이너 기준 각 pane 의 topY 와 height 배열 반환.
|
||||
* IPaneApi.getHTMLElement() 로 pane index ↔ 화면 위치를 직접 매칭한다.
|
||||
@@ -2340,7 +2414,7 @@ export class ChartManager {
|
||||
const span = lr.to - lr.from;
|
||||
const center = (lr.from + lr.to) / 2;
|
||||
const newSpan = Math.max(span * 0.8, 5); // 최소 5바
|
||||
ts.setVisibleLogicalRange({ from: center - newSpan / 2, to: center + newSpan / 2 });
|
||||
this._setVisibleLogicalRange({ from: center - newSpan / 2, to: center + newSpan / 2 });
|
||||
}
|
||||
|
||||
/** 시간축 축소 (visible logical range 20% 확대) */
|
||||
@@ -2351,7 +2425,7 @@ export class ChartManager {
|
||||
const span = lr.to - lr.from;
|
||||
const center = (lr.from + lr.to) / 2;
|
||||
const newSpan = span * 1.25;
|
||||
ts.setVisibleLogicalRange({ from: center - newSpan / 2, to: center + newSpan / 2 });
|
||||
this._setVisibleLogicalRange({ from: center - newSpan / 2, to: center + newSpan / 2 });
|
||||
}
|
||||
|
||||
/** 시간축 왼쪽(과거)으로 스크롤 — visible 범위의 20%씩 이동 */
|
||||
@@ -2360,7 +2434,7 @@ export class ChartManager {
|
||||
const lr = ts.getVisibleLogicalRange();
|
||||
if (!lr) return;
|
||||
const shift = (lr.to - lr.from) * 0.2;
|
||||
ts.setVisibleLogicalRange({ from: lr.from - shift, to: lr.to - shift });
|
||||
this._setVisibleLogicalRange({ from: lr.from - shift, to: lr.to - shift });
|
||||
}
|
||||
|
||||
/** 시간축 오른쪽(미래)으로 스크롤 — visible 범위의 20%씩 이동 */
|
||||
@@ -2369,7 +2443,7 @@ export class ChartManager {
|
||||
const lr = ts.getVisibleLogicalRange();
|
||||
if (!lr) return;
|
||||
const shift = (lr.to - lr.from) * 0.2;
|
||||
ts.setVisibleLogicalRange({ from: lr.from + shift, to: lr.to + shift });
|
||||
this._setVisibleLogicalRange({ from: lr.from + shift, to: lr.to + shift });
|
||||
}
|
||||
|
||||
/** 캔들 전용(보조지표·거래량 숨김) 레이아웃 적용 중 여부 */
|
||||
@@ -2678,25 +2752,23 @@ export class ChartManager {
|
||||
const lr = ts.getVisibleLogicalRange();
|
||||
if (!lr) return null;
|
||||
|
||||
const plotWidth = this._candlePlotWidth();
|
||||
const from = Math.max(0, Math.ceil(lr.from));
|
||||
const to = Math.min(this.rawBars.length - 1, Math.floor(lr.to));
|
||||
if (to <= from) return null;
|
||||
const plotWidth = Math.max(40, ts.width());
|
||||
const lrSpan = lr.to - lr.from;
|
||||
if (!Number.isFinite(lrSpan) || lrSpan <= 0) return null;
|
||||
|
||||
const span = to - from;
|
||||
const targetCount = Math.max(4, Math.min(12, Math.floor(plotWidth / 76)));
|
||||
const step = Math.max(1, Math.ceil(span / targetCount));
|
||||
const step = Math.max(1, Math.ceil((lr.to - Math.max(0, lr.from)) / targetCount));
|
||||
|
||||
const labels: Array<{ x: number; text: string }> = [];
|
||||
const fmt = this.chartTimeFormat;
|
||||
const tz = this.displayTimezone;
|
||||
const startLogical = Math.max(0, Math.ceil(lr.from));
|
||||
const endLogical = Math.min(this.rawBars.length - 1, Math.floor(lr.to));
|
||||
|
||||
for (let i = from; i <= to; i += step) {
|
||||
const bar = this.rawBars[i];
|
||||
for (let logical = startLogical; logical <= endLogical; logical += step) {
|
||||
const bar = this.rawBars[logical];
|
||||
if (!bar?.time) continue;
|
||||
const coord = ts.timeToCoordinate(bar.time as Time);
|
||||
if (coord == null) continue;
|
||||
const x = Number(coord);
|
||||
const x = this._logicalToAxisX(logical, lr, plotWidth);
|
||||
if (!Number.isFinite(x) || x < 12 || x > plotWidth - 12) continue;
|
||||
labels.push({
|
||||
x,
|
||||
@@ -2791,15 +2863,16 @@ export class ChartManager {
|
||||
options?: { allowVerticalPan?: boolean },
|
||||
): void {
|
||||
if (deltaX !== 0) {
|
||||
this._ensureCandleAxisPan();
|
||||
this._candleAxisPanOffsetPx += deltaX;
|
||||
|
||||
const ts = this.chart.timeScale();
|
||||
const lr = ts.getVisibleLogicalRange();
|
||||
if (lr) {
|
||||
const w = Math.max(1, this.container.clientWidth);
|
||||
const w = Math.max(1, ts.width());
|
||||
const span = lr.to - lr.from;
|
||||
const dLog = (deltaX / w) * span;
|
||||
try {
|
||||
ts.setVisibleLogicalRange({ from: lr.from - dLog, to: lr.to - dLog });
|
||||
} catch { /* 데이터 로드 전 */ }
|
||||
this._setVisibleLogicalRange({ from: lr.from - dLog, to: lr.to - dLog });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2901,6 +2974,7 @@ export class ChartManager {
|
||||
if (this.rawBars.length === 0) return;
|
||||
try {
|
||||
this.chart.timeScale().setVisibleRange({ from: from as Time, to: to as Time });
|
||||
this._notifyViewport();
|
||||
} catch { /* 데이터 로딩 전 호출 무시 */ }
|
||||
}
|
||||
|
||||
@@ -2910,9 +2984,7 @@ export class ChartManager {
|
||||
*/
|
||||
applyVisibleLogicalRange(from: number, to: number): void {
|
||||
if (this.rawBars.length === 0) return;
|
||||
try {
|
||||
this.chart.timeScale().setVisibleLogicalRange({ from, to });
|
||||
} catch { /* 데이터 로딩 전 호출 무시 */ }
|
||||
this._setVisibleLogicalRange({ from, to });
|
||||
}
|
||||
|
||||
/** Time sync: 가시 시간 범위 변화 구독. unsubscribe fn 반환 */
|
||||
@@ -2949,7 +3021,10 @@ export class ChartManager {
|
||||
const t0 = this.xToTime(Math.min(x0, x1));
|
||||
const t1 = this.xToTime(Math.max(x0, x1));
|
||||
if (t0 === null || t1 === null || t0 >= t1) return;
|
||||
this.chart.timeScale().setVisibleRange({ from: t0 as import('lightweight-charts').Time, to: t1 as import('lightweight-charts').Time });
|
||||
try {
|
||||
this.chart.timeScale().setVisibleRange({ from: t0 as import('lightweight-charts').Time, to: t1 as import('lightweight-charts').Time });
|
||||
this._notifyViewport();
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3089,12 +3164,10 @@ export class ChartManager {
|
||||
|
||||
// 가시 논리 범위 복원: 앞에 추가된 bar 수만큼 오른쪽으로 이동
|
||||
if (logicalRange) {
|
||||
try {
|
||||
this.chart.timeScale().setVisibleLogicalRange({
|
||||
from: logicalRange.from + newBars.length,
|
||||
to: logicalRange.to + newBars.length,
|
||||
});
|
||||
} catch { /* 무시 */ }
|
||||
this._setVisibleLogicalRange({
|
||||
from: logicalRange.from + newBars.length,
|
||||
to: logicalRange.to + newBars.length,
|
||||
});
|
||||
}
|
||||
|
||||
// 보조지표 전체 재계산 (새 데이터가 왼쪽에 추가됐으므로 전체 갱신 필요)
|
||||
@@ -3309,13 +3382,13 @@ export class ChartManager {
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Subscribe to visible range changes (time scale + price scale). Returns unsubscribe fn. */
|
||||
/** 가시 시간 범위 변경 구독 (LWC 네이티브 스크롤 + 커스텀 패닝). unsubscribe fn 반환 */
|
||||
subscribeViewport(cb: () => void): () => void {
|
||||
this.chart.timeScale().subscribeVisibleTimeRangeChange(cb);
|
||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(cb);
|
||||
this._viewportListeners.add(cb);
|
||||
this._hookViewportLwc();
|
||||
return () => {
|
||||
try { this.chart.timeScale().unsubscribeVisibleTimeRangeChange(cb); } catch { /* ok */ }
|
||||
try { this.chart.timeScale().unsubscribeVisibleLogicalRangeChange(cb); } catch { /* ok */ }
|
||||
this._viewportListeners.delete(cb);
|
||||
if (this._viewportListeners.size === 0) this._unhookViewportLwc();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user