Untitled
unknown
plain_text
2 months ago
11 kB
3
Indexable
/components/historical-fill-chart.tsx "use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, isAfter } from "date-fns" import { 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 { 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 { 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" }, ] const binColors = { bin1: "var(--chart-1)", bin2: "var(--chart-2)", bin3: "var(--chart-3)", bin4: "var(--chart-4)", } export function HistoricalFillChart() { const [isCalendarOpen, setIsCalendarOpen] = useState(false) const searchParams = useSearchParams() const [selectedBins, setSelectedBins] = useState<string>("all") 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 activeBinIds = useMemo(() => { if (!helpers) return [] return helpers.getActiveBinIds() }, [helpers]) useEffect(() => { const binFromUrl = searchParams.get("bin") if (binFromUrl && activeBinIds.includes(binFromUrl)) { setSelectedBins(binFromUrl) } else if (activeBinIds.length > 0) { setSelectedBins("all") } }, [searchParams, activeBinIds]) const { chartData, emptyingEvents } = useMemo(() => { if (!helpers) return { chartData: [], emptyingEvents: [] } 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(undefined, { start: startDate, end: endDate }) const emptyingData = helpers.getEmptyingEvents(undefined, { start: startDate, end: endDate }) const formattedData = historicalData.reduce((acc, item) => { const dateKey = format(parseISO(item.timestamp), "yyyy-MM-dd'T'HH:mm:ss") if (!acc[dateKey]) { acc[dateKey] = { date: parseISO(item.timestamp), [item.bin_id]: item.fill_level, } } else { acc[dateKey][item.bin_id] = item.fill_level } return acc }, {} as Record<string, any>) return { chartData: Object.values(formattedData).sort((a, b) => a.date - b.date), emptyingEvents: emptyingData.map(event => ({ date: parseISO(event.timestamp), bin_id: event.bin_id, fill_level: event.fill_level, })) } }, [helpers, selectedTimeRange, customDateRange]) const chartConfig = useMemo<ChartConfig>(() => { const config: ChartConfig = {} const binsToShow = selectedBins === "all" ? activeBinIds : [selectedBins] binsToShow.forEach(binId => { config[binId] = { label: binId.toUpperCase(), color: binColors[binId as keyof typeof binColors], } }) return config }, [selectedBins, activeBinIds]) 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>Historical Fill Levels</CardTitle> <CardDescription>Fill level trends with emptying events</CardDescription> </CardHeader> <CardContent> <div className="flex flex-wrap gap-4 mb-4"> <Select value={selectedBins} onValueChange={setSelectedBins}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Select bins" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Bins</SelectItem> {activeBinIds.map((binId) => ( <SelectItem key={binId} value={binId}> {binId.toUpperCase()} </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={setIsCalendarOpen}> <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> {(customDateRange.from || customDateRange.to) && ( <div className="ml-2" onClick={(e) => { e.stopPropagation() 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" 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" 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 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) => ( entry.dataKey !== 'date' && ( <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 />} /> {Object.keys(chartConfig).map((binId) => ( <Line key={binId} type="monotone" dataKey={binId} stroke={chartConfig[binId].color} strokeWidth={2} dot={false} connectNulls /> ))} <Scatter name="Emptying Events" data={emptyingEvents} fill="#ff7300" shape={(props) => ( <circle {...props} r={6} fill={binColors[props.payload.bin_id as keyof typeof binColors]} stroke="#fff" strokeWidth={2} /> )} /> </LineChart> </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment