import { useState } from "react"; import type { HourlyBucket } from "../../../shared/api/client"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../../shared/ui"; import { cn } from "../../../shared/lib/utils"; interface ActivityChartProps { hourly: HourlyBucket[]; loading: boolean; } const COLORS = { clean: { fill: "#38bdf8", label: "Clean" }, warned: { fill: "#facc15", label: "Warned" }, flagged: { fill: "#f472b6", label: "Flagged" }, error: { fill: "#fb923c", label: "Error" }, } as const; type BarKey = keyof typeof COLORS; export function ActivityChart({ hourly, loading }: ActivityChartProps) { const [tooltip, setTooltip] = useState<{ hour: string; total: number; } | null>(null); const [hoveredBar, setHoveredBar] = useState(null); if (loading && !hourly?.length) return ; if (!hourly?.length) return ; const data = hourly.map((b) => { const utcHour = parseInt(b.hour.slice(11, 13), 10); const jakartaHour = (utcHour + 7) % 24; return { hour: `${String(jakartaHour).padStart(2, "0")}:00`, clean: b.clean, warned: b.warned, flagged: b.flagged, error: b.error, total: b.count, }; }); const maxTotal = Math.max(...data.map((d) => d.total), 1); // Only show every Nth label to avoid crowding const labelInterval = data.length > 16 ? 2 : 1; const bars: Array<{ key: BarKey; color: string; label: string }> = [ { key: "clean", color: COLORS.clean.fill, label: COLORS.clean.label }, { key: "warned", color: COLORS.warned.fill, label: COLORS.warned.label }, { key: "flagged", color: COLORS.flagged.fill, label: COLORS.flagged.label }, { key: "error", color: COLORS.error.fill, label: COLORS.error.label }, ]; const CHART_HEIGHT = 200; const BAR_GROUP_WIDTH = 28; const BAR_WIDTH = 5; const GAP = 2; return (
Aktivitas per Jam Distribusi pesan per jam — arahkan kursor ke bar untuk detail.
{/* Legend */}
{bars.map((b) => ( {b.label} ))}
{/* Chart area */}
{/* Grid lines */} {[0, 0.25, 0.5, 0.75, 1].map((ratio) => { const y = CHART_HEIGHT - ratio * (CHART_HEIGHT - 20) - 20; return ( {Math.round(ratio * maxTotal)} ); })} {/* Bars */} {data.map((d, i) => { const x = i * BAR_GROUP_WIDTH + 32; let accumulated = 0; return ( {/* Hover target (invisible wider rect) */} { setTooltip({ hour: d.hour, total: d.total }); setHoveredBar(d.hour); }} onMouseLeave={() => { setTooltip(null); setHoveredBar(null); }} /> {/* Stacked bars */} {bars.map((bar) => { const val = d[bar.key]; const barH = (val / maxTotal) * (CHART_HEIGHT - 20); const y = CHART_HEIGHT - accumulated - barH - 20; accumulated += barH; return val > 0 ? ( ) : null; })} {/* X-axis label */} {i % labelInterval === 0 && ( {d.hour} )} ); })} {/* Tooltip */} {tooltip && (
d.hour === tooltip.hour) * BAR_GROUP_WIDTH + 36}px`, }} >
{tooltip.hour}
{bars.map((b) => { const d = data.find((d) => d.hour === tooltip.hour); const val = d?.[b.key] ?? 0; return val > 0 ? (
{b.label} {val}
) : null; })}
Total {tooltip.total}
)}
); } function LoadingBox() { return ( Memuat data... ); } function EmptyBox({ text }: { text: string }) { return ( {text} ); }