실시간 차트 스크롤시 날짜 동기화 문제

This commit is contained in:
Macbook
2026-05-29 21:30:34 +09:00
parent 6556741f83
commit ab5c11fa14
14 changed files with 263 additions and 73 deletions
+76 -31
View File
@@ -1,7 +1,10 @@
/**
* 캔들 pane 하단 시간축 — 거래량·보조지표가 아래에 있을 때 캔들 영역 바로 아래에도 시간 표시.
*
* 드래그 패닝 중에는 라벨 좌표를 재계산하지 않고 translate3d 로 차트와 동일 픽셀만큼 이동한다.
* (setVisibleLogicalRange 직후 LWC 좌표 API가 한 프레임 늦게 갱신되는 문제 회피)
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useEffect, useCallback, useRef } from 'react';
import type { ChartManager } from '../utils/ChartManager';
import { subscribeChartTimeFormat } from '../utils/chartTimeFormat';
@@ -12,24 +15,81 @@ interface CandlePaneTimeAxisProps {
containerEl: HTMLElement;
}
const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, containerEl }) => {
const [state, setState] = useState<ReturnType<ChartManager['getCandlePaneTimeAxisOverlay']>>(null);
function applyOverlayLayout(
root: HTMLDivElement,
pool: HTMLSpanElement[],
state: NonNullable<ReturnType<ChartManager['getCandlePaneTimeAxisOverlay']>>,
): void {
root.style.display = 'flex';
root.style.top = `${state.topY}px`;
root.style.height = `${state.height ?? AXIS_HEIGHT}px`;
root.style.width = `${state.plotWidth}px`;
const sync = useCallback(() => {
if (!containerEl.isConnected) return;
setState(manager.getCandlePaneTimeAxisOverlay());
state.labels.forEach((label, i) => {
let span = pool[i];
if (!span) {
span = document.createElement('span');
span.className = 'candle-pane-time-axis__label';
root.appendChild(span);
pool[i] = span;
}
span.style.display = '';
span.style.left = `${label.x}px`;
span.textContent = label.text;
});
for (let i = state.labels.length; i < pool.length; i++) {
if (pool[i]) pool[i].style.display = 'none';
}
}
const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, containerEl }) => {
const axisRef = useRef<HTMLDivElement>(null);
const labelPoolRef = useRef<HTMLSpanElement[]>([]);
const renderOverlay = useCallback(() => {
const root = axisRef.current;
if (!root || !containerEl.isConnected) return;
const panActive = manager.isCandleAxisPanActive();
const panOffset = manager.getCandleAxisPanOffsetPx();
if (panActive) {
const state = manager.getCandlePaneTimeAxisOverlay();
if (!state || state.labels.length === 0) {
root.style.display = 'none';
return;
}
if (panOffset === 0) {
applyOverlayLayout(root, labelPoolRef.current, state);
}
root.style.transform = `translate3d(${panOffset}px, 0, 0)`;
return;
}
root.style.transform = '';
const state = manager.getCandlePaneTimeAxisOverlay();
if (!state || state.labels.length === 0) {
root.style.display = 'none';
for (const span of labelPoolRef.current) {
span.style.display = 'none';
}
return;
}
applyOverlayLayout(root, labelPoolRef.current, state);
}, [manager, containerEl]);
useEffect(() => {
sync();
const ro = new ResizeObserver(sync);
renderOverlay();
const ro = new ResizeObserver(renderOverlay);
ro.observe(containerEl);
const unsubLayout = manager.subscribePaneLayout(sync);
const unsubRange = manager.subscribeViewport(sync);
const unsubFormat = subscribeChartTimeFormat(sync);
const unsubLayout = manager.subscribePaneLayout(renderOverlay);
const unsubRange = manager.subscribeViewport(renderOverlay);
const unsubFormat = subscribeChartTimeFormat(renderOverlay);
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(sync, ms));
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(renderOverlay, ms));
return () => {
ro.disconnect();
unsubLayout();
@@ -37,30 +97,15 @@ const CandlePaneTimeAxis: React.FC<CandlePaneTimeAxisProps> = ({ manager, contai
unsubFormat();
timers.forEach(clearTimeout);
};
}, [manager, containerEl, sync]);
if (!state || state.labels.length === 0) return null;
}, [manager, containerEl, renderOverlay]);
return (
<div
ref={axisRef}
className="candle-pane-time-axis"
style={{
top: state.topY,
height: state.height ?? AXIS_HEIGHT,
width: state.plotWidth,
}}
style={{ display: 'none' }}
aria-hidden
>
{state.labels.map((label, idx) => (
<span
key={`${label.x}-${idx}`}
className="candle-pane-time-axis__label"
style={{ left: label.x }}
>
{label.text}
</span>
))}
</div>
/>
);
};