mirror of
https://github.com/rmoren97/mc-manager.git
synced 2026-03-28 17:26:47 -07:00
128 lines
4.0 KiB
TypeScript
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>
|
|
)
|
|
}
|