Untitled
unknown
plain_text
2 months ago
11 kB
3
Indexable
"use client" import { useState, useMemo } from "react" import { format } from "date-fns" import { CalendarIcon, X } from "lucide-react" import { CartesianGrid, Line, LineChart, XAxis, YAxis, ResponsiveContainer } from "recharts" import { useMetrics } from "@/hooks/useMetrics" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, } from "@/components/ui/chart" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { Calendar } from "@/components/ui/calendar" const periods = [ { label: "All Time", value: "all" }, { label: "Last 24h", value: "24h" }, { label: "Last Week", value: "week" }, { label: "Last Month", value: "month" }, { label: "Last Year", value: "year" }, { label: "Custom", value: "custom" }, ] export function HistoricalFillLevelChart() { const { metrics, helpers } = useMetrics() const [selectedPeriod, setSelectedPeriod] = useState("all") const [isCalendarOpen, setIsCalendarOpen] = useState(false) const [customDateRange, setCustomDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({ from: undefined, to: undefined, }) const handlePeriodChange = (value: string) => { setSelectedPeriod(value) if (value === "custom") { setTimeout(() => { setIsCalendarOpen(true) }, 0) } } const chartData = useMemo(() => { if (!metrics || !helpers) return [] const now = new Date() let timeFilter if (selectedPeriod === "all") { timeFilter = undefined } else if (selectedPeriod === "custom") { timeFilter = customDateRange.from && customDateRange.to ? { start: customDateRange.from, end: customDateRange.to } : undefined } else { let startDate = new Date(now) switch (selectedPeriod) { case "24h": startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000) break case "week": startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) break case "month": startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()) break case "year": startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate()) break } timeFilter = { start: startDate, end: now } } // Get historical data for all bins const bins = helpers.getActiveBinIds() const allData: Record<string, Array<{ date: Date; fillLevel: number; emptied?: boolean }>> = {} bins.forEach(binId => { const history = helpers.getFillLevelHistory(binId, timeFilter) const emptyingEvents = helpers.getEmptyingEvents(binId, timeFilter) allData[binId] = history.map(item => ({ date: new Date(item.timestamp), fillLevel: item.fill_level, emptied: emptyingEvents.some(event => Math.abs(new Date(event.timestamp).getTime() - new Date(item.timestamp).getTime()) < 1000 * 60 * 5 // Within 5 minutes ) })) }) // Combine all data points into a single array with timestamps as keys const combinedData: Array<{ date: Date } & Record<string, number | null>> = [] const allDates = new Set<string>() Object.entries(allData).forEach(([binId, data]) => { data.forEach(point => { allDates.add(point.date.toISOString()) }) }) Array.from(allDates).sort().forEach(dateStr => { const dataPoint: any = { date: new Date(dateStr) } bins.forEach(binId => { const point = allData[binId].find(p => p.date.toISOString() === dateStr) if (point) { dataPoint[binId] = point.fillLevel if (point.emptied) { dataPoint[`${binId}Emptied`] = point.fillLevel } } }) combinedData.push(dataPoint) }) return combinedData }, [metrics, selectedPeriod, helpers, customDateRange]) const chartConfig: ChartConfig = { bin1: { label: "Organic", color: "hsl(var(--chart-1))", }, bin2: { label: "Recycling", color: "hsl(var(--chart-2))", }, bin3: { label: "Paper", color: "hsl(var(--chart-3))", }, bin4: { label: "Residual", color: "hsl(var(--chart-4))", }, } return ( <Card> <CardHeader className="flex flex-row items-center justify-between gap-4"> <div> <CardTitle>Historical Fill Levels</CardTitle> <CardDescription>Fill level trends over time for all bins</CardDescription> </div> <div className="flex gap-2"> <Select value={selectedPeriod} onValueChange={handlePeriodChange}> <SelectTrigger className="w-[120px]"> <SelectValue placeholder="Period" /> </SelectTrigger> <SelectContent> {periods.map((period) => ( <SelectItem key={period.value} value={period.value}> {period.label} </SelectItem> ))} </SelectContent> </Select> {selectedPeriod === "custom" && ( <Popover open={isCalendarOpen} onOpenChange={(open) => { if (!open && selectedPeriod === "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> {selectedPeriod === "custom" && (customDateRange.from || customDateRange.to) && ( <div className="ml-2" onClick={(e) => { e.stopPropagation() setSelectedPeriod("all") 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> </CardHeader> <CardContent> <ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full"> <ResponsiveContainer width="100%" height="100%"> <LineChart data={chartData}> <CartesianGrid vertical={false} /> <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => format(value, "MMM dd")} minTickGap={32} /> <YAxis domain={[0, 100]} tickLine={false} axisLine={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") 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) => { if (!entry.dataKey.includes("Emptied")) { return ( <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> ) } return null })} </div> </div> ) }} /> <ChartLegend content={<ChartLegendContent />} /> {Object.keys(chartConfig).map((binId) => ( <Line key={binId} type="monotone" dataKey={binId} stroke={`var(--color-${binId})`} strokeWidth={2} dot={false} connectNulls /> ))} {/* Emptying event markers */} {Object.keys(chartConfig).map((binId) => ( <Line key={`${binId}Emptied`} type="monotone" dataKey={`${binId}Emptied`} stroke={`var(--color-${binId})`} strokeWidth={0} dot={{ r: 4, fill: "var(--background)", stroke: `var(--color-${binId})`, strokeWidth: 2, }} activeDot={false} connectNulls={false} /> ))} </LineChart> </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment