Files
goldenChart/frontend/src/components/CandlePaneTimeAxis.tsx
T
2026-05-29 21:37:41 +09:00

154 lines
4.6 KiB
TypeScript

/**
* 캔들 pane 하단 시간축 — 거래량·보조지표가 아래에 있을 때 캔들 영역 바로 아래에도 시간 표시.
* 크로스헤어 세로선 위치에 해당 날짜·시간 배지를 함께 표시한다.
*/
import React, { useEffect, useCallback, useRef } from 'react';
import type { MouseEventParams, Time } from 'lightweight-charts';
import type { ChartManager } from '../utils/ChartManager';
import { subscribeChartTimeFormat } from '../utils/chartTimeFormat';
const AXIS_HEIGHT = 22;
interface CandlePaneTimeAxisProps {
manager: ChartManager;
containerEl: HTMLElement;
}
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`;
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 crosshairRef = useRef<HTMLDivElement>(null);
const labelPoolRef = useRef<HTMLSpanElement[]>([]);
const updateCrosshairLabel = useCallback((p: MouseEventParams<Time>) => {
const el = crosshairRef.current;
if (!el) return;
const band = manager.getCandlePaneTimeAxisBand();
if (!band || !p.time || p.point?.x == null) {
el.style.display = 'none';
return;
}
const x = p.point.x;
if (!Number.isFinite(x) || x < 0 || x > band.plotWidth) {
el.style.display = 'none';
return;
}
el.style.display = 'flex';
el.style.top = `${band.topY}px`;
el.style.height = `${band.height}px`;
el.style.left = `${x}px`;
el.textContent = manager.formatCrosshairAxisTime(p.time);
}, [manager]);
const hideCrosshairLabel = useCallback(() => {
if (crosshairRef.current) crosshairRef.current.style.display = 'none';
}, []);
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(() => {
renderOverlay();
const ro = new ResizeObserver(renderOverlay);
ro.observe(containerEl);
const unsubLayout = manager.subscribePaneLayout(renderOverlay);
const unsubRange = manager.subscribeViewport(renderOverlay);
const unsubFormat = subscribeChartTimeFormat(renderOverlay);
const unsubCrosshair = manager.subscribeCrosshair(updateCrosshairLabel);
const onLeave = () => hideCrosshairLabel();
containerEl.addEventListener('mouseleave', onLeave);
const timers = [0, 50, 150, 400].map(ms => window.setTimeout(renderOverlay, ms));
return () => {
ro.disconnect();
unsubLayout();
unsubRange();
unsubFormat();
unsubCrosshair();
containerEl.removeEventListener('mouseleave', onLeave);
timers.forEach(clearTimeout);
};
}, [manager, containerEl, renderOverlay, updateCrosshairLabel, hideCrosshairLabel]);
return (
<>
<div
ref={axisRef}
className="candle-pane-time-axis"
style={{ display: 'none' }}
aria-hidden
/>
<div
ref={crosshairRef}
className="candle-pane-time-axis__crosshair"
style={{ display: 'none' }}
aria-hidden
/>
</>
);
};
export default CandlePaneTimeAxis;