Untitled
unknown
plain_text
9 months ago
11 kB
5
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