Untitled
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, addDays, isAfter } from "date-fns" import { CartesianGrid, Line, LineChart, XAxis, YAxis, ResponsiveContainer } from "recharts" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { ChartLegend, ChartLegendContent, ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart" import { ChartEmptyState } from "@/components/ui/chart-empty-state" import { useMetrics } from "@/hooks/useMetrics" import { useFillPredictions } from "@/hooks/useFillPredictions" import { Calendar } from "@/components/ui/calendar" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { CalendarIcon } from "lucide-react" import { useRouter, useSearchParams } from "next/navigation" const timeRanges = [ { label: "Last 7 days", value: 7 }, { label: "Last 30 days", value: 30 }, { label: "Last 90 days", value: 90 }, { label: "Last 180 days", value: 180 }, { label: "Last 365 days", value: 365 }, { label: "Custom", value: "custom" }, ] export function EstimateFillLevelChart() { const router = useRouter() const searchParams = useSearchParams() const [selectedBin, setSelectedBin] = useState<string | null>(null) const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(30) const [customDateRange, setCustomDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({ from: undefined, to: undefined, }) const { metrics, helpers, loading, error } = useMetrics() const { predictions } = useFillPredictions() const activeBinIds = useMemo(() => { if (!helpers) return [] return helpers.getActiveBinIds() }, [helpers]) useEffect(() => { const binFromUrl = searchParams.get("bin") if (binFromUrl && activeBinIds.includes(binFromUrl)) { setSelectedBin(binFromUrl) } else if (activeBinIds.length > 0) { setSelectedBin(activeBinIds[0]) } }, [searchParams, activeBinIds]) const chartData = useMemo(() => { if (!helpers || !selectedBin) return [] let startDate: Date let endDate = new Date() if (selectedTimeRange === "custom" && customDateRange.from && customDateRange.to) { startDate = customDateRange.from endDate = customDateRange.to } else if (typeof selectedTimeRange === "number") { startDate = subDays(endDate, selectedTimeRange) } else { startDate = subDays(endDate, 30) // Default to 30 days if something goes wrong } const historicalData = helpers.getFillLevelHistory(selectedBin, { start: startDate, end: endDate }) const prediction = predictions.find((p) => p.binId === selectedBin) const data = historicalData.map((item) => ({ date: parseISO(item.timestamp), fillLevel: item.fill_level, })) if (prediction && data.length > 0) { const lastDataPoint = data[data.length - 1] const predictedFullDate = new Date(prediction.predictedFullTime) if (isAfter(predictedFullDate, lastDataPoint.date)) { const daysUntilFull = (predictedFullDate.getTime() - lastDataPoint.date.getTime()) / (1000 * 60 * 60 * 24) const fillRatePerDay = (100 - lastDataPoint.fillLevel) / daysUntilFull // Add the prediction starting from the last historical point data.push({ date: lastDataPoint.date, fillLevel: lastDataPoint.fillLevel, predictedFillLevel: lastDataPoint.fillLevel }) // Then add the future predictions for (let i = 1; i <= daysUntilFull; i++) { const predictedDate = addDays(lastDataPoint.date, i) const predictedFillLevel = Math.min(lastDataPoint.fillLevel + fillRatePerDay * i, 100) data.push({ date: predictedDate, predictedFillLevel: predictedFillLevel, }) } } } return data }, [helpers, selectedBin, selectedTimeRange, customDateRange, predictions]) const chartConfig: ChartConfig = { fillLevel: { label: "Fill Level", color: "hsl(var(--chart-1))", }, predictedFillLevel: { label: "Predicted Fill Level", color: "hsl(var(--chart-2))", }, } const handleBinChange = (value: string) => { setSelectedBin(value) router.push(`?bin=${value}`) } const handleTimeRangeChange = (value: string) => { setSelectedTimeRange(value === "custom" ? "custom" : Number.parseInt(value)) } if (loading) return <div>Loading...</div> if (error) return <div>Error: {error}</div> const hasHistoricalData = chartData.some(data => data.fillLevel !== undefined) const hasPredictionData = chartData.some(data => data.predictedFillLevel !== undefined) const renderChartContent = () => { if (!selectedBin) { return ( <ChartEmptyState title="No Bin Selected" description="Please select a bin to view its fill level data and predictions." /> ) } if (!hasHistoricalData && !hasPredictionData) { return ( <ChartEmptyState title="No Data Available" description="There is no historical data or predictions available for the selected time range." /> ) } if (!hasHistoricalData) { return ( <ChartEmptyState title="No Historical Data" description="There is no historical fill level data available for the selected time range." /> ) } if (!hasPredictionData) { return ( <ChartEmptyState title="No Prediction Data" description="There are no fill level predictions available for this bin." /> ) } return ( <ResponsiveContainer width="100%" height="100%"> <LineChart data={chartData}> <CartesianGrid vertical={false} /> <XAxis dataKey="date" tickFormatter={(value) => format(value, "MMM dd")} axisLine={false} tickLine={false} tickMargin={8} minTickGap={32} /> <YAxis domain={[0, 100]} axisLine={false} tickLine={false} tickFormatter={(value) => `${value}%`} /> <ChartTooltip content={<ChartTooltipContent />} /> <ChartLegend content={<ChartLegendContent />} /> <Line type="monotone" dataKey="fillLevel" stroke="var(--color-fillLevel)" strokeWidth={2} dot={false} name="Fill Level" connectNulls={false} /> <Line type="monotone" dataKey="predictedFillLevel" stroke="var(--color-predictedFillLevel)" strokeWidth={2} strokeDasharray="10 10" dot={false} name="Predicted Fill Level" connectNulls /> </LineChart> </ResponsiveContainer> ) } return ( <Card> <CardHeader> <CardTitle>Estimate Fill Level Chart</CardTitle> <CardDescription>Historical and predicted fill levels</CardDescription> </CardHeader> <CardContent> <div className="flex flex-wrap gap-4 mb-4"> <Select value={selectedBin || undefined} onValueChange={handleBinChange}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Select a bin" /> </SelectTrigger> <SelectContent> {activeBinIds.map((binId) => ( <SelectItem key={binId} value={binId}> {binId} </SelectItem> ))} </SelectContent> </Select> <Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Select time range" /> </SelectTrigger> <SelectContent> {timeRanges.map((range) => ( <SelectItem key={range.value} value={range.value.toString()}> {range.label} </SelectItem> ))} </SelectContent> </Select> {selectedTimeRange === "custom" && ( <Popover> <PopoverTrigger asChild> <Button variant="outline" className="w-[280px] justify-start text-left font-normal"> <CalendarIcon className="mr-2 h-4 w-4" /> {customDateRange.from ? ( customDateRange.to ? ( <> {format(customDateRange.from, "LLL dd, y")} - {format(customDateRange.to, "LLL dd, y")} </> ) : ( format(customDateRange.from, "LLL dd, y") ) ) : ( <span>Pick a date range</span> )} </Button> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar initialFocus mode="range" defaultMonth={customDateRange.from} selected={customDateRange} onSelect={setCustomDateRange} numberOfMonths={2} /> </PopoverContent> </Popover> )} </div> <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full"> {renderChartContent()} </ChartContainer> </CardContent> </Card> ) }
Leave a Comment