import { useMemo } from 'react'; import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; interface VitalGaugeProps { label: string; value: number; unit: string; optimal: number; min?: number; max?: number; history?: number[]; // Last N readings for sparkline decimals?: number; showTrend?: boolean; } // Generate mock history if not provided const generateMockHistory = (current: number, optimal: number, points: number = 12): number[] => { const history: number[] = []; const variance = optimal * 0.15; for (let i = 0; i < points; i++) { // Trend towards current value const progress = i / (points - 1); const base = optimal + (current - optimal) * progress; const noise = (Math.random() - 0.5) * variance; history.push(base + noise); } return history; }; export function VitalGauge({ label, value, unit, optimal, min, max, history, decimals = 0, showTrend = true }: VitalGaugeProps) { // Generate history if not provided const dataPoints = useMemo(() => { return history || generateMockHistory(value, optimal); }, [history, value, optimal]); // Calculate how close to optimal (0 = far, 1 = optimal) const diff = Math.abs(value - optimal); const maxDiff = optimal * 0.3; const score = Math.max(0, 1 - diff / maxDiff); // Determine status color const status = score > 0.7 ? 'good' : score > 0.4 ? 'warning' : 'critical'; const colorClasses = { good: { text: 'text-green-400', bg: 'bg-green-500', stroke: '#22c55e' }, warning: { text: 'text-yellow-400', bg: 'bg-yellow-500', stroke: '#eab308' }, critical: { text: 'text-red-400', bg: 'bg-red-500', stroke: '#ef4444' }, }; const colors = colorClasses[status]; // Calculate trend from history const trend = useMemo(() => { if (dataPoints.length < 2) return 'stable'; const recentHalf = dataPoints.slice(-Math.floor(dataPoints.length / 2)); const olderHalf = dataPoints.slice(0, Math.floor(dataPoints.length / 2)); const recentAvg = recentHalf.reduce((a, b) => a + b, 0) / recentHalf.length; const olderAvg = olderHalf.reduce((a, b) => a + b, 0) / olderHalf.length; const change = (recentAvg - olderAvg) / olderAvg; if (change > 0.05) return 'up'; if (change < -0.05) return 'down'; return 'stable'; }, [dataPoints]); // SVG sparkline dimensions const sparkWidth = 50; const sparkHeight = 16; const padding = 1; // Generate sparkline path const sparklinePath = useMemo(() => { if (dataPoints.length < 2) return ''; const minVal = min ?? Math.min(...dataPoints) * 0.9; const maxVal = max ?? Math.max(...dataPoints) * 1.1; const range = maxVal - minVal || 1; const points = dataPoints.map((val, i) => { const x = (i / (dataPoints.length - 1)) * (sparkWidth - padding * 2) + padding; const y = sparkHeight - padding - ((val - minVal) / range) * (sparkHeight - padding * 2); return `${x},${y}`; }); return `M${points.join(' L')}`; }, [dataPoints, min, max]); // Gradient fill path (area under line) const areaPath = useMemo(() => { if (!sparklinePath) return ''; return `${sparklinePath} L${sparkWidth - padding},${sparkHeight - padding} L${padding},${sparkHeight - padding} Z`; }, [sparklinePath]); return (
{/* Label */}
{label}
{/* Value with trend indicator */}
{decimals > 0 ? value.toFixed(decimals) : Math.round(value)} {unit} {/* Trend arrow */} {showTrend && ( {trend === 'up' && } {trend === 'down' && } {trend === 'stable' && } )}
{/* Sparkline */} {/* Gradient definition */} {/* Area fill */} {/* Line */} {/* Current value dot */} {dataPoints.length > 0 && ( { const minVal = min ?? Math.min(...dataPoints) * 0.9; const maxVal = max ?? Math.max(...dataPoints) * 1.1; const range = maxVal - minVal || 1; return sparkHeight - padding - ((dataPoints[dataPoints.length - 1] - minVal) / range) * (sparkHeight - padding * 2); })()} r="2" fill={colors.stroke} className="animate-pulse" /> )} {/* Optimal indicator bar */}
{/* Score fill */}
{/* Optimal marker */}
{/* Tooltip on hover */}
Optimal: {optimal}{unit} • Range: {dataPoints.length > 0 ? Math.min(...dataPoints).toFixed(decimals) : '?'} - {dataPoints.length > 0 ? Math.max(...dataPoints).toFixed(decimals) : '?'}
); } // Compact version for lists export function VitalGaugeMini({ value, optimal, status }: { value: number; optimal: number; status?: 'good' | 'warning' | 'critical' }) { const diff = Math.abs(value - optimal); const maxDiff = optimal * 0.3; const score = Math.max(0, 1 - diff / maxDiff); const computed = status || (score > 0.7 ? 'good' : score > 0.4 ? 'warning' : 'critical'); const colorClasses = { good: 'bg-green-500', warning: 'bg-yellow-500', critical: 'bg-red-500', }; return (
); } // Group of vitals for a plant/section interface VitalsGridProps { health?: number; temperature?: number; humidity?: number; vpd?: number; compact?: boolean; } export function VitalsGrid({ health = 85, temperature = 76, humidity = 60, vpd = 1.2, compact = false }: VitalsGridProps) { if (compact) { return (
); } return (
); }