import { useMemo } from 'react'; import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, } from 'recharts'; import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; import { useTheme } from '../contexts/ThemeContext'; import type { PricePoint, Decision } from '../types'; interface PredictionPoint { date: string; decision: Decision; price?: number; } interface StockPriceChartProps { priceHistory: PricePoint[]; predictions?: PredictionPoint[]; symbol: string; showArea?: boolean; } // Custom tooltip component const CustomTooltip = ({ active, payload, label }: any) => { if (active && payload && payload.length) { const data = payload[0].payload; return (

{new Date(label).toLocaleDateString('en-IN', { weekday: 'short', month: 'short', day: 'numeric', })}

₹{data.price.toLocaleString('en-IN', { minimumFractionDigits: 2 })}

{data.prediction && (
{data.prediction === 'BUY' && } {data.prediction === 'SELL' && } {data.prediction === 'HOLD' && } AI: {data.prediction}
)}
); } return null; }; // Custom prediction marker component with arrow symbols const PredictionMarker = (props: any) => { const { cx, cy, payload } = props; if (!payload?.prediction || cx === undefined || cy === undefined) return null; const colors = { BUY: { fill: '#22c55e', stroke: '#16a34a' }, SELL: { fill: '#ef4444', stroke: '#dc2626' }, HOLD: { fill: '#f59e0b', stroke: '#d97706' }, }; const color = colors[payload.prediction as Decision] || colors.HOLD; // Render different shapes based on prediction type if (payload.prediction === 'BUY') { // Up arrow return ( ); } else if (payload.prediction === 'SELL') { // Down arrow return ( ); } else { // Equal/minus sign for HOLD return ( ); } }; export default function StockPriceChart({ priceHistory, predictions = [], symbol, showArea = true, }: StockPriceChartProps) { const { resolvedTheme } = useTheme(); const isDark = resolvedTheme === 'dark'; // Theme-aware colors const gridColor = isDark ? '#475569' : '#e5e7eb'; const tickColor = isDark ? '#94a3b8' : '#6b7280'; // Merge price history with predictions const chartData = useMemo(() => { const predictionMap = new Map( predictions.map(p => [p.date, p.decision]) ); return priceHistory.map(point => ({ ...point, prediction: predictionMap.get(point.date) || null, })); }, [priceHistory, predictions]); // Calculate price range for Y-axis const { minPrice, maxPrice } = useMemo(() => { const prices = priceHistory.map(p => p.price); const min = Math.min(...prices); const max = Math.max(...prices); const padding = (max - min) * 0.1; return { minPrice: Math.floor(min - padding), maxPrice: Math.ceil(max + padding), }; }, [priceHistory]); // Calculate overall trend const trend = useMemo(() => { if (priceHistory.length < 2) return 'flat'; const first = priceHistory[0].price; const last = priceHistory[priceHistory.length - 1].price; const change = ((last - first) / first) * 100; return change > 0 ? 'up' : change < 0 ? 'down' : 'flat'; }, [priceHistory]); const trendColor = trend === 'up' ? '#22c55e' : trend === 'down' ? '#ef4444' : '#6b7280'; const gradientId = `gradient-${symbol}`; if (priceHistory.length === 0) { return (
No price data available
); } // Background color based on theme const chartBgColor = isDark ? '#1e293b' : '#ffffff'; return (
new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric', })} interval="preserveStartEnd" minTickGap={50} /> `₹${value}`} width={60} /> } /> {showArea && ( )} { const { payload, cx, cy } = props; if (payload?.prediction && cx !== undefined && cy !== undefined) { return ; } return ; // Return empty group for non-prediction points }} activeDot={{ r: 4, fill: trendColor }} isAnimationActive={false} /> {/* Legend */}
BUY Signal
HOLD Signal
SELL Signal
); }