chore: initial import
This commit is contained in:
177
apps/visualizer/src/features/chart/ChartPanel.tsx
Normal file
177
apps/visualizer/src/features/chart/ChartPanel.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Candle, ChartIndicators } from '../../lib/api';
|
||||
import Card from '../../ui/Card';
|
||||
import ChartSideToolbar from './ChartSideToolbar';
|
||||
import ChartToolbar from './ChartToolbar';
|
||||
import TradingChart from './TradingChart';
|
||||
import type { FibAnchor, FibRetracement } from './FibRetracementPrimitive';
|
||||
import type { IChartApi } from 'lightweight-charts';
|
||||
|
||||
type Props = {
|
||||
candles: Candle[];
|
||||
indicators: ChartIndicators;
|
||||
timeframe: string;
|
||||
onTimeframeChange: (tf: string) => void;
|
||||
showIndicators: boolean;
|
||||
onToggleIndicators: () => void;
|
||||
seriesLabel: string;
|
||||
};
|
||||
|
||||
export default function ChartPanel({
|
||||
candles,
|
||||
indicators,
|
||||
timeframe,
|
||||
onTimeframeChange,
|
||||
showIndicators,
|
||||
onToggleIndicators,
|
||||
seriesLabel,
|
||||
}: Props) {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [activeTool, setActiveTool] = useState<'cursor' | 'fib-retracement'>('cursor');
|
||||
const [fibStart, setFibStart] = useState<FibAnchor | null>(null);
|
||||
const [fib, setFib] = useState<FibRetracement | null>(null);
|
||||
const [fibDraft, setFibDraft] = useState<FibRetracement | null>(null);
|
||||
const chartApiRef = useRef<IChartApi | null>(null);
|
||||
const activeToolRef = useRef(activeTool);
|
||||
const fibStartRef = useRef<FibAnchor | null>(fibStart);
|
||||
const pendingMoveRef = useRef<FibAnchor | null>(null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFullscreen) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setIsFullscreen(false);
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [isFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle('chartFullscreen', isFullscreen);
|
||||
return () => document.body.classList.remove('chartFullscreen');
|
||||
}, [isFullscreen]);
|
||||
|
||||
const cardClassName = useMemo(() => {
|
||||
return ['chartCard', isFullscreen ? 'chartCard--fullscreen' : null].filter(Boolean).join(' ');
|
||||
}, [isFullscreen]);
|
||||
|
||||
useEffect(() => {
|
||||
activeToolRef.current = activeTool;
|
||||
if (activeTool !== 'fib-retracement') {
|
||||
setFibStart(null);
|
||||
setFibDraft(null);
|
||||
}
|
||||
}, [activeTool]);
|
||||
|
||||
useEffect(() => {
|
||||
fibStartRef.current = fibStart;
|
||||
}, [fibStart]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (activeToolRef.current !== 'fib-retracement') return;
|
||||
setFibStart(null);
|
||||
setFibDraft(null);
|
||||
setActiveTool('cursor');
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function zoomTime(factor: number) {
|
||||
const chart = chartApiRef.current;
|
||||
if (!chart) return;
|
||||
const ts = chart.timeScale();
|
||||
const range = ts.getVisibleLogicalRange();
|
||||
if (!range) return;
|
||||
const from = range.from as number;
|
||||
const to = range.to as number;
|
||||
const span = Math.max(5, (to - from) * factor);
|
||||
const center = (from + to) / 2;
|
||||
ts.setVisibleLogicalRange({ from: center - span / 2, to: center + span / 2 });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFullscreen ? <div className="chartBackdrop" onClick={() => setIsFullscreen(false)} /> : null}
|
||||
<Card className={cardClassName}>
|
||||
<div className="chartCard__toolbar">
|
||||
<ChartToolbar
|
||||
timeframe={timeframe}
|
||||
onTimeframeChange={onTimeframeChange}
|
||||
showIndicators={showIndicators}
|
||||
onToggleIndicators={onToggleIndicators}
|
||||
seriesLabel={seriesLabel}
|
||||
isFullscreen={isFullscreen}
|
||||
onToggleFullscreen={() => setIsFullscreen((v) => !v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="chartCard__content">
|
||||
<ChartSideToolbar
|
||||
timeframe={timeframe}
|
||||
activeTool={activeTool}
|
||||
hasFib={fib != null || fibDraft != null}
|
||||
onToolChange={setActiveTool}
|
||||
onZoomIn={() => zoomTime(0.8)}
|
||||
onZoomOut={() => zoomTime(1.25)}
|
||||
onResetView={() => chartApiRef.current?.timeScale().resetTimeScale()}
|
||||
onClearFib={() => {
|
||||
setFib(null);
|
||||
setFibStart(null);
|
||||
setFibDraft(null);
|
||||
}}
|
||||
/>
|
||||
<div className="chartCard__chart">
|
||||
<TradingChart
|
||||
candles={candles}
|
||||
oracle={indicators.oracle}
|
||||
sma20={indicators.sma20}
|
||||
ema20={indicators.ema20}
|
||||
bb20={indicators.bb20}
|
||||
showIndicators={showIndicators}
|
||||
fib={fibDraft ?? fib}
|
||||
onReady={({ chart }) => {
|
||||
chartApiRef.current = chart;
|
||||
}}
|
||||
onChartClick={(p) => {
|
||||
if (activeTool !== 'fib-retracement') return;
|
||||
if (!fibStartRef.current) {
|
||||
fibStartRef.current = p;
|
||||
setFibStart(p);
|
||||
setFibDraft({ a: p, b: p });
|
||||
return;
|
||||
}
|
||||
setFib({ a: fibStartRef.current, b: p });
|
||||
setFibStart(null);
|
||||
fibStartRef.current = null;
|
||||
setFibDraft(null);
|
||||
setActiveTool('cursor');
|
||||
}}
|
||||
onChartCrosshairMove={(p) => {
|
||||
if (activeToolRef.current !== 'fib-retracement') return;
|
||||
const start = fibStartRef.current;
|
||||
if (!start) return;
|
||||
pendingMoveRef.current = p;
|
||||
if (rafRef.current != null) return;
|
||||
rafRef.current = window.requestAnimationFrame(() => {
|
||||
rafRef.current = null;
|
||||
const move = pendingMoveRef.current;
|
||||
const start2 = fibStartRef.current;
|
||||
if (!move || !start2) return;
|
||||
setFibDraft({ a: start2, b: move });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user