import React, { useRef, useEffect } from 'react'; import { createChart, LineStyle } from 'lightweight-charts'; const cardStyle = { background: '#1a1a1a', padding: '1.5rem', borderRadius: '12px', border: '1px solid #333', marginBottom: '1.5rem', }; const fullscreenStyle = { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999, background: '#0d0d0d', padding: '1.5rem', display: 'flex', flexDirection: 'column', }; const headerStyle = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', }; const titleStyle = { fontSize: '1.1rem', fontWeight: 700, color: '#f7931a' }; const btnStyle = { background: 'none', border: '1px solid #555', color: '#aaa', borderRadius: '6px', padding: '0.4rem 1rem', cursor: 'pointer', fontSize: '0.85rem', marginLeft: '0.5rem', }; export default function BTCCandlestickChart({ candles, purchases, stats, fullscreen, onToggleFullscreen }) { const containerRef = useRef(null); const chartRef = useRef(null); const candleSeriesRef = useRef(null); // Build/rebuild chart when candles data or fullscreen changes useEffect(() => { if (!containerRef.current || !candles || candles.length === 0) return; const container = containerRef.current; const { width, height } = container.getBoundingClientRect(); const chart = createChart(container, { width: width || 800, height: fullscreen ? height - 20 : 350, layout: { background: { color: fullscreen ? '#0d0d0d' : '#1a1a1a' }, textColor: '#ccc', }, grid: { vertLines: { color: '#2a2a2a' }, horzLines: { color: '#2a2a2a' }, }, crosshair: { mode: 1 }, rightPriceScale: { borderColor: '#333' }, timeScale: { borderColor: '#333', timeVisible: false }, }); chartRef.current = chart; // Candlestick series const candleSeries = chart.addCandlestickSeries({ upColor: '#6bff8e', downColor: '#ff6b6b', borderUpColor: '#6bff8e', borderDownColor: '#ff6b6b', wickUpColor: '#6bff8e', wickDownColor: '#ff6b6b', }); candleSeriesRef.current = candleSeries; const candleData = candles.map(c => ({ time: c.date, open: c.open, high: c.high, low: c.low, close: c.close, })); candleSeries.setData(candleData); // Average buy price line const avgPrice = stats?.average_price ?? 0; if (avgPrice > 0) { const avgSeries = chart.addLineSeries({ color: '#4fc3f7', lineWidth: 1, lineStyle: LineStyle.Dashed, priceLineVisible: false, lastValueVisible: false, title: `Avg Buy €${avgPrice.toLocaleString()}`, }); avgSeries.setData(candles.map(c => ({ time: c.date, value: avgPrice }))); } // Purchase markers — use nearest-candle lookup so purchases always show if (purchases && purchases.length > 0) { const sortedDates = candles.map(c => c.date).sort(); const nearestDate = (target) => { let closest = sortedDates[0]; for (const d of sortedDates) { if (d <= target) closest = d; else break; } return closest; }; // Merge multiple purchases on the same candle into one marker const markerMap = {}; for (const p of purchases) { const d = nearestDate(p.date); if (!markerMap[d]) { markerMap[d] = { time: d, position: 'belowBar', color: '#f7931a', shape: 'arrowUp', text: `€${p.amount_eur.toLocaleString()}` }; } else { markerMap[d].text += ` +€${p.amount_eur.toLocaleString()}`; } } const markers = Object.values(markerMap).sort((a, b) => a.time.localeCompare(b.time)); if (markers.length > 0) candleSeries.setMarkers(markers); } chart.timeScale().fitContent(); // Responsive resize const observer = new ResizeObserver(entries => { const entry = entries[0]; if (!entry) return; chart.applyOptions({ width: entry.contentRect.width, height: fullscreen ? entry.contentRect.height - 20 : 350, }); }); observer.observe(container); return () => { observer.disconnect(); chart.remove(); chartRef.current = null; candleSeriesRef.current = null; }; }, [candles, purchases, stats, fullscreen]); const handleSave = () => { if (!chartRef.current) return; const canvas = chartRef.current.takeScreenshot(); const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = 'btc-candles.png'; a.click(); }; const containerWrapStyle = fullscreen ? { flex: 1, minHeight: 0 } : { width: '100%' }; return (