Untitled
unknown
plain_text
3 months ago
14 kB
6
Indexable
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, differenceInHours } from "date-fns" import { CartesianGrid, Line, LineChart, XAxis, YAxis, 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" 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 binOptions = ['all', 'bin1', 'bin2', 'bin3', 'bin4'] // Helper function to create continuous data points const createContinuousData = (historicalData: any[], timeThreshold = 6) => { if (historicalData.length === 0) return [] const sortedData = historicalData.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ) const continuousData = [] let lastValue = sortedData[0].fill_level for (let i = 0; i < sortedData.length; i++) { const current = sortedData[i] const currentDate = parseISO(current.timestamp) if (i > 0) { const prevDate = parseISO(sortedData[i - 1].timestamp) const hoursDiff = differenceInHours(currentDate, prevDate) if (hoursDiff > timeThreshold) { // Add intermediate points maintaining the last known value for (let j = 1; j < hoursDiff / timeThreshold; j++) { const intermediateDate = new Date(prevDate.getTime() + (j * timeThreshold * 60 * 60 * 1000)) continuousData.push({ timestamp: intermediateDate.toISOString(), fill_level: lastValue, isInterpolated: true }) } } } continuousData.push(current) lastValue = current.fill_level } return continuousData } export function HistoricalFillLevelChart() { const [isCalendarOpen, setIsCalendarOpen] = useState(false) const [selectedBin, setSelectedBin] = 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 chartData = useMemo(() => { if (!helpers) 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 binsToShow = selectedBin === "all" ? ['bin1', 'bin2', 'bin3', 'bin4'] : [selectedBin] const dataMap = new Map<string, any>() binsToShow.forEach(binId => { const historicalData = helpers.getFillLevelHistory(binId, { start: startDate, end: endDate }) const continuousData = createContinuousData(historicalData) continuousData.forEach(item => { const date = parseISO(item.timestamp) const dateKey = date.toISOString() const existing = dataMap.get(dateKey) || { date } dataMap.set(dateKey, { ...existing, [binId]: item.fill_level, [`${binId}_isInterpolated`]: item.isInterpolated, }) }) // Add emptying events const emptyingEvents = helpers.getEmptyingEvents(binId, { start: startDate, end: endDate }) emptyingEvents.forEach(event => { const date = parseISO(event.timestamp) const dateKey = date.toISOString() const existing = dataMap.get(dateKey) || { date } dataMap.set(dateKey, { ...existing, [`${binId}_emptied`]: event.fill_level, }) }) }) return Array.from(dataMap.values()) .sort((a, b) => a.date - b.date) .map(point => ({ ...point, // Filter out interpolated points that don't have any real data ...(Object.keys(point).some(k => k.includes('_isInterpolated')) ? { date: point.date, ...Object.fromEntries( Object.entries(point).filter(([key]) => !key.endsWith('_isInterpolated')) ) } : point) })) }, [helpers, selectedBin, selectedTimeRange, customDateRange]) const chartConfig: ChartConfig = { bin1: { label: "bin1", color: "hsl(var(--chart-1))" }, bin2: { label: "bin2", color: "hsl(var(--chart-2))" }, bin3: { label: "bin3", color: "hsl(var(--chart-3))" }, bin4: { label: "bin4", color: "hsl(var(--chart-4))" }, } 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 over time with emptying events</CardDescription> </CardHeader> <CardContent> <div className="flex flex-wrap gap-4 mb-4"> <Select value={selectedBin} onValueChange={setSelectedBin}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Select a bin" /> </SelectTrigger> <SelectContent> {binOptions.map(binId => ( <SelectItem key={binId} value={binId}> {binId === 'all' ? 'All Bins' : 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> {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%"> <LineChart data={chartData}> <CartesianGrid vertical={false} /> <XAxis dataKey="date" tickFormatter={(value) => selectedTimeRange <= 1 ? format(value, "HH:mm") : 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") const bins = Array.from(new Set(payload.map(p => p.name?.replace('_emptied', '')))) 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 .filter(p => !p.name?.endsWith('_emptied')) .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"> {entry.dataKey}: </span> <span className="text-sm font-medium"> {entry.value?.toFixed(0)}% </span> </div> ))} </div> </div> ) }} /> <ChartLegend content={<ChartLegendContent />} /> {selectedBin === 'all' ? ( ['bin1', 'bin2', 'bin3', 'bin4'].map((binId) => ( <Line key={binId} type="monotone" dataKey={binId} stroke={`var(--color-${binId})`} strokeWidth={2} dot={false} name={binId} connectNulls={true} /> )) ) : ( <Line type="monotone" dataKey={selectedBin} stroke={`var(--color-${selectedBin})`} strokeWidth={2} dot={false} connectNulls={true} /> )} {/* Emptying events as scatter points */} {selectedBin === 'all' ? ['bin1', 'bin2', 'bin3', 'bin4'].map(binId => ( <Scatter key={`${binId}_emptied`} dataKey={`${binId}_emptied`} fill={`var(--color-${binId})`} shape={<circle r={4} />} /> )) : <Scatter dataKey={`${selectedBin}_emptied`} fill={`var(--color-${selectedBin})`} shape={<circle r={4} />} /> } </LineChart> </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment