Untitled
unknown
plain_text
2 months ago
9.0 kB
3
Indexable
"use client" import { useState, useEffect, useMemo } from "react" import { format, parseISO, subDays, differenceInDays } 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, useRouter } 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: "#2563eb", bin2: "#059669", bin3: "#7c3aed", bin4: "#db2777", } export function HistoricalFillChart() { const router = useRouter() 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 { 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 }) // Create merged timeline with all timestamps const allTimestamps = Array.from( new Set([ ...historicalData.map(d => parseISO(d.timestamp).getTime()), ...emptyingData.map(d => parseISO(d.timestamp).getTime()) ]) ).sort((a, b) => a - b) // Create data structure with all bins at each timestamp const dataMap = allTimestamps.reduce((acc, timestamp) => { acc[timestamp] = { date: timestamp, ...activeBinIds.reduce((binAcc, binId) => ({ ...binAcc, [binId]: null }), {}) } return acc }, {} as Record<number, any>) // Fill in historical data historicalData.forEach(item => { const timestamp = parseISO(item.timestamp).getTime() if (dataMap[timestamp]) { dataMap[timestamp][item.bin_id] = item.fill_level } }) return { chartData: Object.values(dataMap), emptyingEvents: emptyingData.map(event => ({ date: parseISO(event.timestamp).getTime(), bin_id: event.bin_id, fill_level: event.fill_level, })) } }, [helpers, selectedTimeRange, customDateRange, activeBinIds]) 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]) // ... keep handleBinChange and handleTimeRangeChange the same ... const getXAxisTickFormatter = () => { if (selectedTimeRange === 1) return (ts: number) => format(new Date(ts), "HH:mm") if (selectedTimeRange <= 7) return (ts: number) => format(new Date(ts), "MMM dd") if (selectedTimeRange <= 30) return (ts: number) => format(new Date(ts), "MMM dd") return (ts: number) => format(new Date(ts), "MMM yyyy") } const getXAxisInterval = () => { if (selectedTimeRange === 1) return 0 if (selectedTimeRange <= 7) return 0 if (selectedTimeRange <= 30) return 3 return 7 } return ( <Card> <CardHeader> <CardTitle>Historical Fill Levels</CardTitle> <CardDescription>Fill level trends with emptying events</CardDescription> </CardHeader> <CardContent> {/* Keep UI controls same as before */} <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={getXAxisTickFormatter()} axisLine={false} tickLine={false} tickMargin={8} scale="time" type="number" domain={['dataMin', 'dataMax']} interval={getXAxisInterval()} /> <YAxis domain={[0, 100]} axisLine={false} tickLine={false} tickFormatter={(value) => `${value}%`} /> <ChartTooltip content={({ active, payload }) => { if (!active || !payload?.length) return null const timestamp = payload[0].payload.date return ( <div className="rounded-lg border bg-background p-2 shadow-sm"> <div className="grid gap-2"> <div className="text-sm font-medium"> {format(new Date(timestamp), "MMM dd, yyyy HH:mm")} </div> {payload .filter(entry => entry.dataKey !== 'date') .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> ) }} /> {Object.keys(chartConfig).map((binId) => ( <Line key={binId} type="linear" dataKey={binId} stroke={chartConfig[binId].color} strokeWidth={2} dot={false} connectNulls={false} /> ))} <Scatter name="Emptying Events" data={emptyingEvents} fill="#ff7300" shape={(props) => { const binColor = binColors[props.payload.bin_id as keyof typeof binColors] return ( <circle {...props} cx={props.cx} cy={props.cy} r={6} fill={binColor} stroke="#fff" strokeWidth={2} /> ) }} /> </LineChart> </ResponsiveContainer> </ChartContainer> </CardContent> </Card> ) }
Editor is loading...
Leave a Comment