149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
import React, { Suspense } from 'react';
|
|
import { Box, Typography } from '@mui/material';
|
|
import {
|
|
LazyLineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
Tooltip as RechartsTooltip,
|
|
ResponsiveContainer,
|
|
ChartLoadingFallback
|
|
} from '../../utils/lazyRecharts';
|
|
|
|
interface MiniSparklineProps {
|
|
data: Array<{ date: string; value: number }>;
|
|
color: string;
|
|
height?: number;
|
|
showArea?: boolean;
|
|
formatValue?: (value: number) => string;
|
|
label?: string;
|
|
}
|
|
|
|
/**
|
|
* MiniSparkline - Enhanced trend line chart for metric cards with axes and tooltips
|
|
*
|
|
* Usage:
|
|
* <MiniSparkline
|
|
* data={last7DaysData}
|
|
* color="#4ade80"
|
|
* height={60}
|
|
* formatValue={(v) => `$${v.toFixed(2)}`}
|
|
* label="Cost"
|
|
* />
|
|
*/
|
|
export const MiniSparkline: React.FC<MiniSparklineProps> = ({
|
|
data,
|
|
color,
|
|
height = 60,
|
|
showArea = false,
|
|
formatValue = (v) => v.toLocaleString(),
|
|
label = 'Value'
|
|
}) => {
|
|
// Ensure we have data
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<Box sx={{ height, width: '100%', mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', fontSize: '0.7rem' }}>
|
|
No data available
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// If only one data point, duplicate it for visual consistency
|
|
const chartData = data.length === 1
|
|
? [data[0], { ...data[0], value: data[0].value }]
|
|
: data;
|
|
|
|
// Calculate min/max for Y-axis domain
|
|
const values = chartData.map(d => d.value);
|
|
const minValue = Math.min(...values);
|
|
const maxValue = Math.max(...values);
|
|
const padding = (maxValue - minValue) * 0.1 || 0.1;
|
|
|
|
// Format date for X-axis
|
|
const formatDate = (dateStr: string) => {
|
|
try {
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return dateStr;
|
|
// Show day of month for daily data
|
|
return date.getDate().toString();
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
};
|
|
|
|
// Custom tooltip
|
|
const CustomTooltip = ({ active, payload }: any) => {
|
|
if (active && payload && payload.length) {
|
|
const data = payload[0].payload;
|
|
return (
|
|
<Box
|
|
sx={{
|
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
color: 'white',
|
|
padding: 1,
|
|
borderRadius: 1,
|
|
border: `1px solid ${color}`,
|
|
fontSize: '0.75rem'
|
|
}}
|
|
>
|
|
<Typography variant="caption" sx={{ display: 'block', fontWeight: 'bold', mb: 0.5 }}>
|
|
{new Date(data.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: color }}>
|
|
{label}: {formatValue(data.value)}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ height, width: '100%', mt: 1 }}>
|
|
<Suspense fallback={<ChartLoadingFallback />}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<LazyLineChart
|
|
data={chartData}
|
|
margin={{ top: 5, right: 5, bottom: 20, left: 5 }}
|
|
>
|
|
<XAxis
|
|
dataKey="date"
|
|
tickFormatter={formatDate}
|
|
stroke="rgba(255,255,255,0.5)"
|
|
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 10 }}
|
|
height={20}
|
|
/>
|
|
<YAxis
|
|
domain={[minValue - padding, maxValue + padding]}
|
|
tickFormatter={(value) => {
|
|
if (value >= 1000) return `${(value / 1000).toFixed(1)}k`;
|
|
if (value >= 1) return value.toFixed(0);
|
|
return value.toFixed(2);
|
|
}}
|
|
stroke="rgba(255,255,255,0.5)"
|
|
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 10 }}
|
|
width={40}
|
|
/>
|
|
<RechartsTooltip content={<CustomTooltip />} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="value"
|
|
stroke={color}
|
|
strokeWidth={2}
|
|
dot={{ fill: color, r: 3 }}
|
|
activeDot={{ r: 5, fill: color }}
|
|
isAnimationActive={true}
|
|
animationDuration={1000}
|
|
animationBegin={0}
|
|
/>
|
|
</LazyLineChart>
|
|
</ResponsiveContainer>
|
|
</Suspense>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default MiniSparkline;
|