Untitled
unknown
plain_text
a year ago
14 kB
9
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