Untitled
unknown
plain_text
14 days ago
8.8 kB
1
Indexable
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, addDays, isAfter, 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, 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 } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" const DEFAULT_RANGE = 30 const TIME_RANGES = [ { label: "Week", value: 7 }, { label: "Month", value: 30 }, { label: "Quarter", value: 90 }, { label: "Year", value: 365 }, { label: "Custom", value: "custom" }, ] export function EstimateFillLevelChart() { const { metrics, helpers } = useMetrics() const { predictions } = useFillPredictions() const [dateRange, setDateRange] = useState<{ start?: Date; end?: Date }>({}) const [selectedBin, setSelectedBin] = useState<string>() const [selectedRange, setSelectedRange] = useState<number | 'custom'>(DEFAULT_RANGE) // Bin selection handling const bins = useMemo(() => helpers?.getActiveBinIds() || [], [helpers]) useEffect(() => setSelectedBin(prev => bins.includes(prev!) ? prev : bins[0]), [bins]) // Date range calculation useEffect(() => { if (selectedRange === 'custom') return setDateRange({ start: subDays(new Date(), selectedRange), end: new Date() }) }, [selectedRange]) // Chart data processing const chartData = useMemo(() => { if (!selectedBin || !helpers || !dateRange.start || !dateRange.end) return [] const historical = helpers.getFillLevelHistory(selectedBin, dateRange) const prediction = predictions.find(p => p.binId === selectedBin) if (!historical.length) return [] const data = historical.map(d => ({ date: parseISO(d.timestamp), value: d.fill_level, type: 'historical' as const })) // Add prediction if available if (prediction) { const lastPoint = data[data.length - 1] const predDate = new Date(prediction.predictedFullTime) const daysRemaining = differenceInDays(predDate, lastPoint.date) if (daysRemaining > 0) { const dailyIncrease = (100 - lastPoint.value) / daysRemaining for (let i = 1; i <= daysRemaining; i++) { data.push({ date: addDays(lastPoint.date, i), value: Math.min(lastPoint.value + dailyIncrease * i, 100), type: 'prediction' as const }) } } } return data }, [selectedBin, helpers, predictions, dateRange]) // Chart configuration const chartConfig: ChartConfig = { historical: { label: "Actual Fill Level", color: "hsl(var(--chart-1))" }, prediction: { label: "Predicted Fill Level", color: "hsl(var(--chart-2))" } } if (!helpers || !selectedBin) { return ( <Card> <CardHeader> <Skeleton className="h-6 w-[200px]" /> <Skeleton className="h-4 w-[300px]" /> </CardHeader> <CardContent> <Skeleton className="h-[400px] w-full" /> </CardContent> </Card> ) } return ( <Card> <CardHeader className="pb-2"> <div className="flex items-center justify-between gap-4"> <div> <CardTitle>Fill Level Projection</CardTitle> <CardDescription> {selectedBin} - Historical and predicted fill levels </CardDescription> </div> <div className="flex gap-2"> <Select value={selectedBin} onValueChange={setSelectedBin}> <SelectTrigger className="w-[140px]"> <SelectValue /> </SelectTrigger> <SelectContent> {bins.map(bin => ( <SelectItem key={bin} value={bin}>{bin}</SelectItem> ))} </SelectContent> </Select> <Select value={selectedRange.toString()} onValueChange={v => setSelectedRange(v === 'custom' ? v : +v)}> <SelectTrigger className="w-[120px]"> <SelectValue /> </SelectTrigger> <SelectContent> {TIME_RANGES.map(({ label, value }) => ( <SelectItem key={value} value={value.toString()}>{label}</SelectItem> ))} </SelectContent> </Select> {selectedRange === 'custom' && ( <Popover> <PopoverTrigger asChild> <Button variant="outline" className="w-[240px] justify-start"> <CalendarIcon className="mr-2 h-4 w-4" /> {dateRange.start && dateRange.end ? ( `${format(dateRange.start, 'MMM dd')} - ${format(dateRange.end, 'MMM dd')}` ) : 'Select date range'} </Button> </PopoverTrigger> <PopoverContent className="w-auto p-0"> <Calendar mode="range" selected={{ from: dateRange.start, to: dateRange.end }} onSelect={(v) => v && setDateRange({ start: v.from, end: v.to })} numberOfMonths={2} /> </PopoverContent> </Popover> )} </div> </div> </CardHeader> <CardContent> <ChartContainer config={chartConfig} className="h-[400px]"> <ResponsiveContainer width="100%" height="100%"> <LineChart data={chartData}> <CartesianGrid vertical={false} strokeDasharray="3 3" /> <XAxis dataKey="date" tickFormatter={v => format(v, 'MMM dd')} axisLine={false} tickLine={false} minTickGap={40} /> <YAxis domain={[0, 100]} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} width={60} /> <ChartTooltip content={({ active, payload }) => ( <div className={cn( "rounded-lg border bg-popover p-3 shadow-sm", active && payload?.length ? 'opacity-100' : 'opacity-0' )}> {payload?.map((entry) => ( <div key={entry.name} className="flex items-center gap-2"> <div className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }} /> <div className="flex-1"> <div className="text-sm font-medium"> {format(entry.payload.date, 'MMM dd, yyyy')} </div> <div className="text-sm"> {entry.dataKey === 'prediction' && 'Projected '} Fill Level: {entry.value?.toFixed(1)}% </div> </div> </div> ))} </div> )}/> <Line type="monotone" dataKey="value" stroke="var(--color-historical)" strokeWidth={2} dot={false} name="Historical" connectNulls isAnimationActive={false} /> <Line type="monotone" dataKey="value" stroke="var(--color-prediction)" strokeWidth={2} strokeDasharray="4 4" dot={false} name="Prediction" connectNulls isAnimationActive={false} filter={({ type }) => type === 'prediction'} /> </LineChart> </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment