Untitled
unknown
plain_text
3 months ago
17 kB
4
Indexable
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, startOfDay, endOfDay } from "date-fns" import { AreaChart, Area, LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Scatter } 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { CalendarIcon, X } from "lucide-react" import { Calendar } from "@/components/ui/calendar" import { useRouter, useSearchParams } from "next/navigation" import { useTranslation, type SupportedLanguages } from "@/utils/translations" const binColors = { bin1: "hsl(var(--chart-1))", bin2: "hsl(var(--chart-2))", bin3: "hsl(var(--chart-3))", bin4: "hsl(var(--chart-4))", } const timeRanges = [ { label: "allTime", value: 9999999 }, { label: "last24h", value: 1 }, { label: "lastWeek", value: 7 }, { label: "lastMonth", value: 30 }, { label: "lastYear", value: 365 }, { label: "custom", value: "custom" }, ] export function HistoricalFillChart() { const [selectedBin, setSelectedBin] = useState("all") const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(9999999) const [customDateRange, setCustomDateRange] = useState<{ from?: Date; to?: Date }>({}) const [isCalendarOpen, setIsCalendarOpen] = useState(false) const router = useRouter() const searchParams = useSearchParams() const { metrics, helpers, loading, error } = useMetrics() const { t } = useTranslation() const activeBins = useMemo(() => { 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 || binFromUrl === "all") { setSelectedBin("all") } else if (activeBins.includes(binFromUrl)) { setSelectedBin(binFromUrl) } }, [searchParams, activeBins]) const formatBinName = (binId: string) => { const binNumber = parseInt(binId.replace('bin', '')) return `${t('historicalFillChart.binPrefix')} ${binNumber}` } const handleBinChange = (value: string) => { setSelectedBin(value) if (value === "all") { router.push("?bin=all") } else { router.push(`?bin=${value}`) } } const chartData = useMemo(() => { if (!helpers) return [] let startDate = new Date() let endDate = new Date() if (selectedTimeRange === "custom" && customDateRange.from && customDateRange.to) { startDate = startOfDay(customDateRange.from) endDate = endOfDay(customDateRange.to) } else if (typeof selectedTimeRange === "number") { startDate = subDays(endDate, selectedTimeRange) } const timeFilter = { start: startDate, end: endDate } const emptyingEvents = helpers.getEmptyingEvents(selectedBin === "all" ? undefined : selectedBin, timeFilter) if (selectedBin === "all") { const allFillData = activeBins.flatMap(bin => helpers.getFillLevelHistory(bin, timeFilter).map(item => ({ date: parseISO(item.timestamp), binId: bin, fillLevel: item.fill_level, isEmptied: emptyingEvents.some(e => e.bin_id === bin && parseISO(e.timestamp).getTime() === parseISO(item.timestamp).getTime() ) })) ) allFillData.sort((a, b) => a.date.getTime() - b.date.getTime()) const lastValues = activeBins.reduce((acc, bin) => { acc[bin] = null return acc }, {} as Record<string, number | null>) const normalizedData: Record<number, any> = {} allFillData.forEach(item => { const timestamp = item.date.getTime() if (!normalizedData[timestamp]) { normalizedData[timestamp] = { date: item.date, ...activeBins.reduce((acc, bin) => { acc[bin] = lastValues[bin] !== null ? lastValues[bin] : null return acc }, {} as Record<string, any>) } } normalizedData[timestamp][item.binId] = item.fillLevel lastValues[item.binId] = item.fillLevel }) const sortedTimestamps = Object.keys(normalizedData) .map(Number) .sort((a, b) => a - b) return sortedTimestamps.map(t => normalizedData[t]) } const fillData = helpers.getFillLevelHistory(selectedBin, timeFilter) return fillData.map(item => ({ date: parseISO(item.timestamp), fillLevel: item.fill_level, emptied: emptyingEvents.some(e => e.bin_id === selectedBin && parseISO(e.timestamp).getTime() === parseISO(item.timestamp).getTime() ) })) }, [helpers, selectedBin, selectedTimeRange, customDateRange, activeBins]) const chartConfig: ChartConfig = selectedBin === "all" ? { bin1: { label: t('historicalFillChart.binPrefix') + " 1", color: binColors.bin1 }, bin2: { label: t('historicalFillChart.binPrefix') + " 2", color: binColors.bin2 }, bin3: { label: t('historicalFillChart.binPrefix') + " 3", color: binColors.bin3 }, bin4: { label: t('historicalFillChart.binPrefix') + " 4", color: binColors.bin4 }, } : { fillLevel: { label: formatBinName(selectedBin), color: binColors[selectedBin as keyof typeof binColors] }, } 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('historicalFillChart.title')}</CardTitle> <CardDescription>{t('historicalFillChart.description')}</CardDescription> </CardHeader> <CardContent> <div className="flex flex-wrap gap-4 mb-4"> <Select value={selectedBin} onValueChange={handleBinChange}> <SelectTrigger className="w-[140px]"> <SelectValue placeholder={t('historicalFillChart.selectBin')}> {selectedBin === 'all' ? t('historicalFillChart.allBins') : formatBinName(selectedBin)} </SelectValue> </SelectTrigger> <SelectContent> <SelectItem value="all">{t('historicalFillChart.allBins')}</SelectItem> {activeBins.map(bin => ( <SelectItem key={bin} value={bin}> {formatBinName(bin)} </SelectItem> ))} </SelectContent> </Select> <Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}> <SelectTrigger className="w-[140px]"> <SelectValue placeholder={t('timeRanges.allTime')} /> </SelectTrigger> <SelectContent> {timeRanges.map(range => ( <SelectItem key={range.value} value={range.value.toString()}> {t(`timeRanges.${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> </CardContent> <CardContent> <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full"> <ResponsiveContainer width="100%" height="100%"> {selectedBin === "all" ? ( <LineChart data={chartData}> <CartesianGrid vertical={false} /> <XAxis dataKey="date" tickFormatter={(date) => format(date, "MMM dd")} minTickGap={40} 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 HH:mm") const entries = activeBins.map(bin => ({ dataKey: bin, value: payload[0].payload[bin], color: binColors[bin as keyof typeof binColors] })).filter(e => e.value !== null) 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> {entries.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 />} /> {activeBins.map(bin => ( <Line key={bin} type="monotone" dataKey={bin} stroke={binColors[bin as keyof typeof binColors]} strokeWidth={2} dot={false} connectNulls /> ))} </LineChart> ) : ( <AreaChart data={chartData}> <CartesianGrid vertical={false} /> <XAxis dataKey="date" tickFormatter={(date) => format(date, "MMM dd")} minTickGap={40} 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 HH:mm") const entries = payload.filter(p => p.dataKey === "fillLevel") 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> {entries.map((entry) => ( <div key={entry.dataKey} className="flex items-center gap-2"> <div className="h-2 w-2 rounded-full" style={{ background: binColors[selectedBin as keyof typeof binColors] }} /> <span className="text-sm text-muted-foreground"> {chartConfig.fillLevel.label}: </span> <span className="text-sm font-medium"> {entry.value?.toFixed(0)}% </span> </div> ))} </div> </div> ) }} /> <ChartLegend content={<ChartLegendContent />} /> <defs> <linearGradient id="fillGradient" x1="0" y1="0" x2="0" y2="1"> <stop offset="5%" stopColor={binColors[selectedBin as keyof typeof binColors]} stopOpacity={0.8} /> <stop offset="95%" stopColor={binColors[selectedBin as keyof typeof binColors]} stopOpacity={0.1} /> </linearGradient> </defs> <Area type="monotone" dataKey="fillLevel" stroke={binColors[selectedBin as keyof typeof binColors]} fill="url(#fillGradient)" fillOpacity={0.4} strokeWidth={2} dot={false} connectNulls /> <Scatter data={chartData.filter((d: any) => d.emptied)} dataKey="fillLevel" fill="var(--chart-5)" shape={(props) => ( <circle {...props} r={6} strokeWidth={2} stroke="var(--background)" /> )} /> </AreaChart> )} </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment