Untitled
unknown
plain_text
a year ago
8.8 kB
9
Indexable
"use client"
import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, addDays, isAfter, differenceInDays } 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, type ChartConfig } from "@/components/ui/chart"
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 { Skeleton } from "@/components/ui/skeleton"
import { cn } from "@/lib/utils"
const DEFAULT_RANGE = 30
const TIME_RANGES = [
{ label: "Week", value: 7 },
{ label: "Month", value: 30 },
{ label: "Quarter", value: 90 },
{ label: "Year", value: 365 },
{ label: "Custom", value: "custom" },
]
export function EstimateFillLevelChart() {
const { metrics, helpers } = useMetrics()
const { predictions } = useFillPredictions()
const [dateRange, setDateRange] = useState<{ start?: Date; end?: Date }>({})
const [selectedBin, setSelectedBin] = useState<string>()
const [selectedRange, setSelectedRange] = useState<number | 'custom'>(DEFAULT_RANGE)
// Bin selection handling
const bins = useMemo(() => helpers?.getActiveBinIds() || [], [helpers])
useEffect(() => setSelectedBin(prev => bins.includes(prev!) ? prev : bins[0]), [bins])
// Date range calculation
useEffect(() => {
if (selectedRange === 'custom') return
setDateRange({
start: subDays(new Date(), selectedRange),
end: new Date()
})
}, [selectedRange])
// Chart data processing
const chartData = useMemo(() => {
if (!selectedBin || !helpers || !dateRange.start || !dateRange.end) return []
const historical = helpers.getFillLevelHistory(selectedBin, dateRange)
const prediction = predictions.find(p => p.binId === selectedBin)
if (!historical.length) return []
const data = historical.map(d => ({
date: parseISO(d.timestamp),
value: d.fill_level,
type: 'historical' as const
}))
// Add prediction if available
if (prediction) {
const lastPoint = data[data.length - 1]
const predDate = new Date(prediction.predictedFullTime)
const daysRemaining = differenceInDays(predDate, lastPoint.date)
if (daysRemaining > 0) {
const dailyIncrease = (100 - lastPoint.value) / daysRemaining
for (let i = 1; i <= daysRemaining; i++) {
data.push({
date: addDays(lastPoint.date, i),
value: Math.min(lastPoint.value + dailyIncrease * i, 100),
type: 'prediction' as const
})
}
}
}
return data
}, [selectedBin, helpers, predictions, dateRange])
// Chart configuration
const chartConfig: ChartConfig = {
historical: { label: "Actual Fill Level", color: "hsl(var(--chart-1))" },
prediction: { label: "Predicted Fill Level", color: "hsl(var(--chart-2))" }
}
if (!helpers || !selectedBin) {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-[200px]" />
<Skeleton className="h-4 w-[300px]" />
</CardHeader>
<CardContent>
<Skeleton className="h-[400px] w-full" />
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between gap-4">
<div>
<CardTitle>Fill Level Projection</CardTitle>
<CardDescription>
{selectedBin} - Historical and predicted fill levels
</CardDescription>
</div>
<div className="flex gap-2">
<Select value={selectedBin} onValueChange={setSelectedBin}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{bins.map(bin => (
<SelectItem key={bin} value={bin}>{bin}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedRange.toString()} onValueChange={v => setSelectedRange(v === 'custom' ? v : +v)}>
<SelectTrigger className="w-[120px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIME_RANGES.map(({ label, value }) => (
<SelectItem key={value} value={value.toString()}>{label}</SelectItem>
))}
</SelectContent>
</Select>
{selectedRange === 'custom' && (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[240px] justify-start">
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange.start && dateRange.end ? (
`${format(dateRange.start, 'MMM dd')} - ${format(dateRange.end, 'MMM dd')}`
) : 'Select date range'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="range"
selected={{ from: dateRange.start, to: dateRange.end }}
onSelect={(v) => v && setDateRange({ start: v.from, end: v.to })}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
)}
</div>
</div>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid vertical={false} strokeDasharray="3 3" />
<XAxis
dataKey="date"
tickFormatter={v => format(v, 'MMM dd')}
axisLine={false}
tickLine={false}
minTickGap={40}
/>
<YAxis
domain={[0, 100]}
axisLine={false}
tickLine={false}
tickFormatter={v => `${v}%`}
width={60}
/>
<ChartTooltip content={({ active, payload }) => (
<div className={cn(
"rounded-lg border bg-popover p-3 shadow-sm",
active && payload?.length ? 'opacity-100' : 'opacity-0'
)}>
{payload?.map((entry) => (
<div key={entry.name} className="flex items-center gap-2">
<div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<div className="flex-1">
<div className="text-sm font-medium">
{format(entry.payload.date, 'MMM dd, yyyy')}
</div>
<div className="text-sm">
{entry.dataKey === 'prediction' && 'Projected '}
Fill Level: {entry.value?.toFixed(1)}%
</div>
</div>
</div>
))}
</div>
)}/>
<Line
type="monotone"
dataKey="value"
stroke="var(--color-historical)"
strokeWidth={2}
dot={false}
name="Historical"
connectNulls
isAnimationActive={false}
/>
<Line
type="monotone"
dataKey="value"
stroke="var(--color-prediction)"
strokeWidth={2}
strokeDasharray="4 4"
dot={false}
name="Prediction"
connectNulls
isAnimationActive={false}
filter={({ type }) => type === 'prediction'}
/>
</LineChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
)
}Editor is loading...
Leave a Comment