/** * 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 { useMemo } from "react"; import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import type { TooltipContentProps } from "recharts/types/component/Tooltip"; import { makeStyles } from "@fluentui/react-components"; import { CHART_AXIS_TEXT_TOKEN, CHART_GRID_LINE_TOKEN, CHART_PALETTE, resolveFluentToken, } from "../utils/chartTheme"; import { ChartStateWrapper } from "./ChartStateWrapper"; import { ChartTooltip } from "./ChartTooltip"; 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 CHART_TICK_FONT_SIZE = 12; const CHART_MARGIN = { top: 4, right: 16, bottom: 4, left: 8 }; const Y_AXIS_WIDTH = 160; const useStyles = makeStyles({ wrapper: { width: "100%", overflowX: "hidden", }, }); // --------------------------------------------------------------------------- // 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, reload } = useJailDistribution(timeRange, origin); const entries = buildEntries(jails); const chartHeight = Math.max(entries.length * BAR_HEIGHT_PX, MIN_CHART_HEIGHT); const { primaryColour, axisColour, gridColour } = useMemo( () => ({ primaryColour: resolveFluentToken(CHART_PALETTE[0] ?? ""), axisColour: resolveFluentToken(CHART_AXIS_TEXT_TOKEN), gridColour: resolveFluentToken(CHART_GRID_LINE_TOKEN), }), [], ); return (
); }