Files
paragliding/frontend/components/weather/wind-speed-chart.tsx
2026-01-03 14:16:16 -08:00

201 lines
6.2 KiB
TypeScript

'use client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
ResponsiveContainer,
ReferenceLine,
Legend,
Area,
ComposedChart,
Tooltip,
} from 'recharts'
import { format, parseISO } from 'date-fns'
import type { WeatherPoint } from '@/lib/types'
import { useThresholdStore } from '@/store/threshold-store'
interface WindSpeedChartProps {
data: WeatherPoint[]
yesterdayData?: WeatherPoint[]
className?: string
}
interface ChartDataPoint {
timestamp: string
time: string
speed?: number
yesterdaySpeed?: number
isInRange: boolean
}
export function WindSpeedChart({ data, yesterdayData, className }: WindSpeedChartProps) {
const { speedMin, speedMax } = useThresholdStore()
// Filter data for TODAY's 8am-10pm window only
const filterTimeWindow = (points: WeatherPoint[]) => {
const now = new Date()
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0)
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 22, 0, 0)
return points.filter((point) => {
const timestamp = parseISO(point.timestamp)
return timestamp >= todayStart && timestamp < todayEnd
})
}
// Generate static time slots for 8am-10pm (14 hours)
const generateTimeSlots = (): { hour: number; label: string }[] => {
const slots = []
for (let hour = 8; hour < 22; hour++) {
slots.push({
hour,
label: format(new Date().setHours(hour, 0, 0, 0), 'ha')
})
}
return slots
}
const filteredData = filterTimeWindow(data)
// Don't filter yesterday's data - show all available historical data
const filteredYesterday = yesterdayData || []
// Generate static time slots and map data to them
const timeSlots = generateTimeSlots()
const chartData: ChartDataPoint[] = timeSlots.map(slot => {
// Find forecast data for this hour
const forecastPoint = filteredData.find(point =>
parseISO(point.timestamp).getHours() === slot.hour
)
// Find yesterday's data for this hour
const yesterdayPoint = filteredYesterday.find(yp =>
parseISO(yp.timestamp).getHours() === slot.hour
)
return {
timestamp: forecastPoint?.timestamp || '',
time: slot.label,
speed: forecastPoint?.wind_speed,
yesterdaySpeed: yesterdayPoint?.wind_speed,
isInRange: forecastPoint ?
(forecastPoint.wind_speed >= speedMin && forecastPoint.wind_speed <= speedMax) :
false
}
})
// Custom tooltip
const CustomTooltip = ({ active, payload }: any) => {
if (!active || !payload || !payload.length) return null
const data = payload[0].payload
// Don't show tooltip if there's no forecast data for this time slot
if (data.speed === undefined) return null
return (
<div className="rounded-lg border bg-background p-3 shadow-md">
<p className="font-medium">{data.time}</p>
<p className="text-sm">
<span className="font-medium">Forecast:</span> {data.speed.toFixed(1)} mph
</p>
{data.yesterdaySpeed !== undefined && (
<p className="text-sm text-muted-foreground">
<span className="font-medium">Yesterday:</span> {data.yesterdaySpeed.toFixed(1)} mph
</p>
)}
</div>
)
}
return (
<Card className={className}>
<CardHeader>
<CardTitle>Wind Speed</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<ComposedChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="speedRangeGood" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
<stop offset="95%" stopColor="#22c55e" stopOpacity={0.1} />
</linearGradient>
<linearGradient id="speedRangeBad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.3} />
<stop offset="95%" stopColor="#ef4444" stopOpacity={0.1} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis
dataKey="time"
tick={{ fontSize: 12 }}
interval={0}
angle={0}
height={40}
/>
<YAxis
label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }}
tick={{ fontSize: 12 }}
/>
<Tooltip content={<CustomTooltip />} />
<Legend />
{/* Shaded area for acceptable speed range (green) */}
<Area
type="monotone"
dataKey={() => speedMax}
fill="url(#speedRangeGood)"
stroke="none"
fillOpacity={0.3}
activeDot={false}
/>
{/* Threshold reference lines */}
<ReferenceLine
y={speedMin}
stroke="#22c55e"
strokeDasharray="3 3"
label={{ value: `Min: ${speedMin}`, fontSize: 11, fill: '#22c55e' }}
/>
<ReferenceLine
y={speedMax}
stroke="#22c55e"
strokeDasharray="3 3"
label={{ value: `Max: ${speedMax}`, fontSize: 11, fill: '#22c55e' }}
/>
{/* Yesterday's data (faded) */}
{yesterdayData && (
<Line
type="monotone"
dataKey="yesterdaySpeed"
stroke="#9ca3af"
strokeWidth={1}
name="Yesterday"
dot={false}
activeDot={true}
isAnimationActive={false}
/>
)}
{/* Today's forecast - single continuous line */}
<Line
type="monotone"
dataKey="speed"
stroke="#3b82f6"
strokeWidth={2}
name="Forecast"
dot={false}
activeDot={true}
/>
</ComposedChart>
</ResponsiveContainer>
</CardContent>
</Card>
)
}