import React, { useMemo, useState } from "react"; import { Chart as ChartJS, CategoryScale, LinearScale, BarController, BarElement, Tooltip, type ChartOptions, type ChartData, } from "chart.js"; import { Chart } from "react-chartjs-2"; import { useThemeVars } from "@/lib/theme-provider"; import { Card, CardContent, CardHeader, CardTitle, CardAction } from "./card"; import { Empty, EmptyDescription } from "./empty"; import { Skeleton } from "./skeleton"; import { cn } from "@/lib/utils"; import { FALLBACK_TICK, SEVERITY_COLORS, formatTooltipDate, formatMonthLabel, formatCount, ChartLegendItem, ChartPeriodButton, } from "./chart-shared"; ChartJS.register( CategoryScale, LinearScale, BarController, BarElement, Tooltip, ); // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface AlertHistoryDataPoint { /** ISO date string e.g. "2024-01-01" */ date: string; /** Count of high-severity alerts */ high: number; /** Count of medium-severity alerts */ medium: number; /** Count of low-severity alerts */ low: number; } export type AlertHistoryPeriod = 3 | 6 | 12; export interface BackofficeAlertHistoryChartProps { chartData?: AlertHistoryDataPoint[] | null; title?: string; /** Show or hide the chart legend */ showLegend?: boolean; /** Legend placement relative to the chart */ legendPosition?: "top" | "bottom"; /** Show or hide X axis labels */ showXAxis?: boolean; /** Show or hide Y axis labels */ showYAxis?: boolean; /** Default period selector value */ defaultPeriod?: AlertHistoryPeriod; /** Chart canvas height in pixels */ height?: number; /** Card max-width in pixels or CSS string */ width?: number | string; className?: string; /** Show skeleton loading state instead of the chart */ isLoading?: boolean; } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const ALERT_PERIODS: AlertHistoryPeriod[] = [3, 6, 12]; const SLICE_COUNT: Record = { 3: 3, 6: 6, 12: 12 }; // --------------------------------------------------------------------------- // Legend // --------------------------------------------------------------------------- function ChartLegend() { return (
); } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function BackofficeAlertHistoryChart({ chartData, title = "Alert History", showLegend = true, legendPosition = "top", showXAxis = true, showYAxis = false, defaultPeriod = 6, height = 200, width = "100%", className, isLoading = false, }: BackofficeAlertHistoryChartProps) { const [period, setPeriod] = useState(defaultPeriod); const themeVars = useThemeVars(); const fontFamily: string = (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif"; const sliced = useMemo(() => { if (!chartData?.length) return null; const count = Math.min(SLICE_COUNT[period], chartData.length); return chartData.slice(chartData.length - count); }, [chartData, period]); const labels = useMemo( () => sliced?.map((p) => formatMonthLabel(p.date)) ?? [], [sliced], ); const data = useMemo>(() => { if (!sliced) return { labels: [], datasets: [] }; return { labels, datasets: [ { label: "High", data: sliced.map((p) => p.high), backgroundColor: SEVERITY_COLORS.high, borderWidth: 0, stack: "alerts", }, { label: "Medium", data: sliced.map((p) => p.medium), backgroundColor: SEVERITY_COLORS.medium, borderWidth: 0, stack: "alerts", }, { label: "Low", data: sliced.map((p) => p.low), backgroundColor: SEVERITY_COLORS.low, borderWidth: 0, stack: "alerts", }, ], }; }, [sliced, labels]); const options = useMemo>( () => ({ responsive: true, maintainAspectRatio: false, animation: { duration: 600, easing: "easeOutQuart" }, layout: { padding: 0 }, plugins: { legend: { display: false }, tooltip: { mode: "index", intersect: false, padding: 12, cornerRadius: 0, titleFont: { size: 11, weight: 600 }, bodyFont: { size: 12, weight: 500 }, callbacks: { title: (tooltipItems) => { const idx = tooltipItems[0]?.dataIndex; if (idx != null && sliced?.[idx]?.date) { return formatTooltipDate(sliced[idx].date, "monthly"); } return tooltipItems[0]?.label ?? ""; }, label: (ctx) => { const val = ctx.raw as number; if (val === 0) return; return ` ${ctx.dataset.label}: ${formatCount(val)}`; }, }, }, }, scales: { x: { stacked: true, display: showXAxis, grid: { display: false }, border: { display: false }, ticks: { maxRotation: 0, minRotation: 0, color: FALLBACK_TICK, font: { size: 10 }, }, }, y: { stacked: true, display: showYAxis, grid: { display: false }, border: { display: false }, ticks: { padding: 8, maxTicksLimit: 5, color: FALLBACK_TICK, font: { size: 10 }, callback: (v) => formatCount(Number(v)), }, }, }, }), [showXAxis, showYAxis, sliced], ); return ( {title}
{ALERT_PERIODS.map((p) => ( setPeriod(p)} /> ))}
{isLoading ? ( ) : !sliced ? ( No data available ) : (
{showLegend && legendPosition === "top" && }
{showLegend && legendPosition === "bottom" && }
)}
); }