"use client" /** * ChartsOverview — Dashboard chart gallery * * ── ChartCard variants ─────────────────────────────────────────────────────── * normal — plain card with Ask Leo * tabs — "Chart" | "Trend (Line)" tabs + Ask Leo * selector — quick-filter Select + Ask Leo * metrics-tabs — metric cells ARE the tab triggers (label + value + trend) * * ── ASK LEO ICON GUIDELINE ─────────────────────────────────────────────────── * Always use: * Never use: fa-wand-magic-sparkles (retired, inconsistent) * Size: text-xs (12px via --text-xs) with aria-hidden="true" * Label: "Ask Leo" (never truncate or omit the text label) * Applies to: ALL Ask Leo buttons across the entire app — * ChartCard headers, KeyMetrics card, GreetingWidget, NavUser, etc. * * ── WCAG AA STANDARDS FOR GRAPHS ───────────────────────────────────────────── * 1. Container landmark * • Wrap each chart in a
(or div with role="figure") + * aria-label="" + aria-describedby="" * • Add a visually-hidden
with a plain-text * summary of the key trend (e.g. "Placements rose 12% in Q1 2026"). * * 2. Keyboard navigation * • The ChartContainer wrapper must have tabIndex={0} so it receives focus. * • On focus, announce title + summary via aria-label / aria-describedby. * • Arrow keys (←/→) cycle through data points; announce value via * a live region (role="status" aria-live="polite"). * • Esc clears the selection and returns focus to the container. * * 3. Accessible data table (hidden fallback) * • Immediately after the SVG/canvas, render a wrapped in * (visually hidden, in DOM). * • Columns mirror the chart axes; each data point is a
. * • Screen-reader users can navigate data with standard table shortcuts. * * 4. Colour & contrast * • Chart series colours must achieve ≥ 3:1 contrast against the card bg. * • Never use colour as the ONLY differentiator — pair with: * - Dashed vs solid line strokes * - Direct inline labels on lines/segments * - Shape markers on data points (circle vs square vs triangle) * • Text labels inside charts: ≥ 4.5:1 on their local background. * * 5. Focus ring on data points * • Active/focused data point: 3px outline, ≥ 3:1 contrast, distinct * from the hover state (use outline-offset to separate). * * 6. Tooltip accessibility * • Tooltips must appear on keyboard focus, not only on mouse hover. * • Tooltip content must be announced to the live region. * • Tooltip must remain visible while it has focus (no auto-dismiss). * ───────────────────────────────────────────────────────────────────────────── */ import * as React from "react" import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, ComposedChart, Funnel, FunnelChart, LabelList, Line, LineChart, Pie, PieChart, PolarAngleAxis, PolarGrid, PolarRadiusAxis, Radar, RadarChart, RadialBar, RadialBarChart, Scatter, ScatterChart, Sector, Trapezoid, XAxis, YAxis, ZAxis, type DotItemDotProps, } from "recharts" import type { PieSectorDataItem } from "recharts/types/polar/Pie" import { QuotaLinearProgressCardBody, QuotaRadialChartInner, } from "@/components/dashboard-quota-progress-card" import { DASHBOARD_STUDENT_SCORES, formatBandScore, type StudentScoreRadial, } from "@/lib/mock/dashboard" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, chartTooltipKeyboardSyncProps, ChartTooltipContent, type ChartConfig, } from "@/components/ui/chart" import { chartLineStrokeDash } from "@/lib/chart-line-dash" import { CHART_AXIS_TICK, CHART_TICK_FONT_SIZE, } from "@/lib/chart-typography" import { CHART_DASHBOARD_CELL_CLASS, CHART_DASHBOARD_PLOT_MIN_CLASS, CHART_DASHBOARD_ROW_GRID_CLASS, CHART_DASHBOARD_ROW_GRID_THIRDS_CLASS, } from "@/lib/chart-dashboard-layout" import { chartLeoPeakAnchor } from "@/lib/chart-leo-spotting" import { CHART_KBD_ACTIVE_PIE_SHAPE, } from "@/lib/chart-keyboard-selection" import { cn } from "@/lib/utils" /* ── Core chart shells (no gallery data) ─────────────────────────────────── */ import { ChartCard, ChartDataTable, ChartFigure, ChartLeoInsightOverlay, ChartLeoPlotInsightOverlay, ChartLeoPixelPlotInsightOverlay, type ChartCardVariant, type MiniMetric, type ChartLeoInsight, type ChartLeoInsightAnchor, type ChartLeoInsightKind, } from "@/components/charts-core" export { ChartCard, ChartDataTable, ChartFigure, ChartLeoInsightOverlay, ChartLeoPlotInsightOverlay, ChartLeoPixelPlotInsightOverlay, } export type { ChartCardVariant, MiniMetric, ChartLeoInsight, ChartLeoInsightAnchor, ChartLeoInsightKind } /** Recharts passes `index` into Line `dot` renderers; keep the callback typed against the v3 dot contract. */ type LineDotRenderProps = DotItemDotProps & { index?: number } function donutLeoSector(props: PieSectorDataItem) { const isPeak = props.name === "confirmed" const isActive = (props as PieSectorDataItem & { isActive?: boolean }).isActive return ( ) } function radialLeoSector( props: React.ComponentProps & { payload?: { name?: string }; isActive?: boolean }, ) { const isPeak = props.payload?.name === "nursing" return ( ) } function funnelLeoShape( props: React.ComponentProps & { payload?: { name?: string } }, ) { const isPeak = props.payload?.name === "Applied" return } const SCATTER_LEO_PEAK = { x: 90, y: 97 } as const const activeIndexProps = (activeIndex: number | null) => activeIndex == null ? {} : ({ activeIndex } as Record) const BRAND = "var(--brand-color)" const CHART_1 = "var(--color-chart-1)" const CHART_2 = "var(--color-chart-2)" const CHART_3 = "var(--color-chart-3)" const CHART_4 = "var(--color-chart-4)" const CHART_5 = "var(--color-chart-5)" const SUCCESS = "var(--chart-2)" const WARNING = "var(--chart-4)" const DESTRUCTIVE = "var(--destructive)" /* ── Period filter options (reused across selector cards) ─────────────────── */ const PERIOD_OPTIONS = [ { value: "7d", label: "Last 7 days" }, { value: "30d", label: "Last 30 days" }, { value: "90d", label: "Last quarter" }, { value: "1y", label: "Last year" }, ] const PROGRAM_OPTIONS = [ { value: "all", label: "All programs" }, { value: "nursing", label: "Nursing" }, { value: "pt", label: "PT" }, { value: "ot", label: "OT" }, { value: "pharmacy", label: "Pharmacy" }, ] /* ════════════════════════════════════════════════════════════════════════════ DATA & CHART COMPONENTS ════════════════════════════════════════════════════════════════════════════ */ /* ── Area ─────────────────────────────────────────────────────────────────── */ const areaCfg: ChartConfig = { placements: { label: "Placements", color: BRAND }, applications: { label: "Applications", color: CHART_2 }, reviews: { label: "Reviews", color: CHART_4 }, } const areaData = [ { month: "Aug", placements: 42, applications: 78, reviews: 31 }, { month: "Sep", placements: 58, applications: 91, reviews: 44 }, { month: "Oct", placements: 53, applications: 85, reviews: 39 }, { month: "Nov", placements: 67, applications: 102, reviews: 52 }, { month: "Dec", placements: 49, applications: 76, reviews: 37 }, { month: "Jan", placements: 74, applications: 118, reviews: 60 }, { month: "Feb", placements: 81, applications: 124, reviews: 68 }, { month: "Mar", placements: 89, applications: 137, reviews: 72 }, ] export function AreaChartContent() { return ( {(activeIndex) => ( <>
} /> } />
[d.month, d.placements, d.applications, d.reviews])} /> )}
) } function AreaLineTrendContent() { return ( {(activeIndex) => ( <>
} /> } /> props.index === activeIndex ? : } /> props.index === activeIndex ? : } /> props.index === activeIndex ? : } />
[d.month, d.placements, d.applications, d.reviews])} /> )}
) } /* Selector variant — filter data by period */ const areaDataByPeriod: Record = { "7d": areaData.slice(-2), "30d": areaData.slice(-4), "90d": areaData.slice(-6), "1y": areaData, } function AreaSelectorContent({ filter }: { filter: string }) { const data = areaDataByPeriod[filter] ?? areaData return ( {(activeIndex) => ( <>
} /> } />
[d.month, d.placements, d.applications, d.reviews])} /> )}
) } /* ── Donut ─────────────────────────────────────────────────────────────────── */ const donutCfg: ChartConfig = { confirmed: { label: "Confirmed", color: SUCCESS }, pending: { label: "Pending", color: WARNING }, rejected: { label: "Rejected", color: DESTRUCTIVE }, review: { label: "In Review", color: CHART_1 }, } const donutDataAll = [ { name: "confirmed", value: 58, fill: SUCCESS }, { name: "pending", value: 24, fill: WARNING }, { name: "rejected", value: 9, fill: DESTRUCTIVE }, { name: "review", value: 9, fill: CHART_1 }, ] export function DonutChartContent({ data = donutDataAll }: { data?: typeof donutDataAll }) { const total = data.reduce((s, d) => s + d.value, 0) return ( {(activeIndex) => ( <>
} /> {data.map((d, i) => ( ))}
{data.map((d) => (
{donutCfg[d.name]?.label} {Math.round(d.value / total * 100)}%
))}
{ const raw = donutCfg[d.name]?.label ?? d.name const label = typeof raw === "string" || typeof raw === "number" ? String(raw) : String(d.name) return [label, d.value] as [string, number] })} /> )}
) } const donutBarTrendData = [ { month: "Jan", confirmed: 52, pending: 20, rejected: 7 }, { month: "Feb", confirmed: 60, pending: 18, rejected: 6 }, { month: "Mar", confirmed: 68, pending: 24, rejected: 9 }, ] /* Donut trend — bar chart version */ function DonutBarTrendContent() { return ( } /> } /> ) } /* Donut — selector by program */ const donutByProgram: Record = { all: donutDataAll, nursing: [{ name: "confirmed", value: 72, fill: SUCCESS }, { name: "pending", value: 18, fill: WARNING }, { name: "rejected", value: 5, fill: DESTRUCTIVE }, { name: "review", value: 5, fill: CHART_1 }], pt: [{ name: "confirmed", value: 55, fill: SUCCESS }, { name: "pending", value: 28, fill: WARNING }, { name: "rejected", value: 10, fill: DESTRUCTIVE }, { name: "review", value: 7, fill: CHART_1 }], ot: [{ name: "confirmed", value: 48, fill: SUCCESS }, { name: "pending", value: 30, fill: WARNING }, { name: "rejected", value: 14, fill: DESTRUCTIVE }, { name: "review", value: 8, fill: CHART_1 }], pharmacy: [{ name: "confirmed", value: 40, fill: SUCCESS }, { name: "pending", value: 35, fill: WARNING }, { name: "rejected", value: 15, fill: DESTRUCTIVE }, { name: "review", value: 10, fill: CHART_1 }], } /* ── Grouped Bar ─────────────────────────────────────────────────────────── */ const barCfg: ChartConfig = { new: { label: "New", color: BRAND }, returned: { label: "Returned", color: CHART_2 }, } const barData = [ { program: "Nursing", new: 34, returned: 22 }, { program: "PT", new: 28, returned: 18 }, { program: "OT", new: 21, returned: 14 }, { program: "SW", new: 19, returned: 11 }, { program: "Pharm", new: 15, returned: 9 }, { program: "Rad", new: 12, returned: 7 }, ] export function GroupedBarContent() { return ( {(activeIndex) => ( <>
} /> } />
[d.program, d.new, d.returned])} /> )}
) } function GroupedBarLineTrend() { return ( } /> } /> ) } /* ── Stacked Bar ─────────────────────────────────────────────────────────── */ const stackCfg: ChartConfig = { approved: { label: "Approved", color: SUCCESS }, pending: { label: "Pending", color: WARNING }, rejected: { label: "Rejected", color: DESTRUCTIVE }, } const stackData = [ { month: "Oct", approved: 38, pending: 12, rejected: 4 }, { month: "Nov", approved: 44, pending: 15, rejected: 6 }, { month: "Dec", approved: 31, pending: 8, rejected: 3 }, { month: "Jan", approved: 52, pending: 18, rejected: 7 }, { month: "Feb", approved: 60, pending: 14, rejected: 5 }, { month: "Mar", approved: 68, pending: 20, rejected: 8 }, ] export function StackedBarContent() { return ( {(activeIndex) => ( <>
} /> } />
[d.month, d.approved, d.pending, d.rejected])} /> )}
) } function StackedBarLineTrend() { return (
} /> } />
) } /* ── Line ─────────────────────────────────────────────────────────────────── */ const lineCfg: ChartConfig = { logins: { label: "Logins", color: BRAND }, submissions: { label: "Submissions", color: CHART_2 }, evaluations: { label: "Evaluations", color: CHART_4 }, } const lineData = [ { week: "W1", logins: 148, submissions: 42, evaluations: 29 }, { week: "W2", logins: 162, submissions: 51, evaluations: 35 }, { week: "W3", logins: 139, submissions: 38, evaluations: 27 }, { week: "W4", logins: 175, submissions: 63, evaluations: 48 }, { week: "W5", logins: 182, submissions: 69, evaluations: 52 }, { week: "W6", logins: 196, submissions: 75, evaluations: 58 }, { week: "W7", logins: 211, submissions: 82, evaluations: 63 }, { week: "W8", logins: 204, submissions: 78, evaluations: 60 }, ] const lineDataByPeriod: Record = { "7d": lineData.slice(-2), "30d": lineData.slice(-4), "90d": lineData.slice(-6), "1y": lineData, } export function LineChartContent({ data = lineData }: { data?: typeof lineData }) { return ( {(activeIndex) => ( <>
} /> } /> props.index === activeIndex ? : } /> props.index === activeIndex ? : } /> props.index === activeIndex ? : } />
[d.week, d.logins, d.submissions, d.evaluations])} /> )}
) } function LineAreaTrend() { return ( } /> } /> ) } /* ── Radial Bar ──────────────────────────────────────────────────────────── */ const radialCfg: ChartConfig = { nursing: { label: "Nursing", color: BRAND }, pt: { label: "PT", color: CHART_2 }, ot: { label: "OT", color: SUCCESS }, pharmacy: { label: "Pharmacy", color: WARNING }, social: { label: "Social Work", color: CHART_4 }, } const radialData = [ { name: "nursing", score: 98, fill: BRAND }, { name: "pt", score: 94, fill: CHART_2 }, { name: "ot", score: 91, fill: SUCCESS }, { name: "pharmacy", score: 87, fill: WARNING }, { name: "social", score: 82, fill: CHART_4 }, ] export function RadialBarContent({ data = radialData }: { data?: typeof radialData }) { return ( {(activeIndex) => ( <>
} /> {data.map((d, i) => ( ))}
{data.map((d) => (
{radialCfg[d.name]?.label} {d.score}%
))}
{ const raw = radialCfg[d.name]?.label ?? d.name const label = typeof raw === "string" || typeof raw === "number" ? String(raw) : String(d.name) return [label, `${d.score}%`] as [string, string] })} /> )}
) } const radialLineTrendCfg: ChartConfig = { score: { label: "Current", color: BRAND }, prev: { label: "Previous", color: CHART_2 }, } function RadialLineTrend() { const data = radialData.map((d, i) => ({ name: d.name, score: d.score, prev: d.score - [4, 7, 2, 9, 5][i], })) return ( } /> } /> ) } /** Quota radial — ChartFigure, keyboard tooltip sync, sr-only table (same pattern as RadialBarContent). */ function QuotaRadialGalleryContent({ radial }: { radial: StudentScoreRadial }) { const summary = `Radial gauge for ${radial.title}. Student score ${formatBandScore(radial.studentScore)}. Class average ${formatBandScore(radial.classAverage)}. Scale ${formatBandScore(radial.scaleMin)} to ${formatBandScore(radial.scaleMax)}. ${radial.caption}.` return ( {(activeIndex) => ( <>

Class avg{" "} {formatBandScore(radial.classAverage)} {" "} · scale {formatBandScore(radial.scaleMin)} to {formatBandScore(radial.scaleMax)}

)}
) } /* ── Horizontal Bar ─────────────────────────────────────────────────────── */ const hBarCfg: ChartConfig = { placements: { label: "Placements", color: BRAND }, } const hBarData = [ { site: "City Med", placements: 42 }, { site: "Westside Hosp", placements: 37 }, { site: "North Clinic", placements: 31 }, { site: "Bay Health", placements: 28 }, { site: "Eastview", placements: 22 }, { site: "Lakeshore", placements: 18 }, { site: "Pinehill", placements: 14 }, ] const hBarByPeriod: Record = { "7d": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 0.35) })), "30d": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 0.6) })), "90d": hBarData, "1y": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 4.2) })), } export function HorizontalBarContent({ data = hBarData }: { data?: typeof hBarData }) { return ( {(activeIndex) => ( <>
} />
[d.site, d.placements])} /> )}
) } function HBarLineTrend() { return ( } /> ) } /* ── Composed ─────────────────────────────────────────────────────────────── */ const composedCfg: ChartConfig = { placements: { label: "Placements", color: BRAND }, capacity: { label: "Capacity", color: CHART_3 }, rate: { label: "Fill Rate %", color: CHART_4 }, } const composedData = [ { month: "Sep", placements: 44, capacity: 60, rate: 73 }, { month: "Oct", placements: 53, capacity: 65, rate: 82 }, { month: "Nov", placements: 67, capacity: 80, rate: 84 }, { month: "Dec", placements: 49, capacity: 70, rate: 70 }, { month: "Jan", placements: 74, capacity: 85, rate: 87 }, { month: "Feb", placements: 81, capacity: 90, rate: 90 }, { month: "Mar", placements: 89, capacity: 95, rate: 94 }, ] export function ComposedChartContent() { return ( {(activeIndex) => ( <>
} /> } /> props.index === activeIndex ? : } type="monotone" />
[d.month, d.placements, d.capacity, `${d.rate}%`])} /> )}
) } function ComposedLineTrend() { return ( } /> } /> ) } /* ── Radar ───────────────────────────────────────────────────────────────── */ const radarCfg: ChartConfig = { nursing: { label: "Nursing", color: BRAND }, physical: { label: "PT/OT", color: CHART_2 }, } const radarData = [ { skill: "Clinical", nursing: 92, physical: 88 }, { skill: "Comm.", nursing: 85, physical: 79 }, { skill: "Critical", nursing: 78, physical: 84 }, { skill: "Teamwork", nursing: 91, physical: 90 }, { skill: "Ethics", nursing: 96, physical: 93 }, { skill: "Technical", nursing: 80, physical: 87 }, ] export function RadarChartContent() { return ( {(activeIndex) => ( <>
} /> } /> { const payload = props.payload as { skill?: string } const isPeak = payload?.skill === "Ethics" if (!isPeak) { return } return ( ) }} />
[d.skill, d.nursing, d.physical])} /> )}
) } function RadarBarTrend() { return ( } /> } /> ) } /* ── Scatter ─────────────────────────────────────────────────────────────── */ const scatterCfg: ChartConfig = { nursing: { label: "Nursing", color: BRAND }, pt: { label: "PT", color: CHART_2 }, ot: { label: "OT", color: SUCCESS }, pharmacy: { label: "Pharmacy", color: WARNING }, } const scatterNursing = [{ x: 80, y: 94, z: 42 }, { x: 65, y: 88, z: 35 }, { x: 55, y: 78, z: 28 }, { x: 90, y: 97, z: 51 }, { x: 70, y: 91, z: 38 }] const scatterPT = [{ x: 40, y: 85, z: 22 }, { x: 50, y: 90, z: 27 }, { x: 35, y: 80, z: 18 }, { x: 60, y: 93, z: 31 }] const scatterOT = [{ x: 30, y: 88, z: 16 }, { x: 45, y: 92, z: 24 }, { x: 38, y: 84, z: 19 }] const scatterPharmacy = [{ x: 25, y: 76, z: 12 }, { x: 35, y: 82, z: 17 }, { x: 20, y: 71, z: 9 }] export function ScatterChartContent() { return (
} /> } /> { const payload = props.payload as { x?: number; y?: number } const isPeak = payload?.x === SCATTER_LEO_PEAK.x && payload?.y === SCATTER_LEO_PEAK.y return ( ) }} />
) } const scatterLineTrendCfg: ChartConfig = { nursing: { label: "Nursing", color: BRAND }, pt: { label: "PT", color: CHART_2 }, } const scatterLineTrendData = [ { month: "Oct", nursing: 88, pt: 80 }, { month: "Nov", nursing: 91, pt: 82 }, { month: "Dec", nursing: 89, pt: 79 }, { month: "Jan", nursing: 93, pt: 84 }, { month: "Feb", nursing: 95, pt: 87 }, { month: "Mar", nursing: 94, pt: 85 }, ] function ScatterLineTrend() { return ( } /> } /> ) } /* ── Funnel ──────────────────────────────────────────────────────────────── */ const funnelCfg: ChartConfig = { applied: { label: "Applied", color: BRAND }, screened: { label: "Screened", color: CHART_2 }, matched: { label: "Matched", color: SUCCESS }, placed: { label: "Placed", color: CHART_4 }, completed: { label: "Completed", color: CHART_5 }, } const funnelData = [ { name: "Applied", value: 320, fill: BRAND }, { name: "Screened", value: 240, fill: CHART_2 }, { name: "Matched", value: 175, fill: SUCCESS }, { name: "Placed", value: 128, fill: CHART_4 }, { name: "Completed", value: 98, fill: CHART_5 }, ] const funnelDataByPeriod: Record = { "7d": funnelData.map((d) => ({ ...d, value: Math.round(d.value * 0.08) })), "30d": funnelData.map((d) => ({ ...d, value: Math.round(d.value * 0.3) })), "90d": funnelData, "1y": funnelData.map((d) => ({ ...d, value: d.value * 4 })), } export function FunnelChartContent({ data = funnelData }: { data?: typeof funnelData }) { const summary = `Funnel with ${data.length} stages from ${data[0]?.name ?? ""} to ${data[data.length - 1]?.name ?? ""}.` return ( {(activeIndex) => ( <>
} /> {data.map((d, i) => ( ))}
[d.name, d.value])} /> )}
) } const funnelLineTrendCfg: ChartConfig = { applied: { label: "Applied", color: BRAND }, placed: { label: "Placed", color: CHART_4 }, completed: { label: "Completed", color: CHART_5 }, } const funnelLineTrendData = [ { month: "Oct", applied: 210, placed: 95, completed: 68 }, { month: "Nov", applied: 245, placed: 108, completed: 82 }, { month: "Dec", applied: 180, placed: 88, completed: 64 }, { month: "Jan", applied: 280, placed: 120, completed: 91 }, { month: "Feb", applied: 300, placed: 124, completed: 95 }, { month: "Mar", applied: 320, placed: 128, completed: 98 }, ] function FunnelLineTrend() { return ( } /> } /> ) } export const CHART_GALLERY_LEO_DONUT: ChartLeoInsight = { headline: "Confirmed placements dominate the current pipeline", explanation: "87% of placements are already confirmed, with only 9% pending and 4% in review. This is a healthy distribution suggesting strong conversion from applications to confirmed offers.", kind: "spike", delta: { value: "+12%", label: "vs. last month" }, bullets: [ "Confirmed count has grown steadily across nursing, PT, and OT programs.", "Rejection rate remains low at 1%. Applications are well-qualified.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_APPLICATIONS: ChartLeoInsight = { headline: "Nursing program leads application volume", explanation: "Nursing consistently attracts the most new applicants, with 34 this period. PT and OT follow closely. Returned applications suggest strong re-engagement.", kind: "trend", delta: { value: "+8%", label: "new vs. prior period" }, bullets: [ "Nursing: 34 new, 22 returned. Highest volume and strong re-engagement.", "PT and OT: steady demand. Balanced load across clinical programs.", ], anchor: { xValue: "Nursing", yDataKeys: ["new", "returned"], yCombine: "sum" }, } export const CHART_GALLERY_LEO_LINE: ChartLeoInsight = { headline: "Portal activity peaks mid-week", explanation: "Login, submission, and evaluation activity cluster from Tuesday to Thursday, with weekends showing predictable dips. This pattern is consistent and expected for an academic schedule.", kind: "trend", delta: { value: "", label: "stable pattern" }, bullets: [ "Logins peak at ~450 on Wednesdays.", "Submissions highest Monday to Friday, near-zero on weekends.", ], anchor: { xValue: "W7", yDataKeys: ["logins"], yValue: 211 }, } export const CHART_GALLERY_LEO_COMPLIANCE: ChartLeoInsight = { headline: "PT/OT programs lead compliance scoring", explanation: "PT and OT average 88% to 89% compliance, outpacing Nursing (82%) and Pharmacy (76%). Radiology lags at 71% and may need targeted support.", kind: "dip", delta: { value: "-8%", label: "Radiology vs. PT/OT" }, bullets: [ "PT/OT: consistent excellence across all 6 dimensions.", "Pharmacy: scoring gaps in documentation and timeliness.", "Radiology: needs support in scheduling and follow-up processes.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_HORIZONTAL: ChartLeoInsight = { headline: "Large clinical sites carry most placements", explanation: "The three largest sites (University Hospital, Metro Clinic, Regional Center) account for 58% of all placements. Mid-size sites are under-utilized.", kind: "anomaly", delta: { value: "+22%", label: "top 3 sites total" }, bullets: [ "University Hospital: 156 placements (28% of total).", "Capacity constraints may limit placement growth at smaller sites.", ], anchor: { xValue: "City Med", yDataKeys: ["placements"] }, } export const CHART_GALLERY_LEO_COMPOSED: ChartLeoInsight = { headline: "Site capacity is healthy; fill rates peak Q2", explanation: "Most sites run 85% to 92% capacity utilization. Fill rate (placements / capacity) averages 78%, with spring months (Feb to Mar) consistently hitting 82%+.", kind: "spike", delta: { value: "+6%", label: "fill rate increase" }, bullets: [ "March shows the strongest fill rate at 84%.", "Only 2 sites are below 70% utilization. There is room to rebalance.", ], anchor: { xValue: "Mar", yDataKeys: ["rate"], yValue: 94 }, } export const CHART_GALLERY_LEO_RADAR: ChartLeoInsight = { headline: "Nursing and PT/OT competencies are well-balanced", explanation: "Both programs score 80+ on all six dimensions. Nursing edges slightly on patient care; PT/OT lead in mobility and assessment. Ready for expanded placements.", kind: "trend", delta: { value: "", label: "strong across programs" }, bullets: [ "6-dimension average: Nursing 84%, PT/OT 86%.", "Lowest dimension: patient care (Nursing 79%). Room to develop.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_SCATTER: ChartLeoInsight = { headline: "Application-to-placement funnel is healthy", explanation: "Applications feed steadily into offers; offer-to-confirmation conversion hovers around 72%. A small number of dropouts from offer-to-start, typical for clinical placements.", kind: "trend", delta: { value: "+4%", label: "confirmation rate" }, bullets: [ "Applications → Offers: 63% convert (typical for competitive placements).", "Offers → Confirmed: 72% accept (strong acceptance rate).", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_FUNNEL: ChartLeoInsight = { headline: "Funnel shape is expected; strong at top of pipe", explanation: "4,200 applications narrow to 842 offers (20% funnel rate) and 604 confirmed placements (72% offer acceptance). Losses are proportional, with no anomalous drops.", kind: "trend", delta: { value: "+8%", label: "application volume" }, bullets: [ "Application → Offer: drop-off is typical for screening.", "Offer → Confirmed: acceptance rate of 72% is healthy.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_QUOTA: ChartLeoInsight = { headline: "Student performance tracking and cohort comparison", explanation: "Track individual student progress against class averages and scale benchmarks. Identify outliers above or below cohort norms.", kind: "anomaly", bullets: [ "Performance visualized on a consistent scale across cohorts.", "Class average provides immediate context for comparison.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_TRENDS: ChartLeoInsight = { headline: "December dips across placements, applications, and reviews", explanation: "All three series pull back in December. That is often seasonal (holidays, academic breaks) or a real pipeline stall. Worth confirming whether approvals or site capacity paused.", kind: "dip", delta: { value: "-24%", label: "vs. November" }, bullets: [ "Placements are 18% below the 6-month trailing average.", "Reviews dropped sharply in the last 2 weeks of the month.", "Same pattern appeared in Dec '24, so a seasonal signal is plausible.", ], anchor: { xValue: "Dec", yDataKeys: ["placements", "applications", "reviews"], yCombine: "max", }, } export const CHART_GALLERY_LEO_REVIEWS: ChartLeoInsight = { headline: "December is the low point in review throughput", explanation: "Totals drop before recovering. Worth confirming whether fewer submissions arrived or reviewers were out. Pending and rejected slices still matter once volume returns.", kind: "dip", delta: { value: "-31%", label: "vs. November total" }, bullets: [ "Approved reviews fell from 68 to 47 month-over-month.", "Pending queue grew by 9 items. A backlog is forming.", "Two reviewers were OOO for most of the last two weeks.", ], anchor: { xValue: "Dec", yDataKeys: ["approved", "pending", "rejected"], yCombine: "sum", }, } export const CHART_GALLERY_LEO_WATERFALL: ChartLeoInsight = { headline: "New placements drove the largest positive step this cycle", explanation: "After opening at 120 placements, the New step adds 44. That is the biggest single increase before renewals and closures adjust the total to 158.", kind: "spike", delta: { value: "+37%", label: "vs. opening total" }, bullets: [ "New (+44) is the largest upward move in the sequence.", "Expired (−22) and Closed (−15) are the main reductions after growth.", ], anchor: { xValue: "New", yDataKeys: ["total"] }, } export const CHART_GALLERY_LEO_TREEMAP: ChartLeoInsight = { headline: "Nursing occupies the largest share of program volume", explanation: "The treemap shows Nursing at 34, roughly one-third of the combined program share, with PT and OT as the next largest tiles.", kind: "trend", delta: { value: "34", label: "Nursing share" }, bullets: [ "Tile area tracks proportional share. Nursing leads by a wide margin.", "Smaller programs still appear as readable tiles with labels.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_HEATMAP: ChartLeoInsight = { headline: "Wednesday afternoon shows the busiest activity window", explanation: "Mid-week cells from 12p to 2p run hotter than early-morning slots. Useful when scheduling coordinator coverage.", kind: "spike", delta: { value: "53", label: "Wed 2p peak" }, bullets: [ "Peak intensity clusters on Wed between 12p and 2p.", "Monday mornings stay lighter, which is good for async review work.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_SANKEY: ChartLeoInsight = { headline: "Screening retains most of the application volume", explanation: "240 applications flow into screening; the widest link is Applied → Screened, with expected tapering through placement stages.", kind: "trend", delta: { value: "240", label: "Applied → Screened" }, bullets: [ "Applied → Screened is the highest-volume transition.", "Placed → Completed shows healthy completion conversion.", ], anchor: chartLeoPeakAnchor(), } export const CHART_GALLERY_LEO_TIMELINE: ChartLeoInsight = { headline: "Placement completes around day 56 on average", explanation: "Milestones cluster on a single lane with the longest gap between Placed and Complete. Worth monitoring for cycle-time SLAs.", kind: "trend", delta: { value: "56d", label: "to complete" }, bullets: [ "Matched → Placed spans ~13 days in this sample.", "Complete lands near day 56. Use that for cohort planning.", ], anchor: { xValue: "56", yDataKeys: ["lane"], yValue: 1 }, } export const CHART_GALLERY_LEO_BUBBLE: ChartLeoInsight = { headline: "West region combines high fill rate with strong student volume", explanation: "The West bubble sits high on fill rate (82%) with 52 students, an efficient site cluster worth replicating.", kind: "spike", bullets: [ "West: 82% fill rate at mid-range capacity.", "South shows low fill despite available capacity. Investigate demand.", ], anchor: { xValue: "West", yDataKeys: ["fillRate"], yValue: 82 }, } export const CHART_GALLERY_LEO_BOXPLOT: ChartLeoInsight = { headline: "Nursing scores show the tightest interquartile spread", explanation: "Nursing’s Q1 to Q3 band is narrow relative to Pharm. Performance is consistent across the cohort.", kind: "trend", delta: { value: "Q1 to Q3", label: "Nursing band" }, bullets: [ "Nursing median sits highest in the set.", "Pharm shows the widest whisker span, with more variance to investigate.", ], anchor: { xValue: "Nursing", yDataKeys: ["median"] }, } export const CHART_GALLERY_LEO_RANGE: ChartLeoInsight = { headline: "May shows the widest capacity band in the series", explanation: "The range between low and high capacity peaks in May. Plan staffing for the upper band while monitoring June tightening.", kind: "trend", delta: { value: "91", label: "May high" }, bullets: [ "May high band reaches 91 on this scale.", "June compresses slightly. Confirm whether demand eased.", ], anchor: { xValue: "May", yDataKeys: ["high"] }, } export const CHART_GALLERY_LEO_BULLET: ChartLeoInsight = { headline: "Compliance is closest to its target of the three KPIs", explanation: "Compliance at 86% is 6 points under the 92% target, a narrower gap than Placements or Reviews.", kind: "dip", delta: { value: "−6 pts", label: "vs. compliance target" }, bullets: [ "Compliance: 86 actual vs. 92 target.", "Reviews trail furthest. Prioritize review throughput.", ], anchor: { xValue: "Compliance", yDataKeys: ["value"], yValue: 86 }, } export { CATALOG_PREVIEW_CHART_LEO } from "@/lib/catalog-preview-chart-leo" function ChartRows({ v }: { v: ChartCardVariant }) { const isTabs = v === "tabs" const isSel = v === "selector" const isMT = v === "metrics-tabs" const isKpi = v === "kpi-chart" return ( <> {/* Row 1 · Area (2/3) + Donut (1/3) */}
{isSel ? ( {(f) => } ) : ( } tabOptions={isTabs ? [ { value: "overview", label: "Overview" }, { value: "by-program", label: "By program" }, { value: "trend", label: "Trend" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Placements", value: "89", trend: "up" }, { label: "Fill rate", value: "94%", trend: "up" }, { label: "Avg. weeks", value: "6.2", trend: "neutral" }, ] : undefined}> {isTabs ? (tab: string) => tab === "trend" ? : : } )}
{isSel ? ( {(f) => } ) : ( } tabOptions={isTabs ? [ { value: "current", label: "Current cycle" }, { value: "previous", label: "Previous cycle" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Placed", value: "128", trend: "up" }, { label: "Pending", value: "23", trend: "down" }, ] : undefined}> {isTabs ? (tab: string) => : } )}
{/* Row 2 · Grouped Bar + Stacked Bar */}
{isSel ? ( {() => } ) : ( } tabOptions={isTabs ? [ { value: "all", label: "All students" }, { value: "new", label: "New" }, { value: "trend", label: "Trend" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Total", value: "320", trend: "up" }, { label: "New", value: "78%", trend: "up" }, { label: "Returning", value: "22%", trend: "neutral" }, ] : undefined}> {isTabs ? (tab: string) => tab === "trend" ? : : } )} {isSel ? ( {() => } ) : ( } tabOptions={isTabs ? [ { value: "status", label: "By status" }, { value: "reviewer", label: "By reviewer" }, { value: "trend", label: "Trend" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Approved", value: "68", trend: "up" }, { label: "Pending", value: "14", trend: "down" }, { label: "Rejected", value: "6", trend: "neutral" }, ] : undefined}> {isTabs ? (tab: string) => tab === "trend" ? : : } )}
{/* Row 3 · Line (2/3) + Radial (1/3) */}
{isSel ? ( {(f) => } ) : ( } tabOptions={isTabs ? [ { value: "weekly", label: "Weekly" }, { value: "monthly", label: "Monthly" }, { value: "trend", label: "Trend" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Logins", value: "1.2k", trend: "up" }, { label: "Submissions", value: "340", trend: "up" }, { label: "Evals", value: "88", trend: "neutral" }, ] : undefined}> {isTabs ? (tab: string) => tab === "trend" ? : : } )}
{isSel ? ( {() => } ) : ( } tabOptions={isTabs ? [ { value: "current", label: "Current" }, { value: "historical", label: "Historical" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Avg. score", value: "91%", trend: "up" }, { label: "At risk", value: "3", trend: "down" }, ] : undefined}> {isTabs ? (tab: string) => tab === "historical" ? : : } )}
{/* Row 4 · H-Bar (1/3) + Composed (2/3) */}
{isSel ? ( {(f) => } ) : ( } tabOptions={isTabs ? [ { value: "by-facility", label: "By facility" }, { value: "by-capacity", label: "By capacity" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Sites", value: "7", trend: "up" }, { label: "Capacity", value: "94%", trend: "up" }, ] : undefined}> {isTabs ? () => : } )}
{isSel ? ( {() => } ) : ( } tabOptions={isTabs ? [ { value: "overlay", label: "Overlay" }, { value: "comparison", label: "Side by side" }, { value: "trend", label: "Trend" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Fill rate", value: "94%", trend: "up" }, { label: "Capacity", value: "95", trend: "up" }, { label: "Placed", value: "89", trend: "up" }, ] : undefined}> {isTabs ? (tab: string) => tab === "trend" ? : : } )}
{/* Row 5 · Radar (1/3) + Scatter (2/3) */}
{isSel ? ( {() => } ) : ( } tabOptions={isTabs ? [ { value: "radar", label: "Radar" }, { value: "breakdown", label: "Breakdown" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Avg.", value: "88%", trend: "up" }, { label: "Top", value: "Clinical", trend: "neutral" }, ] : undefined}> {isTabs ? (tab: string) => tab === "breakdown" ? : : } )}
{isSel ? ( {() => } ) : ( } tabOptions={isTabs ? [ { value: "scatter", label: "Scatter" }, { value: "ranking", label: "Ranking" }, { value: "trend", label: "Trend" }, ] : undefined} miniMetrics={(isMT || isKpi) ? [ { label: "Sites", value: "12", trend: "up" }, { label: "Avg. rate", value: "87%", trend: "up" }, { label: "Students", value: "320", trend: "up" }, ] : undefined}> {isTabs ? (tab: string) => tab === "trend" ? : : } )}
{/* Row 6 · Quota reference charts (band + radial demos) */}
{DASHBOARD_STUDENT_SCORES.metrics.map((m) => (
))}
{/* Row 7 · Funnel full width */} {isSel ? ( {(f) => } ) : ( } miniMetrics={(isMT || isKpi) ? [ { label: "Applied", value: "320", trend: "up" }, { label: "Placed", value: "128", trend: "up" }, { label: "Completed", value: "98", trend: "up" }, { label: "Drop-off", value: "69%", trend: "down" }, ] : undefined}> )} ) } /* ════════════════════════════════════════════════════════════════════════════ Design-system chart type previews (gallery bodies) ════════════════════════════════════════════════════════════════════════════ */ export function ChartQuotaRadialPreview() { return } /* ════════════════════════════════════════════════════════════════════════════ Main export ════════════════════════════════════════════════════════════════════════════ */ export function ChartsOverview({ variant = "normal" }: { variant?: ChartCardVariant }) { return (
) }