Untitled
Anonymous
plain_text
02/08/2025 8:03 PM
11.8 KB
19
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