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
);
}