Untitled
unknown
plain_text
9 months ago
9.9 kB
6
Indexable
"use client"
import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, addDays, isAfter } from "date-fns"
import { CartesianGrid, Line, LineChart, XAxis, YAxis, ResponsiveContainer } 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 { ChartEmptyState } from "@/components/ui/chart-empty-state"
import { useMetrics } from "@/hooks/useMetrics"
import { useFillPredictions } from "@/hooks/useFillPredictions"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { CalendarIcon } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
const timeRanges = [
{ label: "Last 7 days", value: 7 },
{ label: "Last 30 days", value: 30 },
{ label: "Last 90 days", value: 90 },
{ label: "Last 180 days", value: 180 },
{ label: "Last 365 days", value: 365 },
{ label: "Custom", value: "custom" },
]
export function EstimateFillLevelChart() {
const router = useRouter()
const searchParams = useSearchParams()
const [selectedBin, setSelectedBin] = useState<string | null>(null)
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 { predictions } = useFillPredictions()
const activeBinIds = useMemo(() => {
if (!helpers) return []
return helpers.getActiveBinIds()
}, [helpers])
useEffect(() => {
const binFromUrl = searchParams.get("bin")
if (binFromUrl && activeBinIds.includes(binFromUrl)) {
setSelectedBin(binFromUrl)
} else if (activeBinIds.length > 0) {
setSelectedBin(activeBinIds[0])
}
}, [searchParams, activeBinIds])
const chartData = useMemo(() => {
if (!helpers || !selectedBin) 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) // Default to 30 days if something goes wrong
}
const historicalData = helpers.getFillLevelHistory(selectedBin, { start: startDate, end: endDate })
const prediction = predictions.find((p) => p.binId === selectedBin)
const data = historicalData.map((item) => ({
date: parseISO(item.timestamp),
fillLevel: item.fill_level,
}))
if (prediction && data.length > 0) {
const lastDataPoint = data[data.length - 1]
const predictedFullDate = new Date(prediction.predictedFullTime)
if (isAfter(predictedFullDate, lastDataPoint.date)) {
const daysUntilFull = (predictedFullDate.getTime() - lastDataPoint.date.getTime()) / (1000 * 60 * 60 * 24)
const fillRatePerDay = (100 - lastDataPoint.fillLevel) / daysUntilFull
// Add the prediction starting from the last historical point
data.push({
date: lastDataPoint.date,
fillLevel: lastDataPoint.fillLevel,
predictedFillLevel: lastDataPoint.fillLevel
})
// Then add the future predictions
for (let i = 1; i <= daysUntilFull; i++) {
const predictedDate = addDays(lastDataPoint.date, i)
const predictedFillLevel = Math.min(lastDataPoint.fillLevel + fillRatePerDay * i, 100)
data.push({
date: predictedDate,
predictedFillLevel: predictedFillLevel,
})
}
}
}
return data
}, [helpers, selectedBin, selectedTimeRange, customDateRange, predictions])
const chartConfig: ChartConfig = {
fillLevel: {
label: "Fill Level",
color: "hsl(var(--chart-1))",
},
predictedFillLevel: {
label: "Predicted Fill Level",
color: "hsl(var(--chart-2))",
},
}
const handleBinChange = (value: string) => {
setSelectedBin(value)
router.push(`?bin=${value}`)
}
const handleTimeRangeChange = (value: string) => {
setSelectedTimeRange(value === "custom" ? "custom" : Number.parseInt(value))
}
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
const hasHistoricalData = chartData.some(data => data.fillLevel !== undefined)
const hasPredictionData = chartData.some(data => data.predictedFillLevel !== undefined)
const renderChartContent = () => {
if (!selectedBin) {
return (
<ChartEmptyState
title="No Bin Selected"
description="Please select a bin to view its fill level data and predictions."
/>
)
}
if (!hasHistoricalData && !hasPredictionData) {
return (
<ChartEmptyState
title="No Data Available"
description="There is no historical data or predictions available for the selected time range."
/>
)
}
if (!hasHistoricalData) {
return (
<ChartEmptyState
title="No Historical Data"
description="There is no historical fill level data available for the selected time range."
/>
)
}
if (!hasPredictionData) {
return (
<ChartEmptyState
title="No Prediction Data"
description="There are no fill level predictions available for this bin."
/>
)
}
return (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickFormatter={(value) => 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={<ChartTooltipContent />} />
<ChartLegend content={<ChartLegendContent />} />
<Line
type="monotone"
dataKey="fillLevel"
stroke="var(--color-fillLevel)"
strokeWidth={2}
dot={false}
name="Fill Level"
connectNulls={false}
/>
<Line
type="monotone"
dataKey="predictedFillLevel"
stroke="var(--color-predictedFillLevel)"
strokeWidth={2}
strokeDasharray="10 10"
dot={false}
name="Predicted Fill Level"
connectNulls
/>
</LineChart>
</ResponsiveContainer>
)
}
return (
<Card>
<CardHeader>
<CardTitle>Estimate Fill Level Chart</CardTitle>
<CardDescription>Historical and predicted fill levels</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4 mb-4">
<Select value={selectedBin || undefined} onValueChange={handleBinChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select a bin" />
</SelectTrigger>
<SelectContent>
{activeBinIds.map((binId) => (
<SelectItem key={binId} value={binId}>
{binId}
</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>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[280px] justify-start text-left font-normal">
<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>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={customDateRange.from}
selected={customDateRange}
onSelect={setCustomDateRange}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
)}
</div>
<ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full">
{renderChartContent()}
</ChartContainer>
</CardContent>
</Card>
)
}Editor is loading...
Leave a Comment