Untitled

 avatar
unknown
plain_text
3 months ago
14 kB
6
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