Untitled
unknown
plain_text
2 months ago
12 kB
3
Indexable
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, addDays, isAfter, eachDayOfInterval, differenceInDays } 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 { 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, X } from "lucide-react" import { useRouter, useSearchParams } from "next/navigation" const timeRanges = [ { label: "All Time", value: 9999999 }, { label: "Last 24h", value: 1 }, { label: "Last Week", value: 7 }, { label: "Last Month", value: 30 }, { label: "Last Year", value: 365 }, { label: "Custom", value: "custom" }, ] export function EstimateFillLevelChart() { const [isCalendarOpen, setIsCalendarOpen] = useState(false) 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 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 getTickInterval = (data: Array<{ date: Date }>) => { if (!data.length) return 1 const daysDiff = differenceInDays(data[data.length - 1].date, data[0].date) if (daysDiff > 60) return 7 if (daysDiff > 30) return 3 if (daysDiff > 14) return 2 return 1 } 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) => { const newRange = value === "custom" ? "custom" : Number.parseInt(value) setSelectedTimeRange(newRange) if (newRange === "custom") { setTimeout(() => { setIsCalendarOpen(true) }, 0) } } if (loading) return <div>Loading...</div> if (error) return <div>Error: {error}</div> 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 open={isCalendarOpen} onOpenChange={(open) => { if (!open && selectedTimeRange === "custom") return setIsCalendarOpen(open) }} > <PopoverTrigger asChild> <Button variant="outline" className="w-[280px] justify-start text-left font-normal"> <div className="flex items-center justify-between w-full"> <div className="flex items-center"> <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> )} </div> {selectedTimeRange === "custom" && (customDateRange.from || customDateRange.to) && ( <div className="ml-2" onClick={(e) => { e.stopPropagation() setSelectedTimeRange(9999999) setCustomDateRange({ from: undefined, to: undefined }) }} > <X className="h-4 w-4 text-muted-foreground hover:text-foreground" /> </div> )} </div> </Button> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar initialFocus mode="range" defaultMonth={customDateRange.from} selected={customDateRange} onSelect={(range) => { setCustomDateRange(range) if (range?.from && range?.to) { setIsCalendarOpen(false) } }} numberOfMonths={2} /> </PopoverContent> </Popover> )} </div> <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full"> <ResponsiveContainer width="100%" height="100%"> <LineChart data={chartData}> <CartesianGrid vertical={false} /> <XAxis dataKey="date" type="number" domain={['dataMin', 'dataMax']} scale="time" tickFormatter={(value) => { if (!(value instanceof Date)) { value = new Date(value) } return format(value, "MMM dd") }} interval={getTickInterval(chartData)} axisLine={false} tickLine={false} tickMargin={8} /> <YAxis domain={[0, 100]} axisLine={false} tickLine={false} tickFormatter={(value) => `${value} %`} /> <ChartTooltip content={({ active, payload }) => { if (!active || !payload?.length) return null const date = format(payload[0].payload.date, "MMM dd, yyyy") return ( <div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="grid gap-2"> <div className="text-sm font-medium">{date}</div> {payload.map((entry) => ( <div key={entry.dataKey} className="flex items-center gap-2"> <div className="h-2 w-2 rounded-full" style={{ background: entry.color }} /> <span className="text-sm text-muted-foreground"> {chartConfig[entry.dataKey].label}: </span> <span className="text-sm font-medium"> {entry.value.toFixed(0)}% </span> </div> ))} </div> </div> ) }} /> <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> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment