mc-manager/src/components/StatsChart.tsx
2026-02-08 00:05:12 -08:00

128 lines
4.0 KiB
TypeScript

'use client'
import { useMemo } from 'react'
interface StatsChartProps {
data: number[]
maxDataPoints?: number
height?: number
label: string
value: string
subValue?: string
color: 'cyan' | 'emerald' | 'amber' | 'red'
maxY?: number
unit?: string
}
const COLORS = {
cyan: { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.15)', text: 'text-cyan-400', bg: 'bg-cyan-500/20' },
emerald: { stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.15)', text: 'text-emerald-400', bg: 'bg-emerald-500/20' },
amber: { stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.15)', text: 'text-amber-400', bg: 'bg-amber-500/20' },
red: { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.15)', text: 'text-red-400', bg: 'bg-red-500/20' },
}
export default function StatsChart({
data,
maxDataPoints = 60,
height = 120,
label,
value,
subValue,
color,
maxY = 100,
unit = '%',
}: StatsChartProps) {
const theme = COLORS[color]
const { linePath, areaPath } = useMemo(() => {
if (data.length < 2) return { linePath: '', areaPath: '' }
const w = 400
const h = height
const padding = 2
const effectiveH = h - padding * 2
const clampedMax = Math.max(maxY, 1)
const points = data.map((val, i) => {
const x = (i / (maxDataPoints - 1)) * w
const y = padding + effectiveH - (Math.min(val, clampedMax) / clampedMax) * effectiveH
return { x, y }
})
// Smooth the line with quadratic bezier curves
let line = `M ${points[0].x},${points[0].y}`
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1]
const curr = points[i]
const cpx = (prev.x + curr.x) / 2
line += ` Q ${cpx},${prev.y} ${curr.x},${curr.y}`
}
const lastPoint = points[points.length - 1]
const firstPoint = points[0]
const area = `${line} L ${lastPoint.x},${h} L ${firstPoint.x},${h} Z`
return { linePath: line, areaPath: area }
}, [data, maxDataPoints, height, maxY])
return (
<div className="glass rounded-xl p-4 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${theme.bg}`}>
<div className={`w-2 h-2 rounded-full animate-pulse`} style={{ backgroundColor: theme.stroke }} />
</div>
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">{label}</span>
</div>
<div className="text-right">
<span className={`text-lg font-semibold ${theme.text}`}>{value}</span>
{subValue && (
<span className="text-xs text-gray-500 ml-1">{subValue}</span>
)}
</div>
</div>
{/* Chart */}
<div className="relative" style={{ height }}>
{data.length < 2 ? (
<div className="flex items-center justify-center h-full">
<span className="text-xs text-gray-600">Collecting data</span>
</div>
) : (
<svg
viewBox={`0 0 400 ${height}`}
preserveAspectRatio="none"
className="w-full h-full"
>
{/* Grid lines */}
{[0, 0.25, 0.5, 0.75, 1].map((pct) => (
<line
key={pct}
x1={0}
y1={2 + (height - 4) * pct}
x2={400}
y2={2 + (height - 4) * pct}
stroke="rgba(55, 65, 81, 0.3)"
strokeWidth={0.5}
/>
))}
{/* Area fill */}
<path d={areaPath} fill={theme.fill} />
{/* Line */}
<path d={linePath} fill="none" stroke={theme.stroke} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
{/* Y-axis labels */}
<div className="absolute top-0 left-0 h-full flex flex-col justify-between pointer-events-none py-0.5">
<span className="text-[10px] text-gray-600">{maxY}{unit}</span>
<span className="text-[10px] text-gray-600">0{unit}</span>
</div>
</div>
</div>
)
}