import { useState } from "react"; import type { TrendBucket } from "../../../shared/api/client"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../../shared/ui"; import { cn } from "../../../shared/lib/utils"; interface TrendChartProps { trend: TrendBucket[]; loading: boolean; } const LINE_COLORS: Array<{ key: keyof Omit; color: string; label: string; dash?: string; }> = [ { key: "count", color: "#38bdf8", label: "Total" }, { key: "clean", color: "#34d399", label: "Clean" }, { key: "flagged", color: "#f472b6", label: "Flagged" }, { key: "warned", color: "#facc15", label: "Warned" }, { key: "error", color: "#fb923c", label: "Error", dash: "4 3" }, ]; export function TrendChart({ trend, loading }: TrendChartProps) { const [tooltip, setTooltip] = useState<{ date: string; values: Array<{ key: string; label: string; value: number; color: string }>; } | null>(null); if (loading && !trend?.length) return ; if (!trend?.length) return null; const CHART_HEIGHT = 200; const CHART_PADDING = { top: 10, right: 16, bottom: 30, left: 40 }; const chartW = Math.max((trend.length - 1) * 64, 200); const plotW = chartW - CHART_PADDING.left - CHART_PADDING.right; const plotH = CHART_HEIGHT - CHART_PADDING.top - CHART_PADDING.bottom; const allValues = trend.flatMap((d) => LINE_COLORS.map((l) => Number(d[l.key] ?? 0)), ); const maxValue = Math.max(...allValues, 1); function getX(index: number): number { if (trend.length <= 1) return CHART_PADDING.left; return ( CHART_PADDING.left + (index / (trend.length - 1)) * plotW ); } function getY(value: number): number { return CHART_PADDING.top + plotH - (value / maxValue) * plotH; } function buildLinePath( data: TrendBucket[], key: keyof Omit, ): string { const points = data.map((d, i) => ({ x: getX(i), y: getY(Number(d[key] ?? 0)), })); if (points.length === 0) return ""; const segments: string[] = [`M ${points[0].x} ${points[0].y}`]; for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; const cx = (prev.x + curr.x) / 2; segments.push(`Q ${cx} ${prev.y} ${curr.x} ${curr.y}`); } return segments.join(" "); } function buildAreaPath( data: TrendBucket[], key: keyof Omit, ): string { const line = buildLinePath(data, key); if (!line) return ""; const first = getX(0); const last = getX(data.length - 1); const bottom = CHART_PADDING.top + plotH; return `${line} L ${last} ${bottom} L ${first} ${bottom} Z`; } const totalMessages = trend.reduce((sum, d) => sum + d.count, 0); return ( Tren Harian Volume pesan per hari — arahkan kursor ke titik untuk detail. {/* Legend */}
{LINE_COLORS.map((l) => ( {l.label} ))}
Rangkuman{" "} {trend.length > 1 ? `${trend.length} hari terakhir` : "hari ini"} {totalMessages} total pesan
{/* Grid lines */} {[0, 0.25, 0.5, 0.75, 1].map((ratio) => { const y = getY(ratio * maxValue); return ( {Math.round(ratio * maxValue)} ); })} {/* Area fills */} {LINE_COLORS.filter((l) => l.key === "count" || l.key === "flagged").map((l) => ( ))} {/* Lines */} {LINE_COLORS.map((l) => ( ))} {/* Interactive dots */} {trend.map((d, i) => { const x = getX(i); const y = getY(d.count); const isActive = tooltip?.date === d.date; return ( {/* Invisible hit area */} { setTooltip({ date: d.date, values: LINE_COLORS.map((l) => ({ key: l.key, label: l.label, value: Number(d[l.key] ?? 0), color: l.color, })), }); }} onMouseLeave={() => setTooltip(null)} /> {/* Dot */} {/* Date label */} {d.date.slice(5)} ); })} {/* Tooltip */} {tooltip && (
d.date === tooltip.date)) + 12}px`, }} >
{tooltip.date}
{tooltip.values .filter((v) => v.value > 0) .map((v) => (
{v.label} {v.value}
))}
)}
); } function LoadingBox() { return ( Memuat data... ); }