Untitled
unknown
plain_text
8 months ago
15 kB
5
Indexable
"use client"
import { useState, useEffect, useMemo } from "react"
import { format, parseISO, subDays, startOfDay, endOfDay } from "date-fns"
import { AreaChart, Area, 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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
import { CalendarIcon, X } from "lucide-react"
import { Calendar } from "@/components/ui/calendar"
import { useRouter, useSearchParams } from "next/navigation"
import { useTranslation, type SupportedLanguages } from "@/utils/translations"
const binColors = {
bin1: "hsl(var(--chart-1))",
bin2: "hsl(var(--chart-2))",
bin3: "hsl(var(--chart-3))",
bin4: "hsl(var(--chart-4))",
}
const timeRanges = [
{ label: "allTime", value: 9999999 },
{ label: "last24h", value: 1 },
{ label: "lastWeek", value: 7 },
{ label: "lastMonth", value: 30 },
{ label: "lastYear", value: 365 },
{ label: "custom", value: "custom" },
]
export function HistoricalFillChart() {
const [selectedBin, setSelectedBin] = useState("all")
const [selectedTimeRange, setSelectedTimeRange] = useState<number | "custom">(9999999)
const [customDateRange, setCustomDateRange] = useState<{ from?: Date; to?: Date }>({})
const [isCalendarOpen, setIsCalendarOpen] = useState(false)
const router = useRouter()
const searchParams = useSearchParams()
const { metrics, helpers, loading, error } = useMetrics()
const { t } = useTranslation()
const activeBins = useMemo(() => {
const bins = helpers?.getActiveBinIds() || []
return [...bins].sort((a, b) => {
const numA = parseInt(a.replace('bin', ''))
const numB = parseInt(b.replace('bin', ''))
return numA - numB
})
}, [helpers])
useEffect(() => {
const binFromUrl = searchParams.get("bin")
if (!binFromUrl || binFromUrl === "all") {
setSelectedBin("all")
} else if (activeBins.includes(binFromUrl)) {
setSelectedBin(binFromUrl)
}
}, [searchParams, activeBins])
const formatBinName = (binId: string) => {
const binNumber = parseInt(binId.replace('bin', ''))
return `${t('historicalFillChart.binPrefix')} ${binNumber}`
}
const handleBinChange = (value: string) => {
setSelectedBin(value)
if (value === "all") {
router.push("?bin=all")
} else {
router.push(`?bin=${value}`)
}
}
const chartData = useMemo(() => {
if (!helpers) return []
let startDate = new Date()
let endDate = new Date()
if (selectedTimeRange === "custom" && customDateRange.from && customDateRange.to) {
startDate = startOfDay(customDateRange.from)
endDate = endOfDay(customDateRange.to)
} else if (typeof selectedTimeRange === "number") {
startDate = subDays(endDate, selectedTimeRange)
}
const timeFilter = { start: startDate, end: endDate }
const emptyingEvents = helpers.getEmptyingEvents(selectedBin === "all" ? undefined : selectedBin, timeFilter)
if (selectedBin === "all") {
const allFillData = activeBins.flatMap(bin =>
helpers.getFillLevelHistory(bin, timeFilter).map(item => ({
date: parseISO(item.timestamp),
binId: bin,
fillLevel: item.fill_level,
isEmptied: emptyingEvents.some(e =>
e.bin_id === bin &&
parseISO(e.timestamp).getTime() === parseISO(item.timestamp).getTime()
)
}))
)
allFillData.sort((a, b) => a.date.getTime() - b.date.getTime())
const lastValues = activeBins.reduce((acc, bin) => {
acc[bin] = null
return acc
}, {} as Record<string, number | null>)
const normalizedData: Record<number, any> = {}
allFillData.forEach(item => {
const timestamp = item.date.getTime()
if (!normalizedData[timestamp]) {
normalizedData[timestamp] = {
date: item.date,
...activeBins.reduce((acc, bin) => {
acc[bin] = lastValues[bin] !== null ? lastValues[bin] : null
return acc
}, {} as Record<string, any>)
}
}
normalizedData[timestamp][item.binId] = item.fillLevel
lastValues[item.binId] = item.fillLevel
})
const sortedTimestamps = Object.keys(normalizedData)
.map(Number)
.sort((a, b) => a - b)
return sortedTimestamps.map(t => normalizedData[t])
}
const fillData = helpers.getFillLevelHistory(selectedBin, timeFilter)
return fillData.map(item => ({
date: parseISO(item.timestamp),
fillLevel: item.fill_level,
emptied: emptyingEvents.some(e =>
e.bin_id === selectedBin &&
parseISO(e.timestamp).getTime() === parseISO(item.timestamp).getTime()
)
}))
}, [helpers, selectedBin, selectedTimeRange, customDateRange, activeBins])
const chartConfig: ChartConfig = selectedBin === "all" ? {
bin1: { label: t('historicalFillChart.binPrefix') + " 1", color: binColors.bin1 },
bin2: { label: t('historicalFillChart.binPrefix') + " 2", color: binColors.bin2 },
bin3: { label: t('historicalFillChart.binPrefix') + " 3", color: binColors.bin3 },
bin4: { label: t('historicalFillChart.binPrefix') + " 4", color: binColors.bin4 },
} : {
fillLevel: {
label: formatBinName(selectedBin),
color: binColors[selectedBin as keyof typeof binColors]
},
}
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>{t('historicalFillChart.loading')}</div>
if (error) return <div>{t('historicalFillChart.error')}{error}</div>
return (
<Card>
<CardHeader>
<CardTitle>{t('historicalFillChart.title')}</CardTitle>
<CardDescription>{t('historicalFillChart.description')}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-4 mb-4">
<Select value={selectedBin} onValueChange={handleBinChange}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder={t('historicalFillChart.selectBin')}>
{selectedBin === 'all' ? t('historicalFillChart.allBins') : formatBinName(selectedBin)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{t('historicalFillChart.allBins')}</SelectItem>
{activeBins.map(bin => (
<SelectItem key={bin} value={bin}>
{formatBinName(bin)}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedTimeRange.toString()} onValueChange={handleTimeRangeChange}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder={t('timeRanges.allTime')} />
</SelectTrigger>
<SelectContent>
{timeRanges.map(range => (
<SelectItem key={range.value} value={range.value.toString()}>
{t(`timeRanges.${range.label}`)}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedTimeRange === "custom" && (
<Popover
open={isCalendarOpen}
onOpenChange={(open) => {
if (!open && selectedTimeRange === "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>{t('estimatechart.pickDateRange')}</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>
</CardContent>
<CardContent>
<ChartContainer config={chartConfig} className="aspect-auto h-[400px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickFormatter={(date) => format(date, "MMM dd")}
minTickGap={40}
axisLine={false}
tickLine={false}
tickMargin={8}
/>
<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 entries = selectedBin === "all"
? activeBins.map(bin => ({
dataKey: bin,
value: payload[0].payload[bin],
color: binColors[bin as keyof typeof binColors]
})).filter(e => e.value !== null)
: payload.filter(p => p.dataKey === "fillLevel")
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>
{entries.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>
)
}} />
<ChartLegend content={<ChartLegendContent />} />
<defs>
{activeBins.map(bin => (
<linearGradient key={bin} id={`fill${bin}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={`var(--color-${bin})`} stopOpacity={0.8} />
<stop offset="95%" stopColor={`var(--color-${bin})`} stopOpacity={0.1} />
</linearGradient>
))}
<linearGradient id="fillSingle" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={`var(--color-${selectedBin})`} stopOpacity={0.8} />
<stop offset="95%" stopColor={`var(--color-${selectedBin})`} stopOpacity={0.1} />
</linearGradient>
</defs>
{selectedBin === "all" ? (
activeBins.map(bin => (
<Area
key={bin}
type="monotone"
dataKey={bin}
stroke={binColors[bin as keyof typeof binColors]}
fill={`url(#fill${bin})`}
fillOpacity={0.4}
strokeWidth={2}
dot={false}
connectNulls
/>
))
) : (
<>
<Area
type="monotone"
dataKey="fillLevel"
stroke={binColors[selectedBin as keyof typeof binColors]}
fill={`url(#fillSingle)`}
fillOpacity={0.4}
strokeWidth={2}
dot={false}
connectNulls
/>
<Scatter
data={chartData.filter((d: any) => d.emptied)}
dataKey="fillLevel"
fill="var(--chart-5)"
shape={(props) => (
<circle {...props} r={6} strokeWidth={2} stroke="var(--background)" />
)}
/>
</>
)}
</AreaChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
</Card>
)
}Editor is loading...
Leave a Comment