/** * JailDistributionChart — horizontal bar chart showing ban counts per jail, * sorted descending, for the selected time window. * * Calls `useJailDistribution` internally and handles loading, error, and * empty states so the parent only needs to pass filter props. */ import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import type { TooltipContentProps } from "recharts/types/component/Tooltip"; import { MessageBar, MessageBarBody, Spinner, Text, tokens, makeStyles, } from "@fluentui/react-components"; import { CHART_AXIS_TEXT_TOKEN, CHART_GRID_LINE_TOKEN, CHART_PALETTE, resolveFluentToken, } from "../utils/chartTheme"; import { useJailDistribution } from "../hooks/useJailDistribution"; import type { BanOriginFilter, TimeRange } from "../types/ban"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** Maximum characters before truncating a jail name on the Y-axis. */ const MAX_LABEL_LENGTH = 24; /** Height per bar row in pixels. */ const BAR_HEIGHT_PX = 36; /** Minimum chart height in pixels. */ const MIN_CHART_HEIGHT = 180; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /** Props for {@link JailDistributionChart}. */ interface JailDistributionChartProps { /** Time-range preset controlling the query window. */ timeRange: TimeRange; /** Origin filter controlling which bans are included. */ origin: BanOriginFilter; } /** Internal chart data point shape. */ interface BarEntry { /** Full jail name used by Tooltip. */ fullName: string; /** Truncated name displayed on the Y-axis. */ name: string; /** Ban count. */ value: number; } // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- const useStyles = makeStyles({ wrapper: { width: "100%", overflowX: "hidden", }, stateWrapper: { width: "100%", minHeight: `${String(MIN_CHART_HEIGHT)}px`, display: "flex", alignItems: "center", justifyContent: "center", }, emptyText: { color: tokens.colorNeutralForeground3, }, }); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Build the chart dataset from the raw jail list. * * @param jails - Ordered list of `{jail, count}` items from the API. * @returns Array of `BarEntry` objects ready for Recharts. */ function buildEntries(jails: Array<{ jail: string; count: number }>): BarEntry[] { return jails.map(({ jail, count }) => ({ fullName: jail, name: jail.length > MAX_LABEL_LENGTH ? `${jail.slice(0, MAX_LABEL_LENGTH)}…` : jail, value: count, })); } // --------------------------------------------------------------------------- // Custom tooltip // --------------------------------------------------------------------------- function JailTooltip(props: TooltipContentProps): React.JSX.Element | null { const { active, payload } = props; if (!active || payload.length === 0) return null; const entry = payload[0]; if (entry == null) return null; const { fullName, value } = entry.payload as BarEntry; return (
{fullName}
{String(value)} ban{value === 1 ? "" : "s"}
); } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- /** * Horizontal bar chart showing ban counts per jail for the selected window. * * Fetches data via `useJailDistribution` and renders loading, error, and * empty states inline. * * @param props - `timeRange` and `origin` filter props. */ export function JailDistributionChart({ timeRange, origin, }: JailDistributionChartProps): React.JSX.Element { const styles = useStyles(); const { jails, isLoading, error } = useJailDistribution(timeRange, origin); if (error != null) { return ( {error} ); } if (isLoading) { return (
); } if (jails.length === 0) { return (
No ban data for the selected period.
); } const entries = buildEntries(jails); const chartHeight = Math.max(entries.length * BAR_HEIGHT_PX, MIN_CHART_HEIGHT); const primaryColour = resolveFluentToken(CHART_PALETTE[0] ?? ""); const axisColour = resolveFluentToken(CHART_AXIS_TEXT_TOKEN); const gridColour = resolveFluentToken(CHART_GRID_LINE_TOKEN); return (
); }