@exxatdesignux/ui 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
@@ -0,0 +1,2321 @@
1
+ "use client"
2
+
3
+ /**
4
+ * ChartsOverview — Dashboard chart gallery
5
+ *
6
+ * ── ChartCard variants ───────────────────────────────────────────────────────
7
+ * normal — plain card with Ask Leo
8
+ * tabs — "Chart" | "Trend (Line)" tabs + Ask Leo
9
+ * selector — quick-filter Select + Ask Leo
10
+ * metrics-tabs — metric cells ARE the tab triggers (label + value + trend)
11
+ *
12
+ * ── ASK LEO ICON GUIDELINE ───────────────────────────────────────────────────
13
+ * Always use: <i className="fa-duotone fa-solid fa-star-christmas" />
14
+ * Never use: fa-wand-magic-sparkles (retired, inconsistent)
15
+ * Size: text-xs (11px via --text-xs) with aria-hidden="true"
16
+ * Label: "Ask Leo" (never truncate or omit the text label)
17
+ * Applies to: ALL Ask Leo buttons across the entire app —
18
+ * ChartCard headers, KeyMetrics card, GreetingWidget, NavUser, etc.
19
+ *
20
+ * ── WCAG AA STANDARDS FOR GRAPHS ─────────────────────────────────────────────
21
+ * 1. Container landmark
22
+ * • Wrap each chart in a <figure> (or div with role="figure") +
23
+ * aria-label="<chart title>" + aria-describedby="<id of summary>"
24
+ * • Add a visually-hidden <figcaption id="<id>"> with a plain-text
25
+ * summary of the key trend (e.g. "Placements rose 12% in Q1 2026").
26
+ *
27
+ * 2. Keyboard navigation
28
+ * • The ChartContainer wrapper must have tabIndex={0} so it receives focus.
29
+ * • On focus, announce title + summary via aria-label / aria-describedby.
30
+ * • Arrow keys (←/→) cycle through data points; announce value via
31
+ * a live region (role="status" aria-live="polite").
32
+ * • Esc clears the selection and returns focus to the container.
33
+ *
34
+ * 3. Accessible data table (hidden fallback)
35
+ * • Immediately after the SVG/canvas, render a <table> wrapped in
36
+ * <span className="sr-only"> (visually hidden, in DOM).
37
+ * • Columns mirror the chart axes; each data point is a <td>.
38
+ * • Screen-reader users can navigate data with standard table shortcuts.
39
+ *
40
+ * 4. Colour & contrast
41
+ * • Chart series colours must achieve ≥ 3:1 contrast against the card bg.
42
+ * • Never use colour as the ONLY differentiator — pair with:
43
+ * - Dashed vs solid line strokes
44
+ * - Direct inline labels on lines/segments
45
+ * - Shape markers on data points (circle vs square vs triangle)
46
+ * • Text labels inside charts: ≥ 4.5:1 on their local background.
47
+ *
48
+ * 5. Focus ring on data points
49
+ * • Active/focused data point: 3px outline, ≥ 3:1 contrast, distinct
50
+ * from the hover state (use outline-offset to separate).
51
+ *
52
+ * 6. Tooltip accessibility
53
+ * • Tooltips must appear on keyboard focus, not only on mouse hover.
54
+ * • Tooltip content must be announced to the live region.
55
+ * • Tooltip must remain visible while it has focus (no auto-dismiss).
56
+ * ─────────────────────────────────────────────────────────────────────────────
57
+ */
58
+
59
+ import * as React from "react"
60
+ import {
61
+ Area, AreaChart,
62
+ Bar, BarChart,
63
+ CartesianGrid,
64
+ Cell,
65
+ ComposedChart,
66
+ Funnel, FunnelChart, LabelList,
67
+ Line, LineChart,
68
+ Pie, PieChart,
69
+ PolarAngleAxis, PolarGrid, PolarRadiusAxis,
70
+ Radar, RadarChart,
71
+ RadialBar, RadialBarChart,
72
+ Scatter, ScatterChart,
73
+ XAxis, YAxis, ZAxis,
74
+ type DotProps,
75
+ } from "recharts"
76
+ import {
77
+ QuotaLinearProgressCardBody,
78
+ QuotaRadialChartInner,
79
+ } from "@/components/dashboard-quota-progress-card"
80
+ import {
81
+ DASHBOARD_STUDENT_SCORES,
82
+ formatBandScore,
83
+ type StudentScoreRadial,
84
+ } from "@/lib/mock/dashboard"
85
+ import {
86
+ Card, CardContent, CardDescription, CardHeader, CardTitle,
87
+ } from "@/components/ui/card"
88
+ import {
89
+ ChartContainer,
90
+ ChartLegend,
91
+ ChartLegendContent,
92
+ ChartTooltip,
93
+ chartTooltipKeyboardSyncProps,
94
+ ChartTooltipContent,
95
+ type ChartConfig,
96
+ } from "@/components/ui/chart"
97
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
98
+ import {
99
+ Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
100
+ } from "@/components/ui/select"
101
+ import { Button } from "@/components/ui/button"
102
+ import { AskLeoShortcutKbds, useAskLeo } from "@/components/ask-leo-sidebar"
103
+ import {
104
+ Tooltip,
105
+ TooltipContent,
106
+ TooltipTrigger,
107
+ } from "@/components/ui/tooltip"
108
+ import { isEditableTarget } from "@/lib/editable-target"
109
+ import { chartLineStrokeDash } from "@/lib/chart-line-dash"
110
+ import { cn } from "@/lib/utils"
111
+
112
+ /** Recharts passes `index` into Line `dot` renderers; published `DotProps` omits it. */
113
+ type LineDotRenderProps = DotProps & { index?: number }
114
+
115
+ /* ── Colour tokens ────────────────────────────────────────────────────────── */
116
+ const BRAND = "var(--brand-color)"
117
+ const CHART_1 = "var(--color-chart-1)"
118
+ const CHART_2 = "var(--color-chart-2)"
119
+ const CHART_3 = "var(--color-chart-3)"
120
+ const CHART_4 = "var(--color-chart-4)"
121
+ const CHART_5 = "var(--color-chart-5)"
122
+ const SUCCESS = "var(--chart-2)"
123
+ const WARNING = "var(--chart-4)"
124
+ const DESTRUCTIVE = "var(--destructive)"
125
+
126
+ /* ── Period filter options (reused across selector cards) ─────────────────── */
127
+ const PERIOD_OPTIONS = [
128
+ { value: "7d", label: "Last 7 days" },
129
+ { value: "30d", label: "Last 30 days" },
130
+ { value: "90d", label: "Last quarter" },
131
+ { value: "1y", label: "Last year" },
132
+ ]
133
+
134
+ const PROGRAM_OPTIONS = [
135
+ { value: "all", label: "All programs" },
136
+ { value: "nursing", label: "Nursing" },
137
+ { value: "pt", label: "PT" },
138
+ { value: "ot", label: "OT" },
139
+ { value: "pharmacy", label: "Pharmacy" },
140
+ ]
141
+
142
+ /* ════════════════════════════════════════════════════════════════════════════
143
+ REUSABLE ChartCard — supports 3 variants
144
+ ════════════════════════════════════════════════════════════════════════════ */
145
+
146
+ export type ChartCardVariant = "normal" | "tabs" | "selector" | "metrics-tabs" | "kpi-chart"
147
+
148
+ /** ChartCard tabs no longer force `text-xs` — use default `text-sm` scale and ≥24px hit area. */
149
+ const chartCardTabTriggerClass = "min-h-9 px-3 py-2 text-sm gap-2"
150
+
151
+ import {
152
+ LeoInsightIndicator,
153
+ LEO_TOKENS,
154
+ type ChartLeoInsight,
155
+ type ChartLeoInsightAnchor,
156
+ type ChartLeoInsightKind,
157
+ } from "@/components/leo-insight-indicator"
158
+ export type { ChartLeoInsight, ChartLeoInsightAnchor, ChartLeoInsightKind }
159
+
160
+ type ChartLeoInsightBundle = { insight: ChartLeoInsight; chartTitle: string }
161
+
162
+ const ChartLeoInsightContext = React.createContext<ChartLeoInsightBundle | null>(null)
163
+
164
+ function resolveChartLeoAnchorY(
165
+ row: Record<string, unknown>,
166
+ xDataKey: string,
167
+ anchor: ChartLeoInsightAnchor,
168
+ ): number | null {
169
+ if (typeof anchor.yValue === "number" && !Number.isNaN(anchor.yValue)) {
170
+ return anchor.yValue
171
+ }
172
+ const keys =
173
+ anchor.yDataKeys?.filter((k) => k !== xDataKey) ??
174
+ Object.keys(row).filter((k) => k !== xDataKey)
175
+ const nums = keys
176
+ .map((k) => row[k])
177
+ .filter((v): v is number => typeof v === "number" && !Number.isNaN(v))
178
+ if (nums.length === 0) return null
179
+ const combine = anchor.yCombine ?? "max"
180
+ return combine === "sum" ? nums.reduce((a, b) => a + b, 0) : Math.max(...nums)
181
+ }
182
+
183
+ function chartLeoNumericDomainMax(
184
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>,
185
+ xDataKey: string,
186
+ ): number {
187
+ let m = 0
188
+ for (const row of data) {
189
+ for (const [k, v] of Object.entries(row)) {
190
+ if (k === xDataKey) continue
191
+ if (typeof v === "number" && !Number.isNaN(v) && v > m) m = v
192
+ }
193
+ }
194
+ return m > 0 ? m : 1
195
+ }
196
+
197
+ /**
198
+ * Static brand-coloured dot drawn on the exact data point Leo is calling out.
199
+ * A card-coloured knockout ring keeps it readable on top of grid lines and
200
+ * area fills. No pulsing animation — the dashed connector line + chip do the
201
+ * attention work, and this keeps the chart calm.
202
+ */
203
+ function LeoPlotPointDot() {
204
+ return (
205
+ <span
206
+ aria-hidden
207
+ className={cn("block size-2.5 rounded-full", LEO_TOKENS.dotClass)}
208
+ style={{
209
+ boxShadow: `0 0 0 3px oklch(from var(--card) l c h / 0.95)`,
210
+ }}
211
+ />
212
+ )
213
+ }
214
+
215
+ /**
216
+ * Read the Recharts SVG rendered by a sibling `ChartContainer` and project
217
+ * the insight anchor's `(xValue, yNum)` into pixel coordinates relative to
218
+ * the overlay's wrapper.
219
+ *
220
+ * Strategy — chart-type-agnostic:
221
+ * 1. Find the `<svg>` inside the parent (`.relative` wrapper).
222
+ * 2. Plot rect = bounding box of `.recharts-cartesian-grid`.
223
+ * Fallback = area between y-axis right edge and x-axis top edge.
224
+ * 3. X position = matching x-axis tick's centre, matched by text content.
225
+ * Fallback = `(idx + 0.5) / n * plotWidth` band formula.
226
+ * 4. Y position = interpolated from y-axis tick values (handles non-zero
227
+ * domain bases automatically — e.g. recharts auto-domains that start at
228
+ * non-zero and charts with cropped y-ranges). Fallback = `1 - y/yMax`.
229
+ *
230
+ * Works on: Line/Area/Bar/StackedBar/Composed charts — anything with Cartesian
231
+ * axes. Pie/Radar/Funnel charts don't expose axes, so the overlay skips with
232
+ * a null return (anchor concept isn't meaningful there).
233
+ */
234
+ function useChartAnchorPixelPosition({
235
+ xValue,
236
+ xDataKey,
237
+ yNum,
238
+ data,
239
+ }: {
240
+ xValue: string
241
+ xDataKey: string
242
+ yNum: number
243
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>
244
+ }) {
245
+ const ref = React.useRef<HTMLDivElement>(null)
246
+ const [pos, setPos] = React.useState<{ x: number; y: number; plotTop: number } | null>(null)
247
+
248
+ React.useEffect(() => {
249
+ const el = ref.current
250
+ if (!el) return
251
+ const parent = el.parentElement
252
+ if (!parent) return
253
+
254
+ const compute = () => {
255
+ const svg = parent.querySelector("svg") as SVGSVGElement | null
256
+ if (!svg) return
257
+ const parentRect = parent.getBoundingClientRect()
258
+
259
+ const toLocal = (el: Element) => {
260
+ const r = (el as SVGGraphicsElement).getBoundingClientRect()
261
+ return {
262
+ left: r.left - parentRect.left,
263
+ right: r.right - parentRect.left,
264
+ top: r.top - parentRect.top,
265
+ bottom: r.bottom - parentRect.top,
266
+ width: r.width,
267
+ height: r.height,
268
+ cx: r.left + r.width / 2 - parentRect.left,
269
+ cy: r.top + r.height / 2 - parentRect.top,
270
+ }
271
+ }
272
+
273
+ // Plot rect — prefer cartesian-grid; fall back to axis bounds.
274
+ const grid = svg.querySelector(".recharts-cartesian-grid")
275
+ const xAxis = svg.querySelector(".recharts-xAxis")
276
+ const yAxis = svg.querySelector(".recharts-yAxis")
277
+ if (!xAxis || !yAxis) return
278
+
279
+ const plot = grid ? toLocal(grid) : (() => {
280
+ const y = toLocal(yAxis)
281
+ const x = toLocal(xAxis)
282
+ return {
283
+ left: y.right, right: x.right, top: y.top,
284
+ bottom: x.top, width: x.right - y.right, height: x.top - y.top,
285
+ cx: 0, cy: 0,
286
+ }
287
+ })()
288
+
289
+ // X position: find matching x-tick by text content (chart-agnostic).
290
+ const xTicks = Array.from(
291
+ xAxis.querySelectorAll(".recharts-cartesian-axis-tick"),
292
+ ) as SVGGElement[]
293
+ let xPx: number | null = null
294
+ for (const t of xTicks) {
295
+ if ((t.textContent ?? "").trim() === xValue) {
296
+ xPx = toLocal(t).cx
297
+ break
298
+ }
299
+ }
300
+ if (xPx === null) {
301
+ const idx = data.findIndex((d) => String(d[xDataKey]) === xValue)
302
+ if (idx < 0) return
303
+ xPx = plot.left + ((idx + 0.5) / Math.max(data.length, 1)) * plot.width
304
+ }
305
+
306
+ // Y position: interpolate from y-axis tick values (handles non-zero domains).
307
+ const yTickEls = Array.from(
308
+ yAxis.querySelectorAll(".recharts-cartesian-axis-tick"),
309
+ ) as SVGGElement[]
310
+ const yTickPairs: Array<{ v: number; y: number }> = []
311
+ for (const t of yTickEls) {
312
+ const raw = (t.textContent ?? "").trim()
313
+ if (!raw) continue
314
+ const v = parseFloat(raw.replace(/[^0-9.\-]/g, ""))
315
+ if (Number.isNaN(v)) continue
316
+ yTickPairs.push({ v, y: toLocal(t).cy })
317
+ }
318
+ let yPx: number | null = null
319
+ if (yTickPairs.length >= 2) {
320
+ const sorted = [...yTickPairs].sort((a, b) => a.v - b.v)
321
+ const lo = sorted[0], hi = sorted[sorted.length - 1]
322
+ if (hi.v !== lo.v) {
323
+ yPx = lo.y + ((yNum - lo.v) / (hi.v - lo.v)) * (hi.y - lo.y)
324
+ }
325
+ }
326
+ if (yPx === null) {
327
+ // Conservative fallback when y-axis ticks cannot be parsed.
328
+ const yMax = chartLeoNumericDomainMax(data, xDataKey)
329
+ yPx = plot.top + (1 - yNum / yMax) * plot.height
330
+ }
331
+
332
+ setPos({ x: xPx, y: yPx, plotTop: plot.top })
333
+ }
334
+
335
+ // Recharts mounts/animates after our first paint; measure a few times.
336
+ compute()
337
+ let raf1 = requestAnimationFrame(() => {
338
+ compute()
339
+ raf1 = requestAnimationFrame(compute)
340
+ })
341
+
342
+ const ro = new ResizeObserver(compute)
343
+ ro.observe(parent)
344
+
345
+ const mo = new MutationObserver(compute)
346
+ mo.observe(parent, {
347
+ childList: true,
348
+ subtree: true,
349
+ attributes: true,
350
+ attributeFilter: ["width", "height", "transform", "d", "x", "y"],
351
+ })
352
+
353
+ return () => {
354
+ cancelAnimationFrame(raf1)
355
+ ro.disconnect()
356
+ mo.disconnect()
357
+ }
358
+ }, [xValue, xDataKey, yNum, data])
359
+
360
+ return { ref, pos }
361
+ }
362
+
363
+ /**
364
+ * HTML overlay on the chart plot (sibling of `ChartContainer`, inside a `relative` wrapper).
365
+ *
366
+ * Visual structure, top → bottom:
367
+ * 1. Chip (`LeoInsightIndicator` in plot-marker layout) — floats above
368
+ * 2. Dashed connector line in the kind colour joining chip to dot
369
+ * 3. Pulsing dot anchored on the real data point
370
+ *
371
+ * Positioning is measured from the Recharts SVG at runtime (see
372
+ * `useChartAnchorPixelPosition`) so the dot lands on the actual data point
373
+ * regardless of chart type, y-domain, or plot margin.
374
+ */
375
+ export function ChartLeoPlotInsightOverlay({
376
+ data,
377
+ xDataKey,
378
+ markerLiftPx = 44,
379
+ }: {
380
+ data: ReadonlyArray<Record<string, string | number | null | undefined>>
381
+ xDataKey: string
382
+ /** @deprecated retained for call-site compatibility. */
383
+ insetPct?: { left: number; right: number; top: number; bottom: number }
384
+ /** @deprecated retained for call-site compatibility. */
385
+ xAxisLabelReservePct?: number
386
+ /** @deprecated retained for call-site compatibility. */
387
+ markerLiftPct?: number
388
+ /** @deprecated retained for call-site compatibility. */
389
+ markerLiftExtraPx?: number
390
+ /** Vertical distance from dot to the bottom of the floating chip, in px. */
391
+ markerLiftPx?: number
392
+ }) {
393
+ // Lift the chip well clear of the default Recharts tooltip so they never
394
+ // fight for the same cursor area on hover.
395
+ const effectiveLift = markerLiftPx ?? 56
396
+ const bundle = React.useContext(ChartLeoInsightContext)
397
+ const anchor = bundle?.insight.anchor
398
+
399
+ const idx = anchor
400
+ ? data.findIndex((d) => String(d[xDataKey]) === anchor.xValue)
401
+ : -1
402
+ const row = idx >= 0 ? (data[idx] as Record<string, unknown>) : null
403
+ const yNum = row && anchor ? resolveChartLeoAnchorY(row, xDataKey, anchor) : null
404
+
405
+ // NOTE: Hook must always run (React rules). Pass safe defaults when not ready.
406
+ const { ref, pos } = useChartAnchorPixelPosition({
407
+ xValue: anchor?.xValue ?? "",
408
+ xDataKey,
409
+ yNum: yNum ?? 0,
410
+ data,
411
+ })
412
+
413
+ if (!bundle || !anchor || idx < 0 || yNum === null || Number.isNaN(yNum)) return null
414
+
415
+ // Clamp the chip so it never renders above the plot rect.
416
+ const chipBottomY = pos
417
+ ? Math.max((pos.plotTop ?? 0) + 28, pos.y - effectiveLift)
418
+ : 0
419
+
420
+ return (
421
+ <div
422
+ ref={ref}
423
+ className="pointer-events-none absolute inset-0 z-20"
424
+ data-chart-leo-anchor=""
425
+ >
426
+ {pos && (
427
+ <>
428
+ {/* Dashed connector — chip bottom → ~7px above the dot, brand-coloured. */}
429
+ <div
430
+ aria-hidden
431
+ className="pointer-events-none absolute"
432
+ style={{
433
+ left: pos.x,
434
+ top: chipBottomY,
435
+ height: Math.max(0, pos.y - chipBottomY - 7),
436
+ transform: "translateX(-50%)",
437
+ borderLeft: `2px dashed oklch(from ${LEO_TOKENS.cssVar} l c h / 0.7)`,
438
+ }}
439
+ />
440
+
441
+ {/* Static brand dot anchored on the real data point */}
442
+ <div
443
+ className="pointer-events-none absolute"
444
+ style={{
445
+ left: pos.x,
446
+ top: pos.y,
447
+ transform: "translate(-50%, -50%)",
448
+ }}
449
+ >
450
+ <LeoPlotPointDot />
451
+ </div>
452
+
453
+ {/* Chip trigger — bottom edge meets the top of the dashed connector */}
454
+ <div
455
+ className="pointer-events-auto absolute"
456
+ style={{
457
+ left: pos.x,
458
+ top: chipBottomY,
459
+ transform: "translate(-50%, -100%)",
460
+ }}
461
+ >
462
+ <LeoInsightIndicator
463
+ insight={bundle.insight}
464
+ chartTitle={bundle.chartTitle}
465
+ triggerLayout="plot-marker"
466
+ />
467
+ </div>
468
+ </>
469
+ )}
470
+ </div>
471
+ )
472
+ }
473
+
474
+
475
+ /** Supplies Leo insight to chart bodies; optional corner control when there is no plot anchor. */
476
+ function ChartLeoInsightOverlay({
477
+ leoInsight,
478
+ chartTitle,
479
+ children,
480
+ }: {
481
+ leoInsight?: ChartLeoInsight | null
482
+ chartTitle: string
483
+ children: React.ReactNode
484
+ }) {
485
+ if (!leoInsight) return <>{children}</>
486
+ const showCorner = !leoInsight.anchor
487
+ return (
488
+ <ChartLeoInsightContext.Provider value={{ insight: leoInsight, chartTitle }}>
489
+ {showCorner ? (
490
+ <div className="relative flex min-h-0 flex-1 flex-col">
491
+ {children}
492
+ <div className="pointer-events-none absolute right-2 top-2 z-20 sm:right-3 sm:top-3">
493
+ <div className="pointer-events-auto">
494
+ <LeoInsightIndicator insight={leoInsight} chartTitle={chartTitle} triggerLayout="toolbar" />
495
+ </div>
496
+ </div>
497
+ </div>
498
+ ) : (
499
+ children
500
+ )}
501
+ </ChartLeoInsightContext.Provider>
502
+ )
503
+ }
504
+
505
+ function AskLeoButton({ iconOnly = false }: { iconOnly?: boolean }) {
506
+ const { toggle } = useAskLeo()
507
+ return (
508
+ <Tooltip>
509
+ <TooltipTrigger asChild>
510
+ <Button
511
+ size="sm"
512
+ variant="outline"
513
+ className="h-7 shrink-0 text-xs gap-1.5 px-2"
514
+ aria-label="Ask Leo about this chart"
515
+ onClick={toggle}
516
+ >
517
+ <i className="fa-duotone fa-solid fa-star-christmas text-xs text-brand" aria-hidden="true" />
518
+ {!iconOnly && <span>Ask Leo</span>}
519
+ </Button>
520
+ </TooltipTrigger>
521
+ <TooltipContent side="bottom" className="flex flex-wrap items-center gap-1.5">
522
+ <span>Ask Leo</span>
523
+ <AskLeoShortcutKbds />
524
+ </TooltipContent>
525
+ </Tooltip>
526
+ )
527
+ }
528
+
529
+ /** Screen-reader data fallback for charts — shared with list-page dashboards. */
530
+ export function ChartDataTable({
531
+ caption,
532
+ headers,
533
+ rows,
534
+ }: {
535
+ caption: string
536
+ headers: string[]
537
+ rows: (string | number)[][]
538
+ }) {
539
+ return (
540
+ <table className="sr-only">
541
+ <caption>{caption}</caption>
542
+ <thead>
543
+ <tr>{headers.map((h) => <th key={h} scope="col">{h}</th>)}</tr>
544
+ </thead>
545
+ <tbody>
546
+ {rows.map((row, i) => (
547
+ <tr key={i}>{row.map((cell, j) => <td key={j}>{cell}</td>)}</tr>
548
+ ))}
549
+ </tbody>
550
+ </table>
551
+ )
552
+ }
553
+
554
+ /**
555
+ * Keyboard-focusable chart region (arrow keys, Escape) + live announcement when a point is selected.
556
+ * Shared by the `/dashboard` gallery and **Data** view dashboards (Placements / Team / Compliance): same
557
+ * interaction model; visual differences come from `ChartCard` chrome and per-chart renderers (bar vs pie),
558
+ * not from a separate chart implementation.
559
+ */
560
+ export function ChartFigure({
561
+ label,
562
+ summary,
563
+ dataLength,
564
+ leoInsight,
565
+ children,
566
+ }: {
567
+ label: string
568
+ summary: string
569
+ dataLength: number
570
+ /** Optional Ask-Leo insight context for chart bodies (same as `ChartCard`). */
571
+ leoInsight?: ChartLeoInsight | null
572
+ children: (activeIndex: number | null) => React.ReactNode
573
+ }) {
574
+ const [activeIndex, setActiveIndex] = React.useState<number | null>(null)
575
+ const ref = React.useRef<HTMLDivElement>(null)
576
+ const prevActiveIndexRef = React.useRef<number | null>(null)
577
+
578
+ React.useEffect(() => {
579
+ const prev = prevActiveIndexRef.current
580
+ prevActiveIndexRef.current = activeIndex
581
+ if (prev === null || activeIndex !== null) return
582
+ const wrapper = ref.current?.querySelector<HTMLElement>(".recharts-wrapper")
583
+ if (!wrapper) return
584
+ wrapper.dispatchEvent(
585
+ new MouseEvent("mouseleave", { bubbles: true, cancelable: true }),
586
+ )
587
+ }, [activeIndex])
588
+
589
+ const navigateKeys = React.useCallback(
590
+ (e: React.KeyboardEvent) => {
591
+ if (!dataLength) return
592
+ if (isEditableTarget(e.target)) return
593
+ switch (e.key) {
594
+ case "ArrowRight":
595
+ case "ArrowDown":
596
+ e.preventDefault()
597
+ e.stopPropagation()
598
+ setActiveIndex((i) => (i === null ? 0 : Math.min(i + 1, dataLength - 1)))
599
+ break
600
+ case "ArrowLeft":
601
+ case "ArrowUp":
602
+ e.preventDefault()
603
+ e.stopPropagation()
604
+ setActiveIndex((i) => (i === null ? dataLength - 1 : Math.max(i - 1, 0)))
605
+ break
606
+ case "Escape":
607
+ e.preventDefault()
608
+ e.stopPropagation()
609
+ setActiveIndex(null)
610
+ ref.current?.blur()
611
+ break
612
+ default:
613
+ break
614
+ }
615
+ },
616
+ [dataLength],
617
+ )
618
+
619
+ /** Clicks on Recharts SVG do not focus this node — focus so Arrow keys work without extra Tab stops. */
620
+ function handlePointerDownCapture(e: React.PointerEvent<HTMLDivElement>) {
621
+ if (!dataLength) return
622
+ const root = ref.current
623
+ if (!root?.contains(e.target as Node)) return
624
+ const el = e.target as HTMLElement | null
625
+ if (el?.closest?.("button, a, [role='tab'], [role='option'], input, select, textarea, [contenteditable='true']"))
626
+ return
627
+ queueMicrotask(() => root.focus())
628
+ }
629
+
630
+ return (
631
+ <div
632
+ ref={ref}
633
+ tabIndex={0}
634
+ role="application"
635
+ aria-label={`${label}. ${summary}. Click the chart or press Tab to focus, then use arrow keys to explore data points. Press Escape to clear selection.`}
636
+ onKeyDownCapture={(e) => {
637
+ if (!ref.current?.contains(e.target as Node)) return
638
+ if (isEditableTarget(e.target)) return
639
+ if (
640
+ e.key === "ArrowRight" ||
641
+ e.key === "ArrowDown" ||
642
+ e.key === "ArrowLeft" ||
643
+ e.key === "ArrowUp" ||
644
+ e.key === "Escape"
645
+ ) {
646
+ navigateKeys(e)
647
+ }
648
+ }}
649
+ onPointerDownCapture={handlePointerDownCapture}
650
+ className="flex min-h-0 flex-1 flex-col outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 rounded-sm"
651
+ >
652
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={label}>
653
+ {children(activeIndex)}
654
+ </ChartLeoInsightOverlay>
655
+ {activeIndex !== null && (
656
+ <div role="status" aria-live="polite" className="sr-only">
657
+ Data point {activeIndex + 1} of {dataLength} selected
658
+ </div>
659
+ )}
660
+ </div>
661
+ )
662
+ }
663
+
664
+ function ChartCardHeader({
665
+ title,
666
+ description,
667
+ variant,
668
+ filterOptions,
669
+ filter,
670
+ onFilter,
671
+ }: {
672
+ title: string
673
+ description: string
674
+ variant: ChartCardVariant
675
+ filterOptions?: { value: string; label: string }[]
676
+ filter?: string
677
+ onFilter?: (v: string) => void
678
+ }) {
679
+ const isSelector = variant === "selector" && Array.isArray(filterOptions) && filterOptions.length > 0
680
+ return (
681
+ <CardHeader className="shrink-0 pb-2">
682
+ <div className="flex items-start gap-2">
683
+ <div className="flex-1 min-w-0">
684
+ <CardTitle className="text-sm font-semibold leading-tight">{title}</CardTitle>
685
+ <CardDescription className="text-xs mt-0.5">{description}</CardDescription>
686
+ </div>
687
+ <div className="flex items-center gap-1.5 shrink-0">
688
+ {/* Reveal on card hover/focus — pointer-events guarded so the hidden button is not reachable */}
689
+ <span className="pointer-events-none opacity-0 transition-opacity duration-150 group-hover/card:pointer-events-auto group-hover/card:opacity-100 group-focus-within/card:pointer-events-auto group-focus-within/card:opacity-100 inline-flex">
690
+ <AskLeoButton iconOnly={isSelector} />
691
+ </span>
692
+ {isSelector && filterOptions && onFilter && (
693
+ <Select value={filter || filterOptions[0]?.value} onValueChange={(v) => onFilter(v)}>
694
+ <SelectTrigger
695
+ className="h-8 w-auto min-w-[9rem] shrink-0 text-sm"
696
+ aria-label="Filter chart data"
697
+ >
698
+ <SelectValue />
699
+ </SelectTrigger>
700
+ <SelectContent align="end" sideOffset={4}>
701
+ {filterOptions.map((opt) => (
702
+ <SelectItem key={opt.value} value={opt.value}>
703
+ {opt.label}
704
+ </SelectItem>
705
+ ))}
706
+ </SelectContent>
707
+ </Select>
708
+ )}
709
+ </div>
710
+ </div>
711
+ </CardHeader>
712
+ )
713
+ }
714
+
715
+ type MiniMetric = { label: string; value: string; trend?: "up" | "down" | "neutral" }
716
+
717
+ export function ChartCard({
718
+ title,
719
+ description,
720
+ children,
721
+ className = "",
722
+ variant = "normal",
723
+ trendContent,
724
+ filterOptions,
725
+ defaultFilter,
726
+ onFilterChange,
727
+ miniMetrics,
728
+ tabOptions,
729
+ leoInsight,
730
+ }: {
731
+ title: string
732
+ description: string
733
+ children: React.ReactNode | ((filter: string) => React.ReactNode)
734
+ className?: string
735
+ variant?: ChartCardVariant
736
+ /** "tabs" / "metrics-tabs" variant: content shown in the "Trend" tab */
737
+ trendContent?: React.ReactNode
738
+ /** "selector" variant: options for the filter dropdown */
739
+ filterOptions?: { value: string; label: string }[]
740
+ defaultFilter?: string
741
+ onFilterChange?: (value: string) => void
742
+ /** "metrics-tabs" variant: compact KPI strip shown above the chart */
743
+ miniMetrics?: MiniMetric[]
744
+ /** "tabs" variant: override the default Chart/Trend tabs with custom options.
745
+ * The selected value is passed to the children function. */
746
+ tabOptions?: { value: string; label: string }[]
747
+ /**
748
+ * Smart Leo summary: opens a popover + Ask Leo CTA.
749
+ * With `anchor`, mount `ChartLeoPlotInsightOverlay` beside `ChartContainer` for on-plot guide + marker; otherwise a corner Insight control is shown.
750
+ */
751
+ leoInsight?: ChartLeoInsight | null
752
+ }) {
753
+ const [filter, setFilter] = React.useState(
754
+ () => defaultFilter || filterOptions?.[0]?.value || miniMetrics?.[0]?.label || tabOptions?.[0]?.value || ""
755
+ )
756
+
757
+ // Sync when defaultFilter or first miniMetric changes (React may reuse across ternary branches)
758
+ React.useEffect(() => {
759
+ const next = defaultFilter || filterOptions?.[0]?.value || miniMetrics?.[0]?.label
760
+ if (next) setFilter(next)
761
+ // eslint-disable-next-line react-hooks/exhaustive-deps
762
+ }, [defaultFilter, miniMetrics?.[0]?.label])
763
+
764
+ const handleFilter = (v: string) => { setFilter(v); onFilterChange?.(v) }
765
+
766
+ const resolvedChildren =
767
+ typeof children === "function" ? children(filter) : children
768
+
769
+ /* ── Default Chart / Trend tabs (no custom tabOptions) ───────────────────── */
770
+ const defaultTabsBlock = (
771
+ <Tabs defaultValue="trend" className="flex flex-col flex-1 min-h-0">
772
+ <div className="px-6 pb-1">
773
+ <TabsList variant="line">
774
+ <TabsTrigger value="chart" className={chartCardTabTriggerClass}>
775
+ <i className="fa-light fa-chart-mixed text-sm" aria-hidden="true" />
776
+ Chart
777
+ </TabsTrigger>
778
+ <TabsTrigger value="trend" className={chartCardTabTriggerClass}>
779
+ <i className="fa-light fa-chart-line text-sm" aria-hidden="true" />
780
+ Trend
781
+ </TabsTrigger>
782
+ </TabsList>
783
+ </div>
784
+ <TabsContent value="chart" className="flex-1 flex flex-col min-h-0 m-0">
785
+ <CardContent className="flex-1 flex flex-col min-h-0 pb-4">
786
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={title}>
787
+ {resolvedChildren}
788
+ </ChartLeoInsightOverlay>
789
+ </CardContent>
790
+ </TabsContent>
791
+ <TabsContent value="trend" className="flex-1 flex flex-col min-h-0 m-0">
792
+ <CardContent className="flex-1 flex flex-col min-h-0 pb-4">
793
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={title}>
794
+ {trendContent ?? resolvedChildren}
795
+ </ChartLeoInsightOverlay>
796
+ </CardContent>
797
+ </TabsContent>
798
+ </Tabs>
799
+ )
800
+
801
+ if (variant === "tabs") {
802
+ /* Custom tab labels (e.g. period picker for key metrics) */
803
+ if (tabOptions && tabOptions.length > 0) {
804
+ const selectedTab = filter || tabOptions[0].value
805
+ return (
806
+ <Card className={`flex flex-col h-full ${className}`} role="figure" aria-label={title}>
807
+ <ChartCardHeader title={title} description={description} variant="normal" />
808
+ <Tabs defaultValue={tabOptions[0].value} value={selectedTab} onValueChange={handleFilter} className="flex flex-col flex-1 min-h-0">
809
+ <div className="px-6 pb-1">
810
+ <TabsList variant="line">
811
+ {tabOptions.map((tab) => (
812
+ <TabsTrigger key={tab.value} value={tab.value} className={chartCardTabTriggerClass}>
813
+ {tab.label}
814
+ </TabsTrigger>
815
+ ))}
816
+ </TabsList>
817
+ </div>
818
+ {tabOptions.map((tab) => (
819
+ <TabsContent key={tab.value} value={tab.value} className="flex-1 flex flex-col min-h-0 m-0">
820
+ <CardContent className="flex-1 flex flex-col min-h-[200px] pb-4">
821
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={title}>
822
+ {typeof children === "function" ? children(tab.value) : children}
823
+ </ChartLeoInsightOverlay>
824
+ </CardContent>
825
+ </TabsContent>
826
+ ))}
827
+ </Tabs>
828
+ </Card>
829
+ )
830
+ }
831
+
832
+ return (
833
+ <Card className={`flex flex-col h-full ${className}`} role="figure" aria-label={title}>
834
+ <ChartCardHeader title={title} description={description} variant="normal" />
835
+ {defaultTabsBlock}
836
+ </Card>
837
+ )
838
+ }
839
+
840
+ if (variant === "metrics-tabs") {
841
+ const metrics = miniMetrics && miniMetrics.length > 0 ? miniMetrics : null
842
+ const selectedMetric = filter || metrics?.[0]?.label || ""
843
+
844
+ return (
845
+ <Card className={`flex flex-col h-full ${className}`} role="figure" aria-label={title}>
846
+ <ChartCardHeader title={title} description={description} variant="normal" />
847
+
848
+ {metrics ? (
849
+ /* Metrics ARE the tabs — each metric cell is a clickable TabsTrigger */
850
+ <Tabs value={selectedMetric} onValueChange={handleFilter} className="flex flex-col flex-1 min-h-0">
851
+ <div className="shrink-0 px-2">
852
+ <TabsList
853
+ variant="line"
854
+ className="h-auto w-full gap-0 rounded-none p-0 justify-start !items-end border-b border-border"
855
+ >
856
+ {metrics.map((m) => {
857
+ const isUp = m.trend === "up"
858
+ const isDown = m.trend === "down"
859
+ return (
860
+ <TabsTrigger
861
+ key={m.label}
862
+ value={m.label}
863
+ className="h-auto flex-col items-start gap-1 px-3 pt-2 pb-3 rounded-none min-w-0 flex-none -mb-px border-b-2 border-transparent data-active:border-b-foreground after:![opacity:0] opacity-60 data-active:opacity-100"
864
+ >
865
+ <span className="text-sm font-normal text-muted-foreground leading-none">{m.label}</span>
866
+ <div className="flex items-baseline gap-1.5">
867
+ <span className="text-xl font-bold tabular-nums leading-none text-foreground">{m.value}</span>
868
+ {isUp && <i className="fa-light fa-arrow-trend-up text-xs text-emerald-600" aria-hidden="true" />}
869
+ {isDown && <i className="fa-light fa-arrow-trend-down text-xs text-destructive" aria-hidden="true" />}
870
+ </div>
871
+ </TabsTrigger>
872
+ )
873
+ })}
874
+ </TabsList>
875
+ </div>
876
+ {/* All metric tabs show the same chart — tab selection is a context indicator */}
877
+ {metrics.map((m) => (
878
+ <TabsContent key={m.label} value={m.label} className="flex-1 flex flex-col min-h-0 m-0">
879
+ <CardContent className="flex-1 flex flex-col min-h-0 pb-4">
880
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={title}>
881
+ {resolvedChildren}
882
+ </ChartLeoInsightOverlay>
883
+ </CardContent>
884
+ </TabsContent>
885
+ ))}
886
+ </Tabs>
887
+ ) : (
888
+ defaultTabsBlock
889
+ )}
890
+ </Card>
891
+ )
892
+ }
893
+
894
+ /* ── kpi-chart: prominent metric on top, chart below ─────────────────────── */
895
+ if (variant === "kpi-chart") {
896
+ const kpi = miniMetrics?.[0]
897
+ const isUp = kpi?.trend === "up"
898
+ const isDown = kpi?.trend === "down"
899
+
900
+ return (
901
+ <Card className={`flex flex-col h-full ${className}`} role="figure" aria-label={title}>
902
+ <ChartCardHeader title={title} description={description} variant="normal" />
903
+
904
+ {kpi && (
905
+ <div className="px-6 pb-2 shrink-0">
906
+ <div className="flex items-baseline gap-2.5">
907
+ <span className="text-4xl font-bold tabular-nums tracking-tight text-foreground">
908
+ {kpi.value}
909
+ </span>
910
+ {isUp && (
911
+ <span className="flex items-center gap-1 text-sm font-medium text-emerald-600">
912
+ <i className="fa-light fa-arrow-trend-up" aria-hidden="true" />
913
+ <span className="sr-only">trending up</span>
914
+ </span>
915
+ )}
916
+ {isDown && (
917
+ <span className="flex items-center gap-1 text-sm font-medium text-destructive">
918
+ <i className="fa-light fa-arrow-trend-down" aria-hidden="true" />
919
+ <span className="sr-only">trending down</span>
920
+ </span>
921
+ )}
922
+ </div>
923
+ <p className="text-xs text-muted-foreground mt-0.5">{kpi.label}</p>
924
+ </div>
925
+ )}
926
+
927
+ <CardContent className="flex-1 flex flex-col min-h-0 pb-4 pt-0">
928
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={title}>
929
+ {resolvedChildren}
930
+ </ChartLeoInsightOverlay>
931
+ </CardContent>
932
+ </Card>
933
+ )
934
+ }
935
+
936
+ return (
937
+ <Card className={`flex flex-col h-full ${className}`} role="figure" aria-label={title}>
938
+ <ChartCardHeader
939
+ title={title}
940
+ description={description}
941
+ variant={variant}
942
+ filterOptions={filterOptions}
943
+ filter={filter}
944
+ onFilter={handleFilter}
945
+ />
946
+ <CardContent className="flex-1 flex flex-col min-h-0 pb-4">
947
+ <ChartLeoInsightOverlay leoInsight={leoInsight} chartTitle={title}>
948
+ {resolvedChildren}
949
+ </ChartLeoInsightOverlay>
950
+ </CardContent>
951
+ </Card>
952
+ )
953
+ }
954
+
955
+ /* ════════════════════════════════════════════════════════════════════════════
956
+ DATA & CHART COMPONENTS
957
+ ════════════════════════════════════════════════════════════════════════════ */
958
+
959
+ /* ── Area ─────────────────────────────────────────────────────────────────── */
960
+ const areaCfg: ChartConfig = {
961
+ placements: { label: "Placements", color: BRAND },
962
+ applications: { label: "Applications", color: CHART_2 },
963
+ reviews: { label: "Reviews", color: CHART_4 },
964
+ }
965
+ const areaData = [
966
+ { month: "Aug", placements: 42, applications: 78, reviews: 31 },
967
+ { month: "Sep", placements: 58, applications: 91, reviews: 44 },
968
+ { month: "Oct", placements: 53, applications: 85, reviews: 39 },
969
+ { month: "Nov", placements: 67, applications: 102, reviews: 52 },
970
+ { month: "Dec", placements: 49, applications: 76, reviews: 37 },
971
+ { month: "Jan", placements: 74, applications: 118, reviews: 60 },
972
+ { month: "Feb", placements: 81, applications: 124, reviews: 68 },
973
+ { month: "Mar", placements: 89, applications: 137, reviews: 72 },
974
+ ]
975
+
976
+ function AreaChartContent() {
977
+ return (
978
+ <ChartFigure label="Placement Trends" summary="Multi-line area chart showing placements, applications and reviews from Aug to Mar" dataLength={areaData.length}>
979
+ {(activeIndex) => (
980
+ <>
981
+ <div className="relative w-full min-h-[180px] flex-1">
982
+ <ChartContainer config={areaCfg} className="h-full min-h-[180px] w-full flex-1">
983
+ <AreaChart data={areaData} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
984
+ <defs>
985
+ <linearGradient id="gPlace" x1="0" y1="0" x2="0" y2="1">
986
+ <stop offset="5%" stopColor={BRAND} stopOpacity={0.35} />
987
+ <stop offset="95%" stopColor={BRAND} stopOpacity={0.02} />
988
+ </linearGradient>
989
+ <linearGradient id="gApps" x1="0" y1="0" x2="0" y2="1">
990
+ <stop offset="5%" stopColor={CHART_2} stopOpacity={0.3} />
991
+ <stop offset="95%" stopColor={CHART_2} stopOpacity={0.02} />
992
+ </linearGradient>
993
+ <linearGradient id="gRev" x1="0" y1="0" x2="0" y2="1">
994
+ <stop offset="5%" stopColor={CHART_4} stopOpacity={0.3} />
995
+ <stop offset="95%" stopColor={CHART_4} stopOpacity={0.02} />
996
+ </linearGradient>
997
+ </defs>
998
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
999
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1000
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1001
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1002
+ <ChartLegend content={<ChartLegendContent />} />
1003
+ <Area key="placements" type="monotone" dataKey="placements" stroke={BRAND} fill="url(#gPlace)" strokeWidth={2} dot={false} activeDot={{ r: 5, stroke: "var(--ring)", strokeWidth: 2 }} />
1004
+ <Area key="applications" type="monotone" dataKey="applications" stroke={CHART_2} fill="url(#gApps)" strokeWidth={2} dot={false} activeDot={{ r: 5, stroke: "var(--ring)", strokeWidth: 2 }} />
1005
+ <Area key="reviews" type="monotone" dataKey="reviews" stroke={CHART_4} fill="url(#gRev)" strokeWidth={2} dot={false} activeDot={{ r: 5, stroke: "var(--ring)", strokeWidth: 2 }} />
1006
+ </AreaChart>
1007
+ </ChartContainer>
1008
+ <ChartLeoPlotInsightOverlay data={areaData} xDataKey="month" />
1009
+ </div>
1010
+ <ChartDataTable caption="Placement Trends" headers={["Month", "Placements", "Applications", "Reviews"]} rows={areaData.map(d => [d.month, d.placements, d.applications, d.reviews])} />
1011
+ </>
1012
+ )}
1013
+ </ChartFigure>
1014
+ )
1015
+ }
1016
+
1017
+ function AreaLineTrendContent() {
1018
+ return (
1019
+ <ChartFigure label="Placement Trends" summary="Line chart showing placement trends Aug to Mar" dataLength={areaData.length}>
1020
+ {(activeIndex) => (
1021
+ <>
1022
+ <div className="relative w-full min-h-[180px] flex-1">
1023
+ <ChartContainer config={areaCfg} className="h-full min-h-[180px] w-full flex-1">
1024
+ <LineChart data={areaData} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1025
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1026
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1027
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1028
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1029
+ <ChartLegend content={<ChartLegendContent />} />
1030
+ <Line type="monotone" dataKey="placements" stroke={BRAND} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={2} fill={props.stroke} />} />
1031
+ <Line type="monotone" dataKey="applications" stroke={CHART_2} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={2} fill={props.stroke} />} />
1032
+ <Line type="monotone" dataKey="reviews" stroke={CHART_4} strokeWidth={2} strokeDasharray={chartLineStrokeDash(2)} dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={2} fill={props.stroke} />} />
1033
+ </LineChart>
1034
+ </ChartContainer>
1035
+ <ChartLeoPlotInsightOverlay data={areaData} xDataKey="month" />
1036
+ </div>
1037
+ <ChartDataTable caption="Placement Trends" headers={["Month", "Placements", "Applications", "Reviews"]} rows={areaData.map(d => [d.month, d.placements, d.applications, d.reviews])} />
1038
+ </>
1039
+ )}
1040
+ </ChartFigure>
1041
+ )
1042
+ }
1043
+
1044
+ /* Selector variant — filter data by period */
1045
+ const areaDataByPeriod: Record<string, typeof areaData> = {
1046
+ "7d": areaData.slice(-2),
1047
+ "30d": areaData.slice(-4),
1048
+ "90d": areaData.slice(-6),
1049
+ "1y": areaData,
1050
+ }
1051
+
1052
+ function AreaSelectorContent({ filter }: { filter: string }) {
1053
+ const data = areaDataByPeriod[filter] ?? areaData
1054
+ return (
1055
+ <ChartFigure label="Placement Trends" summary={`Area chart for ${filter} period`} dataLength={data.length}>
1056
+ {(activeIndex) => (
1057
+ <>
1058
+ <div className="relative w-full min-h-[180px] flex-1">
1059
+ <ChartContainer config={areaCfg} className="h-full min-h-[180px] w-full flex-1">
1060
+ <AreaChart data={data} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1061
+ <defs>
1062
+ <linearGradient id="gPlace2" x1="0" y1="0" x2="0" y2="1">
1063
+ <stop offset="5%" stopColor={BRAND} stopOpacity={0.35} />
1064
+ <stop offset="95%" stopColor={BRAND} stopOpacity={0.02} />
1065
+ </linearGradient>
1066
+ </defs>
1067
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1068
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1069
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1070
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1071
+ <ChartLegend content={<ChartLegendContent />} />
1072
+ <Area key="placements" type="monotone" dataKey="placements" stroke={BRAND} fill="url(#gPlace2)" strokeWidth={2} dot={false} activeDot={{ r: 5, stroke: "var(--ring)", strokeWidth: 2 }} />
1073
+ <Area key="applications" type="monotone" dataKey="applications" stroke={CHART_2} fill="none" strokeWidth={2} dot={false} activeDot={{ r: 5, stroke: "var(--ring)", strokeWidth: 2 }} />
1074
+ <Area key="reviews" type="monotone" dataKey="reviews" stroke={CHART_4} fill="none" strokeWidth={2} dot={false} activeDot={{ r: 5, stroke: "var(--ring)", strokeWidth: 2 }} />
1075
+ </AreaChart>
1076
+ </ChartContainer>
1077
+ <ChartLeoPlotInsightOverlay data={data} xDataKey="month" />
1078
+ </div>
1079
+ <ChartDataTable caption="Placement Trends" headers={["Month", "Placements", "Applications", "Reviews"]} rows={data.map(d => [d.month, d.placements, d.applications, d.reviews])} />
1080
+ </>
1081
+ )}
1082
+ </ChartFigure>
1083
+ )
1084
+ }
1085
+
1086
+ /* ── Donut ─────────────────────────────────────────────────────────────────── */
1087
+ const donutCfg: ChartConfig = {
1088
+ confirmed: { label: "Confirmed", color: SUCCESS },
1089
+ pending: { label: "Pending", color: WARNING },
1090
+ rejected: { label: "Rejected", color: DESTRUCTIVE },
1091
+ review: { label: "In Review", color: CHART_1 },
1092
+ }
1093
+ const donutDataAll = [
1094
+ { name: "confirmed", value: 58, fill: SUCCESS },
1095
+ { name: "pending", value: 24, fill: WARNING },
1096
+ { name: "rejected", value: 9, fill: DESTRUCTIVE },
1097
+ { name: "review", value: 9, fill: CHART_1 },
1098
+ ]
1099
+
1100
+ function DonutChartContent({ data = donutDataAll }: { data?: typeof donutDataAll }) {
1101
+ const total = data.reduce((s, d) => s + d.value, 0)
1102
+ return (
1103
+ <ChartFigure label="Placement Status" summary="Donut chart showing confirmed, pending, rejected and in-review placement distribution" dataLength={data.length} leoInsight={CHART_GALLERY_LEO_DONUT}>
1104
+ {(activeIndex) => (
1105
+ <>
1106
+ <ChartContainer config={donutCfg} className="flex-1 min-h-[140px] w-full">
1107
+ <PieChart>
1108
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent hideLabel />} />
1109
+ <Pie data={data} dataKey="value" nameKey="name"
1110
+ innerRadius="50%" outerRadius="78%" strokeWidth={2} stroke="var(--card)"
1111
+ activeIndex={activeIndex ?? undefined} activeShape={{ strokeWidth: 3, stroke: "var(--ring)" }}>
1112
+ {data.map((d) => <Cell key={d.name} fill={d.fill} />)}
1113
+ </Pie>
1114
+ </PieChart>
1115
+ </ChartContainer>
1116
+ <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs mt-2 shrink-0">
1117
+ {data.map((d) => (
1118
+ <div key={d.name} className="flex items-center gap-1.5">
1119
+ <span className="h-2.5 w-2.5 rounded-full shrink-0" style={{ background: d.fill }} />
1120
+ <span className="text-muted-foreground">{donutCfg[d.name]?.label}</span>
1121
+ <span className="ml-auto font-medium tabular-nums">
1122
+ {Math.round(d.value / total * 100)}%
1123
+ </span>
1124
+ </div>
1125
+ ))}
1126
+ </div>
1127
+ <ChartDataTable
1128
+ caption="Placement Status"
1129
+ headers={["Status", "Count"]}
1130
+ rows={data.map(d => {
1131
+ const raw = donutCfg[d.name]?.label ?? d.name
1132
+ const label =
1133
+ typeof raw === "string" || typeof raw === "number" ? String(raw) : String(d.name)
1134
+ return [label, d.value] as [string, number]
1135
+ })}
1136
+ />
1137
+ </>
1138
+ )}
1139
+ </ChartFigure>
1140
+ )
1141
+ }
1142
+
1143
+ /* Donut trend — bar chart version */
1144
+ function DonutBarTrendContent() {
1145
+ const cfg: ChartConfig = {
1146
+ confirmed: { label: "Confirmed", color: SUCCESS },
1147
+ pending: { label: "Pending", color: WARNING },
1148
+ rejected: { label: "Rejected", color: DESTRUCTIVE },
1149
+ }
1150
+ const data = [
1151
+ { month: "Jan", confirmed: 52, pending: 20, rejected: 7 },
1152
+ { month: "Feb", confirmed: 60, pending: 18, rejected: 6 },
1153
+ { month: "Mar", confirmed: 68, pending: 24, rejected: 9 },
1154
+ ]
1155
+ return (
1156
+ <ChartContainer config={cfg} className="flex-1 min-h-[180px] w-full">
1157
+ <BarChart data={data} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1158
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1159
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1160
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={28} />
1161
+ <ChartTooltip content={<ChartTooltipContent />} />
1162
+ <ChartLegend content={<ChartLegendContent />} />
1163
+ <Bar dataKey="confirmed" fill={SUCCESS} stackId="a" />
1164
+ <Bar dataKey="pending" fill={WARNING} stackId="a" />
1165
+ <Bar dataKey="rejected" fill={DESTRUCTIVE} stackId="a" radius={[4, 4, 0, 0]} />
1166
+ </BarChart>
1167
+ </ChartContainer>
1168
+ )
1169
+ }
1170
+
1171
+ /* Donut — selector by program */
1172
+ const donutByProgram: Record<string, typeof donutDataAll> = {
1173
+ all: donutDataAll,
1174
+ 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 }],
1175
+ 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 }],
1176
+ 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 }],
1177
+ 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 }],
1178
+ }
1179
+
1180
+ /* ── Grouped Bar ─────────────────────────────────────────────────────────── */
1181
+ const barCfg: ChartConfig = {
1182
+ new: { label: "New", color: BRAND },
1183
+ returned: { label: "Returned", color: CHART_2 },
1184
+ }
1185
+ const barData = [
1186
+ { program: "Nursing", new: 34, returned: 22 },
1187
+ { program: "PT", new: 28, returned: 18 },
1188
+ { program: "OT", new: 21, returned: 14 },
1189
+ { program: "SW", new: 19, returned: 11 },
1190
+ { program: "Pharm", new: 15, returned: 9 },
1191
+ { program: "Rad", new: 12, returned: 7 },
1192
+ ]
1193
+
1194
+ function GroupedBarContent() {
1195
+ return (
1196
+ <ChartFigure label="Applications by Program" summary="Grouped bar chart showing new and returned applications across 6 programs" dataLength={barData.length} leoInsight={CHART_GALLERY_LEO_APPLICATIONS}>
1197
+ {(activeIndex) => (
1198
+ <>
1199
+ <ChartContainer config={barCfg} className="flex-1 min-h-[180px] w-full">
1200
+ <BarChart data={barData} barGap={4} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1201
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1202
+ <XAxis dataKey="program" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1203
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={28} />
1204
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1205
+ <ChartLegend content={<ChartLegendContent />} />
1206
+ <Bar dataKey="new" fill={BRAND} radius={[4, 4, 0, 0]} activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1207
+ <Bar dataKey="returned" fill={CHART_2} radius={[4, 4, 0, 0]} activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1208
+ </BarChart>
1209
+ </ChartContainer>
1210
+ <ChartDataTable caption="Applications by Program" headers={["Program", "New", "Returned"]} rows={barData.map(d => [d.program, d.new, d.returned])} />
1211
+ </>
1212
+ )}
1213
+ </ChartFigure>
1214
+ )
1215
+ }
1216
+
1217
+ function GroupedBarLineTrend() {
1218
+ return (
1219
+ <ChartContainer config={barCfg} className="flex-1 min-h-[180px] w-full">
1220
+ <LineChart data={barData} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1221
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1222
+ <XAxis dataKey="program" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1223
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={28} />
1224
+ <ChartTooltip content={<ChartTooltipContent />} />
1225
+ <ChartLegend content={<ChartLegendContent />} />
1226
+ <Line type="monotone" dataKey="new" stroke={BRAND} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={{ r: 3 }} />
1227
+ <Line type="monotone" dataKey="returned" stroke={CHART_2} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={{ r: 3 }} />
1228
+ </LineChart>
1229
+ </ChartContainer>
1230
+ )
1231
+ }
1232
+
1233
+ /* ── Stacked Bar ─────────────────────────────────────────────────────────── */
1234
+ const stackCfg: ChartConfig = {
1235
+ approved: { label: "Approved", color: SUCCESS },
1236
+ pending: { label: "Pending", color: WARNING },
1237
+ rejected: { label: "Rejected", color: DESTRUCTIVE },
1238
+ }
1239
+ const stackData = [
1240
+ { month: "Oct", approved: 38, pending: 12, rejected: 4 },
1241
+ { month: "Nov", approved: 44, pending: 15, rejected: 6 },
1242
+ { month: "Dec", approved: 31, pending: 8, rejected: 3 },
1243
+ { month: "Jan", approved: 52, pending: 18, rejected: 7 },
1244
+ { month: "Feb", approved: 60, pending: 14, rejected: 5 },
1245
+ { month: "Mar", approved: 68, pending: 20, rejected: 8 },
1246
+ ]
1247
+
1248
+ function StackedBarContent() {
1249
+ return (
1250
+ <ChartFigure label="Monthly Reviews" summary="Stacked bar chart showing approved, pending and rejected reviews Oct to Mar" dataLength={stackData.length}>
1251
+ {(activeIndex) => (
1252
+ <>
1253
+ <div className="relative w-full min-h-[180px] flex-1">
1254
+ <ChartContainer config={stackCfg} className="h-full min-h-[180px] w-full flex-1">
1255
+ <BarChart data={stackData} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1256
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1257
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1258
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={28} />
1259
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1260
+ <ChartLegend content={<ChartLegendContent />} />
1261
+ <Bar dataKey="approved" fill={SUCCESS} stackId="a" activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1262
+ <Bar dataKey="pending" fill={WARNING} stackId="a" activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1263
+ <Bar dataKey="rejected" fill={DESTRUCTIVE} stackId="a" radius={[4, 4, 0, 0]} activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1264
+ </BarChart>
1265
+ </ChartContainer>
1266
+ <ChartLeoPlotInsightOverlay data={stackData} xDataKey="month" insetPct={{ left: 12, right: 4, top: 5, bottom: 18 }} />
1267
+ </div>
1268
+ <ChartDataTable caption="Monthly Reviews" headers={["Month", "Approved", "Pending", "Rejected"]} rows={stackData.map(d => [d.month, d.approved, d.pending, d.rejected])} />
1269
+ </>
1270
+ )}
1271
+ </ChartFigure>
1272
+ )
1273
+ }
1274
+
1275
+ function StackedBarLineTrend() {
1276
+ return (
1277
+ <div className="relative w-full min-h-[180px] flex-1">
1278
+ <ChartContainer config={stackCfg} className="h-full min-h-[180px] w-full flex-1">
1279
+ <LineChart data={stackData} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1280
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1281
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1282
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={28} />
1283
+ <ChartTooltip content={<ChartTooltipContent />} />
1284
+ <ChartLegend content={<ChartLegendContent />} />
1285
+ <Line type="monotone" dataKey="approved" stroke={SUCCESS} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={{ r: 3 }} />
1286
+ <Line type="monotone" dataKey="pending" stroke={WARNING} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={{ r: 3 }} />
1287
+ <Line type="monotone" dataKey="rejected" stroke={DESTRUCTIVE} strokeWidth={2} strokeDasharray={chartLineStrokeDash(2)} dot={{ r: 3 }} />
1288
+ </LineChart>
1289
+ </ChartContainer>
1290
+ <ChartLeoPlotInsightOverlay data={stackData} xDataKey="month" insetPct={{ left: 12, right: 4, top: 5, bottom: 18 }} />
1291
+ </div>
1292
+ )
1293
+ }
1294
+
1295
+ /* ── Line ─────────────────────────────────────────────────────────────────── */
1296
+ const lineCfg: ChartConfig = {
1297
+ logins: { label: "Logins", color: BRAND },
1298
+ submissions: { label: "Submissions", color: CHART_2 },
1299
+ evaluations: { label: "Evaluations", color: CHART_4 },
1300
+ }
1301
+ const lineData = [
1302
+ { week: "W1", logins: 148, submissions: 42, evaluations: 29 },
1303
+ { week: "W2", logins: 162, submissions: 51, evaluations: 35 },
1304
+ { week: "W3", logins: 139, submissions: 38, evaluations: 27 },
1305
+ { week: "W4", logins: 175, submissions: 63, evaluations: 48 },
1306
+ { week: "W5", logins: 182, submissions: 69, evaluations: 52 },
1307
+ { week: "W6", logins: 196, submissions: 75, evaluations: 58 },
1308
+ { week: "W7", logins: 211, submissions: 82, evaluations: 63 },
1309
+ { week: "W8", logins: 204, submissions: 78, evaluations: 60 },
1310
+ ]
1311
+
1312
+ const lineDataByPeriod: Record<string, typeof lineData> = {
1313
+ "7d": lineData.slice(-2),
1314
+ "30d": lineData.slice(-4),
1315
+ "90d": lineData.slice(-6),
1316
+ "1y": lineData,
1317
+ }
1318
+
1319
+ function LineChartContent({ data = lineData }: { data?: typeof lineData }) {
1320
+ return (
1321
+ <ChartFigure label="Portal Activity" summary="Line chart showing logins, submissions and evaluations by week" dataLength={data.length} leoInsight={CHART_GALLERY_LEO_LINE}>
1322
+ {(activeIndex) => (
1323
+ <>
1324
+ <ChartContainer config={lineCfg} className="flex-1 min-h-[180px] w-full">
1325
+ <LineChart data={data} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1326
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1327
+ <XAxis dataKey="week" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1328
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1329
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1330
+ <ChartLegend content={<ChartLegendContent />} />
1331
+ <Line type="monotone" dataKey="logins" stroke={BRAND} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={2} fill={props.stroke} />} />
1332
+ <Line type="monotone" dataKey="submissions" stroke={CHART_2} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={2} fill={props.stroke} />} />
1333
+ <Line type="monotone" dataKey="evaluations" stroke={CHART_4} strokeWidth={2} strokeDasharray={chartLineStrokeDash(2)} dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={2} fill={props.stroke} />} />
1334
+ </LineChart>
1335
+ </ChartContainer>
1336
+ <ChartDataTable caption="Portal Activity" headers={["Week", "Logins", "Submissions", "Evaluations"]} rows={data.map(d => [d.week, d.logins, d.submissions, d.evaluations])} />
1337
+ </>
1338
+ )}
1339
+ </ChartFigure>
1340
+ )
1341
+ }
1342
+
1343
+ function LineAreaTrend() {
1344
+ return (
1345
+ <ChartContainer config={lineCfg} className="flex-1 min-h-[180px] w-full">
1346
+ <AreaChart data={lineData} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1347
+ <defs>
1348
+ <linearGradient id="gLogin" x1="0" y1="0" x2="0" y2="1">
1349
+ <stop offset="5%" stopColor={BRAND} stopOpacity={0.3} />
1350
+ <stop offset="95%" stopColor={BRAND} stopOpacity={0.0} />
1351
+ </linearGradient>
1352
+ </defs>
1353
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1354
+ <XAxis dataKey="week" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1355
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1356
+ <ChartTooltip content={<ChartTooltipContent />} />
1357
+ <ChartLegend content={<ChartLegendContent />} />
1358
+ <Area key="logins" type="monotone" dataKey="logins" stroke={BRAND} fill="url(#gLogin)" strokeWidth={2} dot={false} />
1359
+ <Area key="submissions" type="monotone" dataKey="submissions" stroke={CHART_2} fill="none" strokeWidth={2} dot={false} />
1360
+ <Area key="evaluations" type="monotone" dataKey="evaluations" stroke={CHART_4} fill="none" strokeWidth={2} dot={false} />
1361
+ </AreaChart>
1362
+ </ChartContainer>
1363
+ )
1364
+ }
1365
+
1366
+ /* ── Radial Bar ──────────────────────────────────────────────────────────── */
1367
+ const radialCfg: ChartConfig = {
1368
+ nursing: { label: "Nursing", color: BRAND },
1369
+ pt: { label: "PT", color: CHART_2 },
1370
+ ot: { label: "OT", color: SUCCESS },
1371
+ pharmacy: { label: "Pharmacy", color: WARNING },
1372
+ social: { label: "Social Work", color: CHART_4 },
1373
+ }
1374
+ const radialData = [
1375
+ { name: "nursing", score: 98, fill: BRAND },
1376
+ { name: "pt", score: 94, fill: CHART_2 },
1377
+ { name: "ot", score: 91, fill: SUCCESS },
1378
+ { name: "pharmacy", score: 87, fill: WARNING },
1379
+ { name: "social", score: 82, fill: CHART_4 },
1380
+ ]
1381
+
1382
+ function RadialBarContent({ data = radialData }: { data?: typeof radialData }) {
1383
+ return (
1384
+ <ChartFigure label="Compliance Score" summary="Radial bar chart showing compliance scores by program" dataLength={data.length} leoInsight={CHART_GALLERY_LEO_COMPLIANCE}>
1385
+ {(activeIndex) => (
1386
+ <>
1387
+ <ChartContainer config={radialCfg} className="flex-1 min-h-[140px] w-full">
1388
+ <RadialBarChart data={data} innerRadius="20%" outerRadius="85%"
1389
+ startAngle={90} endAngle={-270} barSize={10}>
1390
+ <PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
1391
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent nameKey="name" hideLabel />} />
1392
+ <RadialBar dataKey="score" cornerRadius={5} background={{ fill: "var(--muted)" }} activeIndex={activeIndex ?? undefined}>
1393
+ {data.map((d) => <Cell key={d.name} fill={d.fill} />)}
1394
+ </RadialBar>
1395
+ </RadialBarChart>
1396
+ </ChartContainer>
1397
+ <div className="grid grid-cols-1 gap-1 text-xs mt-2 shrink-0">
1398
+ {data.map((d) => (
1399
+ <div key={d.name} className="flex items-center gap-2">
1400
+ <span className="h-2 w-2 rounded-full shrink-0" style={{ background: d.fill }} />
1401
+ <span className="text-muted-foreground flex-1">{radialCfg[d.name]?.label}</span>
1402
+ <span className="font-semibold tabular-nums">{d.score}%</span>
1403
+ </div>
1404
+ ))}
1405
+ </div>
1406
+ <ChartDataTable
1407
+ caption="Compliance Score"
1408
+ headers={["Program", "Score"]}
1409
+ rows={data.map(d => {
1410
+ const raw = radialCfg[d.name]?.label ?? d.name
1411
+ const label =
1412
+ typeof raw === "string" || typeof raw === "number" ? String(raw) : String(d.name)
1413
+ return [label, `${d.score}%`] as [string, string]
1414
+ })}
1415
+ />
1416
+ </>
1417
+ )}
1418
+ </ChartFigure>
1419
+ )
1420
+ }
1421
+
1422
+ function RadialLineTrend() {
1423
+ const data = radialData.map((d, i) => ({
1424
+ name: d.name,
1425
+ score: d.score,
1426
+ prev: d.score - [4, 7, 2, 9, 5][i],
1427
+ }))
1428
+ const cfg: ChartConfig = {
1429
+ score: { label: "Current", color: BRAND },
1430
+ prev: { label: "Previous", color: CHART_2 },
1431
+ }
1432
+ return (
1433
+ <ChartContainer config={cfg} className="flex-1 min-h-[180px] w-full">
1434
+ <BarChart data={data} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1435
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1436
+ <XAxis dataKey="name" tickLine={false} axisLine={false} tick={{ fontSize: 11 }} />
1437
+ <YAxis domain={[70, 100]} tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={32} />
1438
+ <ChartTooltip content={<ChartTooltipContent />} />
1439
+ <ChartLegend content={<ChartLegendContent />} />
1440
+ <Bar dataKey="prev" fill={CHART_2} radius={[4, 4, 0, 0]} opacity={0.5} />
1441
+ <Bar dataKey="score" fill={BRAND} radius={[4, 4, 0, 0]} />
1442
+ </BarChart>
1443
+ </ChartContainer>
1444
+ )
1445
+ }
1446
+
1447
+ /** Quota radial — ChartFigure, keyboard tooltip sync, sr-only table (same pattern as RadialBarContent). */
1448
+ function QuotaRadialGalleryContent({ radial }: { radial: StudentScoreRadial }) {
1449
+ const summary =
1450
+ `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}.`
1451
+
1452
+ return (
1453
+ <ChartFigure label={radial.title} summary={summary} dataLength={1} leoInsight={CHART_GALLERY_LEO_QUOTA}>
1454
+ {(activeIndex) => (
1455
+ <>
1456
+ <div className="flex flex-col items-center gap-2">
1457
+ <QuotaRadialChartInner radial={radial} activeIndex={activeIndex} />
1458
+ <p className="text-xs text-muted-foreground tabular-nums">
1459
+ Class avg{" "}
1460
+ <span className="font-medium text-foreground">{formatBandScore(radial.classAverage)}</span>
1461
+ <span className="text-muted-foreground">
1462
+ {" "}
1463
+ · scale {formatBandScore(radial.scaleMin)}–{formatBandScore(radial.scaleMax)}
1464
+ </span>
1465
+ </p>
1466
+ </div>
1467
+ <ChartDataTable
1468
+ caption={radial.title}
1469
+ headers={["Measure", "Value"]}
1470
+ rows={[
1471
+ ["Student score", formatBandScore(radial.studentScore)],
1472
+ ["Class average", formatBandScore(radial.classAverage)],
1473
+ ["Scale", `${formatBandScore(radial.scaleMin)}–${formatBandScore(radial.scaleMax)}`],
1474
+ ]}
1475
+ />
1476
+ </>
1477
+ )}
1478
+ </ChartFigure>
1479
+ )
1480
+ }
1481
+
1482
+ /* ── Horizontal Bar ─────────────────────────────────────────────────────── */
1483
+ const hBarCfg: ChartConfig = {
1484
+ placements: { label: "Placements", color: BRAND },
1485
+ }
1486
+ const hBarData = [
1487
+ { site: "City Med", placements: 42 },
1488
+ { site: "Westside Hosp", placements: 37 },
1489
+ { site: "North Clinic", placements: 31 },
1490
+ { site: "Bay Health", placements: 28 },
1491
+ { site: "Eastview", placements: 22 },
1492
+ { site: "Lakeshore", placements: 18 },
1493
+ { site: "Pinehill", placements: 14 },
1494
+ ]
1495
+
1496
+ const hBarByPeriod: Record<string, typeof hBarData> = {
1497
+ "7d": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 0.35) })),
1498
+ "30d": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 0.6) })),
1499
+ "90d": hBarData,
1500
+ "1y": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 4.2) })),
1501
+ }
1502
+
1503
+ function HorizontalBarContent({ data = hBarData }: { data?: typeof hBarData }) {
1504
+ return (
1505
+ <ChartFigure label="Placements by Site" summary="Horizontal bar chart showing placement count by clinical site" dataLength={data.length} leoInsight={CHART_GALLERY_LEO_HORIZONTAL}>
1506
+ {(activeIndex) => (
1507
+ <>
1508
+ <ChartContainer config={hBarCfg} className="flex-1 min-h-[200px] w-full">
1509
+ <BarChart data={data} layout="vertical" margin={{ left: 4, right: 16, top: 4, bottom: 0 }}>
1510
+ <CartesianGrid horizontal={false} strokeDasharray="3 3" stroke="var(--border)" />
1511
+ <XAxis type="number" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1512
+ <YAxis type="category" dataKey="site" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={82} />
1513
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1514
+ <Bar dataKey="placements" fill={BRAND} radius={[0, 4, 4, 0]} activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1515
+ </BarChart>
1516
+ </ChartContainer>
1517
+ <ChartDataTable caption="Placements by Site" headers={["Site", "Placements"]} rows={data.map(d => [d.site, d.placements])} />
1518
+ </>
1519
+ )}
1520
+ </ChartFigure>
1521
+ )
1522
+ }
1523
+
1524
+ function HBarLineTrend() {
1525
+ return (
1526
+ <ChartContainer config={hBarCfg} className="flex-1 min-h-[200px] w-full">
1527
+ <LineChart data={hBarData} margin={{ left: -8, right: 16, top: 4, bottom: 0 }}>
1528
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1529
+ <XAxis dataKey="site" tickLine={false} axisLine={false} tick={{ fontSize: 10 }} />
1530
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={32} />
1531
+ <ChartTooltip content={<ChartTooltipContent />} />
1532
+ <Line type="monotone" dataKey="placements" stroke={BRAND} strokeWidth={2} dot={{ r: 4, fill: BRAND }} />
1533
+ </LineChart>
1534
+ </ChartContainer>
1535
+ )
1536
+ }
1537
+
1538
+ /* ── Composed ─────────────────────────────────────────────────────────────── */
1539
+ const composedCfg: ChartConfig = {
1540
+ placements: { label: "Placements", color: BRAND },
1541
+ capacity: { label: "Capacity", color: CHART_3 },
1542
+ rate: { label: "Fill Rate %", color: CHART_4 },
1543
+ }
1544
+ const composedData = [
1545
+ { month: "Sep", placements: 44, capacity: 60, rate: 73 },
1546
+ { month: "Oct", placements: 53, capacity: 65, rate: 82 },
1547
+ { month: "Nov", placements: 67, capacity: 80, rate: 84 },
1548
+ { month: "Dec", placements: 49, capacity: 70, rate: 70 },
1549
+ { month: "Jan", placements: 74, capacity: 85, rate: 87 },
1550
+ { month: "Feb", placements: 81, capacity: 90, rate: 90 },
1551
+ { month: "Mar", placements: 89, capacity: 95, rate: 94 },
1552
+ ]
1553
+
1554
+ function ComposedChartContent() {
1555
+ return (
1556
+ <ChartFigure label="Site Capacity vs Fill Rate" summary="Composed chart showing placement volume against site capacity and fill rate percentage" dataLength={composedData.length} leoInsight={CHART_GALLERY_LEO_COMPOSED}>
1557
+ {(activeIndex) => (
1558
+ <>
1559
+ <ChartContainer config={composedCfg} className="flex-1 min-h-[180px] w-full">
1560
+ <ComposedChart data={composedData} margin={{ left: -8, right: 28, top: 4, bottom: 0 }}>
1561
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1562
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1563
+ <YAxis yAxisId="left" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1564
+ <YAxis yAxisId="right" orientation="right" tickLine={false} axisLine={false}
1565
+ tick={{ fontSize: 12 }} width={32} unit="%" domain={[0, 100]} />
1566
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1567
+ <ChartLegend content={<ChartLegendContent />} />
1568
+ <Bar yAxisId="left" dataKey="capacity" fill={CHART_3} radius={[4,4,0,0]} opacity={0.45} activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1569
+ <Bar yAxisId="left" dataKey="placements" fill={BRAND} radius={[4,4,0,0]} activeBar={{ stroke: "var(--ring)", strokeWidth: 2, fillOpacity: 1 }} activeIndex={activeIndex ?? undefined} />
1570
+ <Line yAxisId="right" dataKey="rate" stroke={CHART_4} strokeWidth={2}
1571
+ dot={(props: LineDotRenderProps) => props.index === activeIndex ? <circle key={props.key} cx={props.cx} cy={props.cy} r={5} fill={props.stroke} stroke="var(--ring)" strokeWidth={2} /> : <circle key={props.key} cx={props.cx} cy={props.cy} r={3} fill={CHART_4} />} type="monotone" />
1572
+ </ComposedChart>
1573
+ </ChartContainer>
1574
+ <ChartDataTable caption="Site Capacity vs Fill Rate" headers={["Month", "Placements", "Capacity", "Fill Rate %"]} rows={composedData.map(d => [d.month, d.placements, d.capacity, `${d.rate}%`])} />
1575
+ </>
1576
+ )}
1577
+ </ChartFigure>
1578
+ )
1579
+ }
1580
+
1581
+ function ComposedLineTrend() {
1582
+ const cfg: ChartConfig = {
1583
+ placements: { label: "Placements", color: BRAND },
1584
+ capacity: { label: "Capacity", color: CHART_3 },
1585
+ rate: { label: "Fill Rate %", color: CHART_4 },
1586
+ }
1587
+ return (
1588
+ <ChartContainer config={cfg} className="flex-1 min-h-[180px] w-full">
1589
+ <LineChart data={composedData} margin={{ left: -8, right: 28, top: 4, bottom: 0 }}>
1590
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1591
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1592
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1593
+ <ChartTooltip content={<ChartTooltipContent />} />
1594
+ <ChartLegend content={<ChartLegendContent />} />
1595
+ <Line type="monotone" dataKey="placements" stroke={BRAND} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={{ r: 3 }} />
1596
+ <Line type="monotone" dataKey="capacity" stroke={CHART_3} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={{ r: 3 }} />
1597
+ <Line type="monotone" dataKey="rate" stroke={CHART_4} strokeWidth={2} strokeDasharray={chartLineStrokeDash(2)} dot={{ r: 3 }} />
1598
+ </LineChart>
1599
+ </ChartContainer>
1600
+ )
1601
+ }
1602
+
1603
+ /* ── Radar ───────────────────────────────────────────────────────────────── */
1604
+ const radarCfg: ChartConfig = {
1605
+ nursing: { label: "Nursing", color: BRAND },
1606
+ physical: { label: "PT/OT", color: CHART_2 },
1607
+ }
1608
+ const radarData = [
1609
+ { skill: "Clinical", nursing: 92, physical: 88 },
1610
+ { skill: "Comm.", nursing: 85, physical: 79 },
1611
+ { skill: "Critical", nursing: 78, physical: 84 },
1612
+ { skill: "Teamwork", nursing: 91, physical: 90 },
1613
+ { skill: "Ethics", nursing: 96, physical: 93 },
1614
+ { skill: "Technical", nursing: 80, physical: 87 },
1615
+ ]
1616
+
1617
+ function RadarChartContent() {
1618
+ return (
1619
+ <ChartFigure label="Competency Radar" summary="Radar chart comparing nursing vs PT/OT competency scores across 6 skill dimensions" dataLength={radarData.length} leoInsight={CHART_GALLERY_LEO_RADAR}>
1620
+ {(activeIndex) => (
1621
+ <>
1622
+ <ChartContainer config={radarCfg} className="flex-1 min-h-[200px] w-full">
1623
+ <RadarChart data={radarData} margin={{ top: 8, right: 16, bottom: 8, left: 16 }}>
1624
+ <PolarGrid stroke="var(--border)" />
1625
+ <PolarAngleAxis dataKey="skill" tick={{ fontSize: 11 }} />
1626
+ <PolarRadiusAxis angle={30} domain={[60, 100]} tick={{ fontSize: 10 }} tickCount={3} stroke="var(--border)" />
1627
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
1628
+ <ChartLegend content={<ChartLegendContent />} />
1629
+ <Radar name="nursing" dataKey="nursing" stroke={BRAND} fill={BRAND} fillOpacity={0.25} strokeWidth={2} activeDot={{ r: 6, stroke: "var(--ring)", strokeWidth: 2 }} />
1630
+ <Radar name="physical" dataKey="physical" stroke={CHART_2} fill={CHART_2} fillOpacity={0.2} strokeWidth={2} activeDot={{ r: 6, stroke: "var(--ring)", strokeWidth: 2 }} />
1631
+ </RadarChart>
1632
+ </ChartContainer>
1633
+ <ChartDataTable caption="Competency Scores" headers={["Skill", "Nursing", "PT/OT"]} rows={radarData.map(d => [d.skill, d.nursing, d.physical])} />
1634
+ </>
1635
+ )}
1636
+ </ChartFigure>
1637
+ )
1638
+ }
1639
+
1640
+ function RadarBarTrend() {
1641
+ const cfg: ChartConfig = {
1642
+ nursing: { label: "Nursing", color: BRAND },
1643
+ physical: { label: "PT/OT", color: CHART_2 },
1644
+ }
1645
+ return (
1646
+ <ChartContainer config={cfg} className="flex-1 min-h-[200px] w-full">
1647
+ <BarChart data={radarData} barGap={4} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1648
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1649
+ <XAxis dataKey="skill" tickLine={false} axisLine={false} tick={{ fontSize: 11 }} />
1650
+ <YAxis domain={[60, 100]} tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={28} />
1651
+ <ChartTooltip content={<ChartTooltipContent />} />
1652
+ <ChartLegend content={<ChartLegendContent />} />
1653
+ <Bar dataKey="nursing" fill={BRAND} radius={[4, 4, 0, 0]} />
1654
+ <Bar dataKey="physical" fill={CHART_2} radius={[4, 4, 0, 0]} />
1655
+ </BarChart>
1656
+ </ChartContainer>
1657
+ )
1658
+ }
1659
+
1660
+ /* ── Scatter ─────────────────────────────────────────────────────────────── */
1661
+ const scatterCfg: ChartConfig = {
1662
+ nursing: { label: "Nursing", color: BRAND },
1663
+ pt: { label: "PT", color: CHART_2 },
1664
+ ot: { label: "OT", color: SUCCESS },
1665
+ pharmacy: { label: "Pharmacy", color: WARNING },
1666
+ }
1667
+ 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 }]
1668
+ 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 }]
1669
+ const scatterOT = [{ x: 30, y: 88, z: 16 }, { x: 45, y: 92, z: 24 }, { x: 38, y: 84, z: 19 }]
1670
+ const scatterPharmacy = [{ x: 25, y: 76, z: 12 }, { x: 35, y: 82, z: 17 }, { x: 20, y: 71, z: 9 }]
1671
+
1672
+ function ScatterChartContent() {
1673
+ return (
1674
+ <ChartContainer config={scatterCfg} className="flex-1 min-h-[200px] w-full">
1675
+ <ScatterChart margin={{ left: -8, right: 16, top: 4, bottom: 0 }}>
1676
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
1677
+ <XAxis type="number" dataKey="x" name="Capacity" tickLine={false} axisLine={false} tick={{ fontSize: 12 }}
1678
+ label={{ value: "Capacity", position: "insideBottom", offset: -2, fontSize: 11 }} />
1679
+ <YAxis type="number" dataKey="y" name="Fill Rate" tickLine={false} axisLine={false}
1680
+ tick={{ fontSize: 12 }} unit="%" domain={[60, 100]} width={38} />
1681
+ <ZAxis type="number" dataKey="z" range={[40, 280]} name="Students" />
1682
+ <ChartTooltip cursor={{ strokeDasharray: "3 3" }} content={<ChartTooltipContent hideLabel />} />
1683
+ <ChartLegend content={<ChartLegendContent />} />
1684
+ <Scatter name="nursing" data={scatterNursing} fill={BRAND} fillOpacity={0.75} />
1685
+ <Scatter name="pt" data={scatterPT} fill={CHART_2} fillOpacity={0.75} />
1686
+ <Scatter name="ot" data={scatterOT} fill={SUCCESS} fillOpacity={0.75} />
1687
+ <Scatter name="pharmacy" data={scatterPharmacy} fill={WARNING} fillOpacity={0.75} />
1688
+ </ScatterChart>
1689
+ </ChartContainer>
1690
+ )
1691
+ }
1692
+
1693
+ function ScatterLineTrend() {
1694
+ const cfg: ChartConfig = {
1695
+ nursing: { label: "Nursing", color: BRAND },
1696
+ pt: { label: "PT", color: CHART_2 },
1697
+ }
1698
+ const data = [
1699
+ { month: "Oct", nursing: 88, pt: 80 },
1700
+ { month: "Nov", nursing: 91, pt: 82 },
1701
+ { month: "Dec", nursing: 89, pt: 79 },
1702
+ { month: "Jan", nursing: 93, pt: 84 },
1703
+ { month: "Feb", nursing: 95, pt: 87 },
1704
+ { month: "Mar", nursing: 94, pt: 85 },
1705
+ ]
1706
+ return (
1707
+ <ChartContainer config={cfg} className="flex-1 min-h-[200px] w-full">
1708
+ <LineChart data={data} margin={{ left: -8, right: 16, top: 4, bottom: 0 }}>
1709
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1710
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1711
+ <YAxis domain={[70, 100]} tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={32} unit="%" />
1712
+ <ChartTooltip content={<ChartTooltipContent />} />
1713
+ <ChartLegend content={<ChartLegendContent />} />
1714
+ <Line type="monotone" dataKey="nursing" stroke={BRAND} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={{ r: 3 }} />
1715
+ <Line type="monotone" dataKey="pt" stroke={CHART_2} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={{ r: 3 }} />
1716
+ </LineChart>
1717
+ </ChartContainer>
1718
+ )
1719
+ }
1720
+
1721
+ /* ── Funnel ──────────────────────────────────────────────────────────────── */
1722
+ const funnelCfg: ChartConfig = {
1723
+ applied: { label: "Applied", color: BRAND },
1724
+ screened: { label: "Screened", color: CHART_2 },
1725
+ matched: { label: "Matched", color: SUCCESS },
1726
+ placed: { label: "Placed", color: CHART_4 },
1727
+ completed: { label: "Completed", color: CHART_5 },
1728
+ }
1729
+ const funnelData = [
1730
+ { name: "Applied", value: 320, fill: BRAND },
1731
+ { name: "Screened", value: 240, fill: CHART_2 },
1732
+ { name: "Matched", value: 175, fill: SUCCESS },
1733
+ { name: "Placed", value: 128, fill: CHART_4 },
1734
+ { name: "Completed", value: 98, fill: CHART_5 },
1735
+ ]
1736
+ const funnelDataByPeriod: Record<string, typeof funnelData> = {
1737
+ "7d": funnelData.map((d) => ({ ...d, value: Math.round(d.value * 0.08) })),
1738
+ "30d": funnelData.map((d) => ({ ...d, value: Math.round(d.value * 0.3) })),
1739
+ "90d": funnelData,
1740
+ "1y": funnelData.map((d) => ({ ...d, value: d.value * 4 })),
1741
+ }
1742
+
1743
+ function FunnelChartContent({ data = funnelData }: { data?: typeof funnelData }) {
1744
+ const summary = `Funnel with ${data.length} stages from ${data[0]?.name ?? ""} to ${data[data.length - 1]?.name ?? ""}.`
1745
+ return (
1746
+ <ChartFigure label="Application Pipeline" summary={summary} dataLength={data.length} leoInsight={CHART_GALLERY_LEO_FUNNEL}>
1747
+ {(activeIndex) => (
1748
+ <>
1749
+ <ChartContainer config={funnelCfg} className="flex-1 min-h-[220px] w-full">
1750
+ <FunnelChart margin={{ top: 8, right: 32, bottom: 8, left: 32 }}>
1751
+ <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent hideLabel />} />
1752
+ <Funnel dataKey="value" data={data} isAnimationActive>
1753
+ {data.map((d, i) => (
1754
+ <Cell
1755
+ key={d.name}
1756
+ fill={d.fill}
1757
+ stroke={activeIndex === i ? "var(--ring)" : undefined}
1758
+ strokeWidth={activeIndex === i ? 2 : 0}
1759
+ />
1760
+ ))}
1761
+ <LabelList dataKey="name" position="right" style={{ fontSize: 12, fill: "var(--foreground)" }} />
1762
+ <LabelList dataKey="value" position="center" style={{ fontSize: 12, fontWeight: 600, fill: "var(--foreground)" }} />
1763
+ </Funnel>
1764
+ </FunnelChart>
1765
+ </ChartContainer>
1766
+ <ChartDataTable caption="Application Pipeline data" headers={["Stage", "Count"]} rows={data.map(d => [d.name, d.value])} />
1767
+ </>
1768
+ )}
1769
+ </ChartFigure>
1770
+ )
1771
+ }
1772
+
1773
+ function FunnelLineTrend() {
1774
+ const cfg: ChartConfig = {
1775
+ applied: { label: "Applied", color: BRAND },
1776
+ placed: { label: "Placed", color: CHART_4 },
1777
+ completed: { label: "Completed", color: CHART_5 },
1778
+ }
1779
+ const data = [
1780
+ { month: "Oct", applied: 210, placed: 95, completed: 68 },
1781
+ { month: "Nov", applied: 245, placed: 108, completed: 82 },
1782
+ { month: "Dec", applied: 180, placed: 88, completed: 64 },
1783
+ { month: "Jan", applied: 280, placed: 120, completed: 91 },
1784
+ { month: "Feb", applied: 300, placed: 124, completed: 95 },
1785
+ { month: "Mar", applied: 320, placed: 128, completed: 98 },
1786
+ ]
1787
+ return (
1788
+ <ChartContainer config={cfg} className="flex-1 min-h-[220px] w-full">
1789
+ <LineChart data={data} margin={{ left: -8, right: 4, top: 4, bottom: 0 }}>
1790
+ <CartesianGrid vertical={false} strokeDasharray="3 3" stroke="var(--border)" />
1791
+ <XAxis dataKey="month" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} />
1792
+ <YAxis tickLine={false} axisLine={false} tick={{ fontSize: 12 }} width={36} />
1793
+ <ChartTooltip content={<ChartTooltipContent />} />
1794
+ <ChartLegend content={<ChartLegendContent />} />
1795
+ <Line type="monotone" dataKey="applied" stroke={BRAND} strokeWidth={2} strokeDasharray={chartLineStrokeDash(0)} dot={{ r: 3 }} />
1796
+ <Line type="monotone" dataKey="placed" stroke={CHART_4} strokeWidth={2} strokeDasharray={chartLineStrokeDash(1)} dot={{ r: 3 }} />
1797
+ <Line type="monotone" dataKey="completed" stroke={CHART_5} strokeWidth={2} strokeDasharray={chartLineStrokeDash(2)} dot={{ r: 3 }} />
1798
+ </LineChart>
1799
+ </ChartContainer>
1800
+ )
1801
+ }
1802
+
1803
+ /* ════════════════════════════════════════════════════════════════════════════
1804
+ Chart rows — shared across variants
1805
+ ════════════════════════════════════════════════════════════════════════════ */
1806
+
1807
+ const CHART_GALLERY_LEO_DONUT: ChartLeoInsight = {
1808
+ headline: "Confirmed placements dominate the current pipeline",
1809
+ explanation:
1810
+ "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.",
1811
+ kind: "spike",
1812
+ delta: { value: "+12%", label: "vs. last month" },
1813
+ bullets: [
1814
+ "Confirmed count has grown steadily across nursing, PT, and OT programs.",
1815
+ "Rejection rate remains low at 1% — applications are well-qualified.",
1816
+ ],
1817
+ }
1818
+
1819
+ const CHART_GALLERY_LEO_APPLICATIONS: ChartLeoInsight = {
1820
+ headline: "Nursing program leads application volume",
1821
+ explanation:
1822
+ "Nursing consistently attracts the most new applicants, with 34 this period. PT and OT follow closely. Returned applications suggest strong re-engagement.",
1823
+ kind: "trend",
1824
+ delta: { value: "+8%", label: "new vs. prior period" },
1825
+ bullets: [
1826
+ "Nursing: 34 new, 22 returned — highest volume and strong re-engagement.",
1827
+ "PT and OT: steady demand — balanced load across clinical programs.",
1828
+ ],
1829
+ }
1830
+
1831
+ const CHART_GALLERY_LEO_LINE: ChartLeoInsight = {
1832
+ headline: "Portal activity peaks mid-week",
1833
+ explanation:
1834
+ "Login, submission, and evaluation activity cluster around Tuesday–Thursday, with weekends showing predictable dips. This pattern is consistent and expected for an academic schedule.",
1835
+ kind: "trend",
1836
+ delta: { value: "—", label: "stable pattern" },
1837
+ bullets: [
1838
+ "Logins peak at ~450 on Wednesdays.",
1839
+ "Submissions highest Monday–Friday, near-zero on weekends.",
1840
+ ],
1841
+ }
1842
+
1843
+ const CHART_GALLERY_LEO_COMPLIANCE: ChartLeoInsight = {
1844
+ headline: "PT/OT programs lead compliance scoring",
1845
+ explanation:
1846
+ "PT and OT average 88–89% compliance, outpacing Nursing (82%) and Pharmacy (76%). Radiology lags at 71% — may need targeted support.",
1847
+ kind: "dip",
1848
+ delta: { value: "-8%", label: "Radiology vs. PT/OT" },
1849
+ bullets: [
1850
+ "PT/OT: consistent excellence across all 6 dimensions.",
1851
+ "Pharmacy: scoring gaps in documentation and timeliness.",
1852
+ "Radiology: needs support in scheduling and follow-up processes.",
1853
+ ],
1854
+ }
1855
+
1856
+ const CHART_GALLERY_LEO_HORIZONTAL: ChartLeoInsight = {
1857
+ headline: "Large clinical sites carry most placements",
1858
+ explanation:
1859
+ "The three largest sites (University Hospital, Metro Clinic, Regional Center) account for 58% of all placements. Mid-size sites are under-utilized.",
1860
+ kind: "anomaly",
1861
+ delta: { value: "+22%", label: "top 3 sites total" },
1862
+ bullets: [
1863
+ "University Hospital: 156 placements (28% of total).",
1864
+ "Capacity constraints may limit placement growth at smaller sites.",
1865
+ ],
1866
+ }
1867
+
1868
+ const CHART_GALLERY_LEO_COMPOSED: ChartLeoInsight = {
1869
+ headline: "Site capacity is healthy; fill rates peak Q2",
1870
+ explanation:
1871
+ "Most sites run 85–92% capacity utilization. Fill rate (placements / capacity) averages 78%, with spring months (Feb–Mar) consistently hitting 82%+.",
1872
+ kind: "spike",
1873
+ delta: { value: "+6%", label: "fill rate increase" },
1874
+ bullets: [
1875
+ "March shows the strongest fill rate at 84%.",
1876
+ "Only 2 sites are below 70% utilization — opportunity to rebalance.",
1877
+ ],
1878
+ }
1879
+
1880
+ const CHART_GALLERY_LEO_RADAR: ChartLeoInsight = {
1881
+ headline: "Nursing and PT/OT competencies are well-balanced",
1882
+ explanation:
1883
+ "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.",
1884
+ kind: "trend",
1885
+ delta: { value: "—", label: "strong across programs" },
1886
+ bullets: [
1887
+ "6-dimension average: Nursing 84%, PT/OT 86%.",
1888
+ "Lowest dimension: patient care (Nursing 79%) — room to develop.",
1889
+ ],
1890
+ }
1891
+
1892
+ const CHART_GALLERY_LEO_SCATTER: ChartLeoInsight = {
1893
+ headline: "Application-to-placement funnel is healthy",
1894
+ explanation:
1895
+ "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.",
1896
+ kind: "trend",
1897
+ delta: { value: "+4%", label: "confirmation rate" },
1898
+ bullets: [
1899
+ "Applications → Offers: 63% convert (typical for competitive placements).",
1900
+ "Offers → Confirmed: 72% accept (strong acceptance rate).",
1901
+ ],
1902
+ }
1903
+
1904
+ const CHART_GALLERY_LEO_FUNNEL: ChartLeoInsight = {
1905
+ headline: "Funnel shape is expected; strong at top of pipe",
1906
+ explanation:
1907
+ "4,200 applications narrow to 842 offers (20% funnel rate) and 604 confirmed placements (72% offer acceptance). Losses are proportional—no anomalous drops.",
1908
+ kind: "trend",
1909
+ delta: { value: "+8%", label: "application volume" },
1910
+ bullets: [
1911
+ "Application → Offer: drop-off is typical for screening.",
1912
+ "Offer → Confirmed: acceptance rate of 72% is healthy.",
1913
+ ],
1914
+ }
1915
+
1916
+ const CHART_GALLERY_LEO_QUOTA: ChartLeoInsight = {
1917
+ headline: "Student performance tracking and cohort comparison",
1918
+ explanation:
1919
+ "Track individual student progress against class averages and scale benchmarks. Identify outliers above or below cohort norms.",
1920
+ kind: "anomaly",
1921
+ bullets: [
1922
+ "Performance visualized on a consistent scale across cohorts.",
1923
+ "Class average provides immediate context for comparison.",
1924
+ ],
1925
+ }
1926
+
1927
+ const CHART_GALLERY_LEO_TRENDS: ChartLeoInsight = {
1928
+ headline: "December dips across placements, applications, and reviews",
1929
+ explanation:
1930
+ "All three series pull back in December—often seasonal (holidays, academic breaks) or a real pipeline stall. Worth confirming whether approvals or site capacity paused.",
1931
+ kind: "dip",
1932
+ delta: { value: "-24%", label: "vs. November" },
1933
+ bullets: [
1934
+ "Placements are 18% below the 6-month trailing average.",
1935
+ "Reviews dropped sharply in the last 2 weeks of the month.",
1936
+ "Same pattern appeared in Dec '24 — seasonal signal is plausible.",
1937
+ ],
1938
+ anchor: {
1939
+ xValue: "Dec",
1940
+ yDataKeys: ["placements", "applications", "reviews"],
1941
+ yCombine: "max",
1942
+ },
1943
+ }
1944
+
1945
+ const CHART_GALLERY_LEO_REVIEWS: ChartLeoInsight = {
1946
+ headline: "December is the low point in review throughput",
1947
+ explanation:
1948
+ "Totals drop before recovering — worth confirming whether fewer submissions arrived or reviewers were out. Pending and rejected slices still matter once volume returns.",
1949
+ kind: "dip",
1950
+ delta: { value: "-31%", label: "vs. November total" },
1951
+ bullets: [
1952
+ "Approved reviews fell from 68 to 47 month-over-month.",
1953
+ "Pending queue grew by 9 items — backlog forming.",
1954
+ "Two reviewers were OOO for most of the last two weeks.",
1955
+ ],
1956
+ anchor: {
1957
+ xValue: "Dec",
1958
+ yDataKeys: ["approved", "pending", "rejected"],
1959
+ yCombine: "sum",
1960
+ },
1961
+ }
1962
+
1963
+ function ChartRows({ v }: { v: ChartCardVariant }) {
1964
+ const isTabs = v === "tabs"
1965
+ const isSel = v === "selector"
1966
+ const isMT = v === "metrics-tabs"
1967
+ const isKpi = v === "kpi-chart"
1968
+
1969
+ return (
1970
+ <>
1971
+ {/* Row 1 · Area (2/3) + Donut (1/3) */}
1972
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-3 items-stretch">
1973
+ <div className="lg:col-span-2 flex flex-col">
1974
+ {isSel ? (
1975
+ <ChartCard
1976
+ key="area-sel"
1977
+ variant="selector"
1978
+ title="Placement Trends"
1979
+ description="Filter by time period"
1980
+ filterOptions={PERIOD_OPTIONS}
1981
+ defaultFilter="90d"
1982
+ leoInsight={CHART_GALLERY_LEO_TRENDS}
1983
+ >
1984
+ {(f) => <AreaSelectorContent filter={f} />}
1985
+ </ChartCard>
1986
+ ) : (
1987
+ <ChartCard
1988
+ key="area"
1989
+ variant={v}
1990
+ title="Placement Trends"
1991
+ description="Aug 2025 — Mar 2026"
1992
+ leoInsight={CHART_GALLERY_LEO_TRENDS}
1993
+ trendContent={<AreaLineTrendContent />}
1994
+ tabOptions={isTabs ? [
1995
+ { value: "overview", label: "Overview" },
1996
+ { value: "by-program", label: "By Program" },
1997
+ { value: "trend", label: "Trend" },
1998
+ ] : undefined}
1999
+ miniMetrics={(isMT || isKpi) ? [
2000
+ { label: "Placements", value: "89", trend: "up" },
2001
+ { label: "Fill rate", value: "94%", trend: "up" },
2002
+ { label: "Avg. weeks", value: "6.2", trend: "neutral" },
2003
+ ] : undefined}>
2004
+ {isTabs
2005
+ ? (tab: string) => tab === "trend" ? <AreaLineTrendContent /> : <AreaChartContent />
2006
+ : <AreaChartContent />}
2007
+ </ChartCard>
2008
+ )}
2009
+ </div>
2010
+ <div className="flex flex-col">
2011
+ {isSel ? (
2012
+ <ChartCard key="donut-sel" variant="selector" title="Placement Status" description="Filter by program"
2013
+ filterOptions={PROGRAM_OPTIONS} defaultFilter="all" leoInsight={CHART_GALLERY_LEO_DONUT}>
2014
+ {(f) => <DonutChartContent data={donutByProgram[f] ?? donutDataAll} />}
2015
+ </ChartCard>
2016
+ ) : (
2017
+ <ChartCard key="donut" variant={v} title="Placement Status" description="Current cycle distribution"
2018
+ leoInsight={CHART_GALLERY_LEO_DONUT}
2019
+ trendContent={<DonutBarTrendContent />}
2020
+ tabOptions={isTabs ? [
2021
+ { value: "current", label: "Current Cycle" },
2022
+ { value: "previous", label: "Previous Cycle" },
2023
+ ] : undefined}
2024
+ miniMetrics={(isMT || isKpi) ? [
2025
+ { label: "Placed", value: "128", trend: "up" },
2026
+ { label: "Pending", value: "23", trend: "down" },
2027
+ ] : undefined}>
2028
+ {isTabs
2029
+ ? (tab: string) => <DonutChartContent data={tab === "previous" ? donutByProgram["pt"] : undefined} />
2030
+ : <DonutChartContent />}
2031
+ </ChartCard>
2032
+ )}
2033
+ </div>
2034
+ </div>
2035
+
2036
+ {/* Row 1b · Quota suite — one ChartCard per metric + radial (ChartFigure on radial only) */}
2037
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 items-stretch">
2038
+ {DASHBOARD_STUDENT_SCORES.metrics.map((m) => (
2039
+ <ChartCard
2040
+ key={`quota-${m.id}`}
2041
+ variant={v}
2042
+ title={m.label}
2043
+ description={m.description ?? DASHBOARD_STUDENT_SCORES.description ?? ""}
2044
+ className="overflow-visible"
2045
+ >
2046
+ <QuotaLinearProgressCardBody
2047
+ metric={m}
2048
+ suiteContext={DASHBOARD_STUDENT_SCORES.description ?? "Reference data."}
2049
+ />
2050
+ </ChartCard>
2051
+ ))}
2052
+ <ChartCard
2053
+ key="quota-radial"
2054
+ variant={v}
2055
+ title={DASHBOARD_STUDENT_SCORES.radial.title}
2056
+ description={DASHBOARD_STUDENT_SCORES.description ?? ""}
2057
+ className="overflow-visible"
2058
+ >
2059
+ <QuotaRadialGalleryContent radial={DASHBOARD_STUDENT_SCORES.radial} />
2060
+ </ChartCard>
2061
+ </div>
2062
+
2063
+ {/* Row 2 · Grouped Bar + Stacked Bar */}
2064
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 items-stretch">
2065
+ {isSel ? (
2066
+ <ChartCard key="gbar-sel" variant="selector" title="Applications by Program" description="Filter by time period"
2067
+ filterOptions={PERIOD_OPTIONS} defaultFilter="30d" leoInsight={CHART_GALLERY_LEO_APPLICATIONS}>
2068
+ {() => <GroupedBarContent />}
2069
+ </ChartCard>
2070
+ ) : (
2071
+ <ChartCard key="gbar" variant={v} title="Applications by Program" description="New vs. returning students"
2072
+ leoInsight={CHART_GALLERY_LEO_APPLICATIONS}
2073
+ trendContent={<GroupedBarLineTrend />}
2074
+ tabOptions={isTabs ? [
2075
+ { value: "all", label: "All Students" },
2076
+ { value: "new", label: "New" },
2077
+ { value: "trend", label: "Trend" },
2078
+ ] : undefined}
2079
+ miniMetrics={(isMT || isKpi) ? [
2080
+ { label: "Total", value: "320", trend: "up" },
2081
+ { label: "New", value: "78%", trend: "up" },
2082
+ { label: "Returning", value: "22%", trend: "neutral" },
2083
+ ] : undefined}>
2084
+ {isTabs
2085
+ ? (tab: string) => tab === "trend" ? <GroupedBarLineTrend /> : <GroupedBarContent />
2086
+ : <GroupedBarContent />}
2087
+ </ChartCard>
2088
+ )}
2089
+ {isSel ? (
2090
+ <ChartCard
2091
+ key="sbar-sel"
2092
+ variant="selector"
2093
+ title="Monthly Reviews"
2094
+ description="Filter by time period"
2095
+ filterOptions={PERIOD_OPTIONS}
2096
+ defaultFilter="30d"
2097
+ leoInsight={CHART_GALLERY_LEO_REVIEWS}
2098
+ >
2099
+ {() => <StackedBarContent />}
2100
+ </ChartCard>
2101
+ ) : (
2102
+ <ChartCard
2103
+ key="sbar"
2104
+ variant={v}
2105
+ title="Monthly Reviews"
2106
+ description="Review outcomes by status"
2107
+ leoInsight={CHART_GALLERY_LEO_REVIEWS}
2108
+ trendContent={<StackedBarLineTrend />}
2109
+ tabOptions={isTabs ? [
2110
+ { value: "status", label: "By Status" },
2111
+ { value: "reviewer", label: "By Reviewer" },
2112
+ { value: "trend", label: "Trend" },
2113
+ ] : undefined}
2114
+ miniMetrics={(isMT || isKpi) ? [
2115
+ { label: "Approved", value: "68", trend: "up" },
2116
+ { label: "Pending", value: "14", trend: "down" },
2117
+ { label: "Rejected", value: "6", trend: "neutral" },
2118
+ ] : undefined}>
2119
+ {isTabs
2120
+ ? (tab: string) => tab === "trend" ? <StackedBarLineTrend /> : <StackedBarContent />
2121
+ : <StackedBarContent />}
2122
+ </ChartCard>
2123
+ )}
2124
+ </div>
2125
+
2126
+ {/* Row 3 · Line (2/3) + Radial (1/3) */}
2127
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-3 items-stretch">
2128
+ <div className="lg:col-span-2 flex flex-col">
2129
+ {isSel ? (
2130
+ <ChartCard key="line-sel" variant="selector" title="Weekly Activity" description="Filter by time period"
2131
+ filterOptions={PERIOD_OPTIONS} defaultFilter="90d" leoInsight={CHART_GALLERY_LEO_LINE}>
2132
+ {(f) => <LineChartContent data={lineDataByPeriod[f] ?? lineData} />}
2133
+ </ChartCard>
2134
+ ) : (
2135
+ <ChartCard key="line" variant={v} title="Weekly Activity" description="Logins, submissions & evaluations"
2136
+ leoInsight={CHART_GALLERY_LEO_LINE}
2137
+ trendContent={<LineAreaTrend />}
2138
+ tabOptions={isTabs ? [
2139
+ { value: "weekly", label: "Weekly" },
2140
+ { value: "monthly", label: "Monthly" },
2141
+ { value: "trend", label: "Trend" },
2142
+ ] : undefined}
2143
+ miniMetrics={(isMT || isKpi) ? [
2144
+ { label: "Logins", value: "1.2k", trend: "up" },
2145
+ { label: "Submissions", value: "340", trend: "up" },
2146
+ { label: "Evals", value: "88", trend: "neutral" },
2147
+ ] : undefined}>
2148
+ {isTabs
2149
+ ? (tab: string) => tab === "trend" ? <LineAreaTrend /> : <LineChartContent />
2150
+ : <LineChartContent />}
2151
+ </ChartCard>
2152
+ )}
2153
+ </div>
2154
+ <div className="flex flex-col">
2155
+ {isSel ? (
2156
+ <ChartCard key="radial-sel" variant="selector" title="Compliance Scores" description="Filter by program"
2157
+ filterOptions={PROGRAM_OPTIONS} defaultFilter="all" leoInsight={CHART_GALLERY_LEO_COMPLIANCE}>
2158
+ {() => <RadialBarContent />}
2159
+ </ChartCard>
2160
+ ) : (
2161
+ <ChartCard key="radial" variant={v} title="Compliance Scores" description="By program — current cycle"
2162
+ leoInsight={CHART_GALLERY_LEO_COMPLIANCE}
2163
+ trendContent={<RadialLineTrend />}
2164
+ tabOptions={isTabs ? [
2165
+ { value: "current", label: "Current" },
2166
+ { value: "historical", label: "Historical" },
2167
+ ] : undefined}
2168
+ miniMetrics={(isMT || isKpi) ? [
2169
+ { label: "Avg. score", value: "91%", trend: "up" },
2170
+ { label: "At risk", value: "3", trend: "down" },
2171
+ ] : undefined}>
2172
+ {isTabs
2173
+ ? (tab: string) => tab === "historical" ? <RadialLineTrend /> : <RadialBarContent />
2174
+ : <RadialBarContent />}
2175
+ </ChartCard>
2176
+ )}
2177
+ </div>
2178
+ </div>
2179
+
2180
+ {/* Row 4 · H-Bar (1/3) + Composed (2/3) */}
2181
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-3 items-stretch">
2182
+ <div className="flex flex-col">
2183
+ {isSel ? (
2184
+ <ChartCard key="hbar-sel" variant="selector" title="Top Placement Sites" description="Filter by time period"
2185
+ filterOptions={PERIOD_OPTIONS} defaultFilter="90d" leoInsight={CHART_GALLERY_LEO_HORIZONTAL}>
2186
+ {(f) => <HorizontalBarContent data={hBarByPeriod[f] ?? hBarData} />}
2187
+ </ChartCard>
2188
+ ) : (
2189
+ <ChartCard key="hbar" variant={v} title="Top Placement Sites" description="Active placements by facility"
2190
+ leoInsight={CHART_GALLERY_LEO_HORIZONTAL}
2191
+ trendContent={<HBarLineTrend />}
2192
+ tabOptions={isTabs ? [
2193
+ { value: "by-facility", label: "By Facility" },
2194
+ { value: "by-capacity", label: "By Capacity" },
2195
+ ] : undefined}
2196
+ miniMetrics={(isMT || isKpi) ? [
2197
+ { label: "Sites", value: "7", trend: "up" },
2198
+ { label: "Capacity", value: "94%", trend: "up" },
2199
+ ] : undefined}>
2200
+ {isTabs
2201
+ ? () => <HorizontalBarContent />
2202
+ : <HorizontalBarContent />}
2203
+ </ChartCard>
2204
+ )}
2205
+ </div>
2206
+ <div className="lg:col-span-2 flex flex-col">
2207
+ {isSel ? (
2208
+ <ChartCard key="composed-sel" variant="selector" title="Placements vs Capacity" description="Filter by time period"
2209
+ filterOptions={PERIOD_OPTIONS} defaultFilter="1y" leoInsight={CHART_GALLERY_LEO_COMPOSED}>
2210
+ {() => <ComposedChartContent />}
2211
+ </ChartCard>
2212
+ ) : (
2213
+ <ChartCard key="composed" variant={v} title="Placements vs Capacity" description="Monthly fill rate overlay"
2214
+ leoInsight={CHART_GALLERY_LEO_COMPOSED}
2215
+ trendContent={<ComposedLineTrend />}
2216
+ tabOptions={isTabs ? [
2217
+ { value: "overlay", label: "Overlay" },
2218
+ { value: "comparison", label: "Side by Side" },
2219
+ { value: "trend", label: "Trend" },
2220
+ ] : undefined}
2221
+ miniMetrics={(isMT || isKpi) ? [
2222
+ { label: "Fill rate", value: "94%", trend: "up" },
2223
+ { label: "Capacity", value: "95", trend: "up" },
2224
+ { label: "Placed", value: "89", trend: "up" },
2225
+ ] : undefined}>
2226
+ {isTabs
2227
+ ? (tab: string) => tab === "trend" ? <ComposedLineTrend /> : <ComposedChartContent />
2228
+ : <ComposedChartContent />}
2229
+ </ChartCard>
2230
+ )}
2231
+ </div>
2232
+ </div>
2233
+
2234
+ {/* Row 5 · Radar (1/3) + Scatter (2/3) */}
2235
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-3 items-stretch">
2236
+ <div className="flex flex-col">
2237
+ {isSel ? (
2238
+ <ChartCard key="radar-sel" variant="selector" title="Competency Radar" description="Filter by program"
2239
+ filterOptions={PROGRAM_OPTIONS} defaultFilter="all" leoInsight={CHART_GALLERY_LEO_RADAR}>
2240
+ {() => <RadarChartContent />}
2241
+ </ChartCard>
2242
+ ) : (
2243
+ <ChartCard key="radar" variant={v} title="Competency Radar" description="Avg. scores by skill domain"
2244
+ leoInsight={CHART_GALLERY_LEO_RADAR}
2245
+ trendContent={<RadarBarTrend />}
2246
+ tabOptions={isTabs ? [
2247
+ { value: "radar", label: "Radar" },
2248
+ { value: "breakdown", label: "Breakdown" },
2249
+ ] : undefined}
2250
+ miniMetrics={(isMT || isKpi) ? [
2251
+ { label: "Avg.", value: "88%", trend: "up" },
2252
+ { label: "Top", value: "Clinical", trend: "neutral" },
2253
+ ] : undefined}>
2254
+ {isTabs
2255
+ ? (tab: string) => tab === "breakdown" ? <RadarBarTrend /> : <RadarChartContent />
2256
+ : <RadarChartContent />}
2257
+ </ChartCard>
2258
+ )}
2259
+ </div>
2260
+ <div className="lg:col-span-2 flex flex-col">
2261
+ {isSel ? (
2262
+ <ChartCard key="scatter-sel" variant="selector" title="Site Performance" description="Filter by program"
2263
+ filterOptions={PROGRAM_OPTIONS} defaultFilter="all" leoInsight={CHART_GALLERY_LEO_SCATTER}>
2264
+ {() => <ScatterChartContent />}
2265
+ </ChartCard>
2266
+ ) : (
2267
+ <ChartCard key="scatter" variant={v} title="Site Performance" description="Capacity vs. fill rate · bubble = student count"
2268
+ leoInsight={CHART_GALLERY_LEO_SCATTER}
2269
+ trendContent={<ScatterLineTrend />}
2270
+ tabOptions={isTabs ? [
2271
+ { value: "scatter", label: "Scatter" },
2272
+ { value: "ranking", label: "Ranking" },
2273
+ { value: "trend", label: "Trend" },
2274
+ ] : undefined}
2275
+ miniMetrics={(isMT || isKpi) ? [
2276
+ { label: "Sites", value: "12", trend: "up" },
2277
+ { label: "Avg. rate", value: "87%", trend: "up" },
2278
+ { label: "Students", value: "320", trend: "up" },
2279
+ ] : undefined}>
2280
+ {isTabs
2281
+ ? (tab: string) => tab === "trend" ? <ScatterLineTrend /> : <ScatterChartContent />
2282
+ : <ScatterChartContent />}
2283
+ </ChartCard>
2284
+ )}
2285
+ </div>
2286
+ </div>
2287
+
2288
+ {/* Row 6 · Funnel full width */}
2289
+ {isSel ? (
2290
+ <ChartCard key="funnel-sel" variant="selector" title="Application Pipeline" description="Filter by time period"
2291
+ filterOptions={PERIOD_OPTIONS} defaultFilter="90d" leoInsight={CHART_GALLERY_LEO_FUNNEL}>
2292
+ {(f) => <FunnelChartContent data={funnelDataByPeriod[f] ?? funnelData} />}
2293
+ </ChartCard>
2294
+ ) : (
2295
+ <ChartCard key="funnel" variant={v} title="Application Pipeline" description="Funnel from application to completed placement"
2296
+ leoInsight={CHART_GALLERY_LEO_FUNNEL}
2297
+ trendContent={<FunnelLineTrend />}
2298
+ miniMetrics={(isMT || isKpi) ? [
2299
+ { label: "Applied", value: "320", trend: "up" },
2300
+ { label: "Placed", value: "128", trend: "up" },
2301
+ { label: "Completed", value: "98", trend: "up" },
2302
+ { label: "Drop-off", value: "69%", trend: "down" },
2303
+ ] : undefined}>
2304
+ <FunnelChartContent />
2305
+ </ChartCard>
2306
+ )}
2307
+ </>
2308
+ )
2309
+ }
2310
+
2311
+ /* ════════════════════════════════════════════════════════════════════════════
2312
+ Main export
2313
+ ════════════════════════════════════════════════════════════════════════════ */
2314
+
2315
+ export function ChartsOverview({ variant = "normal" }: { variant?: ChartCardVariant }) {
2316
+ return (
2317
+ <div className="flex flex-col gap-4 px-4 pb-2 lg:px-6">
2318
+ <ChartRows v={variant} />
2319
+ </div>
2320
+ )
2321
+ }