201 lines
6.2 KiB
TypeScript
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>
|
|
)
|
|
}
|