/** * 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 (