Untitled
unknown
plain_text
3 months ago
13 kB
4
Indexable
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, addDays, isAfter } from "date-fns" import { CartesianGrid, Area, AreaChart, 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, X } from "lucide-react" import { useRouter, useSearchParams } from "next/navigation" import { useTranslation, type SupportedLanguages } from "@/utils/translations" import { useSettings } from "@/hooks/useSettings" const formatBinName = (binId: string, binPrefix: string) => { const binNumber = parseInt(binId.replace('bin', '')) return `${binPrefix} ${binNumber}` } export function EstimateFillLevelChart() { const { settings } = useSettings() const { t } = useTranslation(settings?.language as SupportedLanguages || 'EN') const timeRanges = [ { label: t('timeRanges.allTime'), value: 9999999 }, { label: t('timeRanges.last24h'), value: 1 }, { label: t('timeRanges.lastWeek'), value: 7 }, { label: t('timeRanges.lastMonth'), value: 30 }, { label: t('timeRanges.lastYear'), value: 365 }, { label: t('timeRanges.custom'), value: "custom" }, ] const [isCalendarOpen, setIsCalendarOpen] = useState(false) const router = useRouter() const searchParams = useSearchParams() const [selectedBin, setSelectedBin] = useState<string | null>(null) const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(9999999) 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 [] const bins = helpers.getActiveBinIds() return [...bins].sort((a, b) => { const numA = parseInt(a.replace('bin', '')) const numB = parseInt(b.replace('bin', '')) return numA - numB }) }, [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) } 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 chartConfig: ChartConfig = useMemo(() => ({ fillLevel: { label: selectedBin ? `${formatBinName(selectedBin, t('estimatechart.binPrefix'))} - ${t('estimatechart.current')}` : t('estimatechart.fillLevel'), color: "hsl(var(--chart-1))", }, predictedFillLevel: { label: selectedBin ? `${formatBinName(selectedBin, t('estimatechart.binPrefix'))} - ${t('estimatechart.predicted')}` : t('estimatechart.fillLevel'), color: "hsl(var(--chart-2))", }, }), [selectedBin, t]) 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>{t('historicalFillChart.loading')}</div> if (error) return <div>{t('historicalFillChart.error')}{error}</div> return ( <Card> <CardHeader> <CardTitle>{t('estimatechart.title')}</CardTitle> <CardDescription>{t('estimatechart.description')}</CardDescription> </CardHeader> <CardContent> <div className="flex flex-wrap gap-4 mb-4"> <Select value={selectedBin || undefined} onValueChange={handleBinChange}> <SelectTrigger className="w-[140px]"> <SelectValue placeholder={t('estimatechart.selectBin')}> {selectedBin ? formatBinName(selectedBin, t('estimatechart.binPrefix')) : t('estimatechart.selectBin')} </SelectValue> </SelectTrigger> <SelectContent> {activeBinIds.map((binId) => ( <SelectItem key={binId} value={binId}> {formatBinName(binId, t('estimatechart.binPrefix'))} </SelectItem> ))} </SelectContent> </Select> <Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}> <SelectTrigger className="w-[140px]"> <SelectValue placeholder={t('estimatechart.selectTimeRange')} /> </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>{t('estimatechart.pickDateRange')}</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%"> <AreaChart 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={({ 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 />} /> <defs> <linearGradient id="fillLevel" x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor="var(--color-fillLevel)" stopOpacity={0.8} /> <stop offset="95%" stopColor="var(--color-fillLevel)" stopOpacity={0.1} /> </linearGradient> <linearGradient id="predictedFillLevel" x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor="var(--color-predictedFillLevel)" stopOpacity={0.8} /> <stop offset="95%" stopColor="var(--color-predictedFillLevel)" stopOpacity={0.1} /> </linearGradient> </defs> <Area type="monotone" dataKey="fillLevel" stroke="var(--color-fillLevel)" fill="url(#fillLevel)" fillOpacity={0.4} strokeWidth={2} dot={false} name="Fill Level" connectNulls={false} /> <Area type="monotone" dataKey="predictedFillLevel" stroke="var(--color-predictedFillLevel)" fill="url(#predictedFillLevel)" fillOpacity={0.4} strokeWidth={2} strokeDasharray="10 10" dot={false} name="Predicted Fill Level" connectNulls /> </AreaChart> </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment