138 lines
4.1 KiB
TypeScript
138 lines
4.1 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;
|
|
|
|
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;
|