wrapped in
* (visually hidden, in DOM).
* • Columns mirror the chart axes; each data point is a .
* • Screen-reader users can navigate data with standard table shortcuts.
*
* 4. Colour & contrast
* • Chart series colours must achieve ≥ 3:1 contrast against the card bg.
* • Never use colour as the ONLY differentiator — pair with:
* - Dashed vs solid line strokes
* - Direct inline labels on lines/segments
* - Shape markers on data points (circle vs square vs triangle)
* • Text labels inside charts: ≥ 4.5:1 on their local background.
*
* 5. Focus ring on data points
* • Active/focused data point: 3px outline, ≥ 3:1 contrast, distinct
* from the hover state (use outline-offset to separate).
*
* 6. Tooltip accessibility
* • Tooltips must appear on keyboard focus, not only on mouse hover.
* • Tooltip content must be announced to the live region.
* • Tooltip must remain visible while it has focus (no auto-dismiss).
* ─────────────────────────────────────────────────────────────────────────────
*/
import * as React from "react"
import {
Area, AreaChart,
Bar, BarChart,
CartesianGrid,
Cell,
ComposedChart,
Funnel, FunnelChart, LabelList,
Line, LineChart,
Pie, PieChart,
PolarAngleAxis, PolarGrid, PolarRadiusAxis,
Radar, RadarChart,
RadialBar, RadialBarChart,
Scatter, ScatterChart,
XAxis, YAxis, ZAxis,
type DotItemDotProps,
} from "recharts"
import {
QuotaLinearProgressCardBody,
QuotaRadialChartInner,
} from "@/components/dashboard-quota-progress-card"
import {
DASHBOARD_STUDENT_SCORES,
formatBandScore,
type StudentScoreRadial,
} from "@/lib/mock/dashboard"
import {
Card, CardContent, CardDescription, CardHeader, CardTitle,
} from "@/components/ui/card"
import {
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
chartTooltipKeyboardSyncProps,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart"
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select"
import { Button } from "@/components/ui/button"
import { AskLeoButton } from "@/components/ask-leo-button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { isEditableTarget } from "@/lib/editable-target"
import { chartLineStrokeDash } from "@/lib/chart-line-dash"
import { rafThrottle } from "@/lib/raf-throttle"
import { cn } from "@/lib/utils"
import { metricTrendTone, type MetricTrendPolarity } from "@/components/key-metrics"
/** Recharts passes `index` into Line `dot` renderers; keep the callback typed against the v3 dot contract. */
type LineDotRenderProps = DotItemDotProps & { index?: number }
const activeIndexProps = (activeIndex: number | null) =>
activeIndex == null ? {} : ({ activeIndex } as Record)
type MiniMetric = {
label: string
value: string
trend?: "up" | "down" | "neutral"
/** Same semantics as `MetricItem.trendPolarity` on `KeyMetrics`. */
trendPolarity?: MetricTrendPolarity
}
/* ── Colour tokens ────────────────────────────────────────────────────────── */
const BRAND = "var(--brand-color)"
const CHART_1 = "var(--color-chart-1)"
const CHART_2 = "var(--color-chart-2)"
const CHART_3 = "var(--color-chart-3)"
const CHART_4 = "var(--color-chart-4)"
const CHART_5 = "var(--color-chart-5)"
const SUCCESS = "var(--chart-2)"
const WARNING = "var(--chart-4)"
const DESTRUCTIVE = "var(--destructive)"
/* ── Period filter options (reused across selector cards) ─────────────────── */
const PERIOD_OPTIONS = [
{ value: "7d", label: "Last 7 days" },
{ value: "30d", label: "Last 30 days" },
{ value: "90d", label: "Last quarter" },
{ value: "1y", label: "Last year" },
]
const PROGRAM_OPTIONS = [
{ value: "all", label: "All programs" },
{ value: "nursing", label: "Nursing" },
{ value: "pt", label: "PT" },
{ value: "ot", label: "OT" },
{ value: "pharmacy", label: "Pharmacy" },
]
/* ════════════════════════════════════════════════════════════════════════════
REUSABLE ChartCard — supports 3 variants
════════════════════════════════════════════════════════════════════════════ */
export type ChartCardVariant = "normal" | "tabs" | "selector" | "metrics-tabs" | "kpi-chart"
/** ChartCard tabs no longer force `text-xs` — use default `text-sm` scale and ≥24px hit area. */
const chartCardTabTriggerClass = "min-h-9 px-3 py-2 text-sm gap-2"
import {
LeoInsightIndicator,
LEO_TOKENS,
type ChartLeoInsight,
type ChartLeoInsightAnchor,
type ChartLeoInsightKind,
} from "@/components/leo-insight-indicator"
export type { ChartLeoInsight, ChartLeoInsightAnchor, ChartLeoInsightKind }
type ChartLeoInsightBundle = { insight: ChartLeoInsight; chartTitle: string }
const ChartLeoInsightContext = React.createContext(null)
function resolveChartLeoAnchorY(
row: Record,
xDataKey: string,
anchor: ChartLeoInsightAnchor,
): number | null {
if (typeof anchor.yValue === "number" && !Number.isNaN(anchor.yValue)) {
return anchor.yValue
}
const keys =
anchor.yDataKeys?.filter((k) => k !== xDataKey) ??
Object.keys(row).filter((k) => k !== xDataKey)
const nums = keys
.map((k) => row[k])
.filter((v): v is number => typeof v === "number" && !Number.isNaN(v))
if (nums.length === 0) return null
const combine = anchor.yCombine ?? "max"
return combine === "sum" ? nums.reduce((a, b) => a + b, 0) : Math.max(...nums)
}
function chartLeoNumericDomainMax(
data: ReadonlyArray>,
xDataKey: string,
): number {
let m = 0
for (const row of data) {
for (const [k, v] of Object.entries(row)) {
if (k === xDataKey) continue
if (typeof v === "number" && !Number.isNaN(v) && v > m) m = v
}
}
return m > 0 ? m : 1
}
/**
* Static brand-coloured dot drawn on the exact data point Leo is calling out.
* A card-coloured knockout ring keeps it readable on top of grid lines and
* area fills. No pulsing animation — the dashed connector line + chip do the
* attention work, and this keeps the chart calm.
*/
function LeoPlotPointDot() {
return (
)
}
/**
* Read the Recharts SVG rendered by a sibling `ChartContainer` and project
* the insight anchor's `(xValue, yNum)` into pixel coordinates relative to
* the overlay's wrapper.
*
* Strategy — chart-type-agnostic:
* 1. Find the `` inside the parent (`.relative` wrapper).
* 2. Plot rect = bounding box of `.recharts-cartesian-grid`.
* Fallback = area between y-axis right edge and x-axis top edge.
* 3. X position = matching x-axis tick's centre, matched by text content.
* Fallback = `(idx + 0.5) / n * plotWidth` band formula.
* 4. Y position = interpolated from y-axis tick values (handles non-zero
* domain bases automatically — e.g. recharts auto-domains that start at
* non-zero and charts with cropped y-ranges). Fallback = `1 - y/yMax`.
*
* Works on: Line/Area/Bar/StackedBar/Composed charts — anything with Cartesian
* axes. Pie/Radar/Funnel charts don't expose axes, so the overlay skips with
* a null return (anchor concept isn't meaningful there).
*/
function useChartAnchorPixelPosition({
xValue,
xDataKey,
yNum,
data,
}: {
xValue: string
xDataKey: string
yNum: number
data: ReadonlyArray>
}) {
const ref = React.useRef(null)
const [pos, setPos] = React.useState<{ x: number; y: number; plotTop: number } | null>(null)
React.useEffect(() => {
const el = ref.current
if (!el) return
const parent = el.parentElement
if (!parent) return
const compute = () => {
const svg = parent.querySelector("svg") as SVGSVGElement | null
if (!svg) return
const parentRect = parent.getBoundingClientRect()
const toLocal = (el: Element) => {
const r = (el as SVGGraphicsElement).getBoundingClientRect()
return {
left: r.left - parentRect.left,
right: r.right - parentRect.left,
top: r.top - parentRect.top,
bottom: r.bottom - parentRect.top,
width: r.width,
height: r.height,
cx: r.left + r.width / 2 - parentRect.left,
cy: r.top + r.height / 2 - parentRect.top,
}
}
// Plot rect — prefer cartesian-grid; fall back to axis bounds.
const grid = svg.querySelector(".recharts-cartesian-grid")
const xAxis = svg.querySelector(".recharts-xAxis")
const yAxis = svg.querySelector(".recharts-yAxis")
if (!xAxis || !yAxis) return
const plot = grid ? toLocal(grid) : (() => {
const y = toLocal(yAxis)
const x = toLocal(xAxis)
return {
left: y.right, right: x.right, top: y.top,
bottom: x.top, width: x.right - y.right, height: x.top - y.top,
cx: 0, cy: 0,
}
})()
// X position: find matching x-tick by text content (chart-agnostic).
const xTicks = Array.from(
xAxis.querySelectorAll(".recharts-cartesian-axis-tick"),
) as SVGGElement[]
let xPx: number | null = null
for (const t of xTicks) {
if ((t.textContent ?? "").trim() === xValue) {
xPx = toLocal(t).cx
break
}
}
if (xPx === null) {
const idx = data.findIndex((d) => String(d[xDataKey]) === xValue)
if (idx < 0) return
xPx = plot.left + ((idx + 0.5) / Math.max(data.length, 1)) * plot.width
}
// Y position: interpolate from y-axis tick values (handles non-zero domains).
const yTickEls = Array.from(
yAxis.querySelectorAll(".recharts-cartesian-axis-tick"),
) as SVGGElement[]
const yTickPairs: Array<{ v: number; y: number }> = []
for (const t of yTickEls) {
const raw = (t.textContent ?? "").trim()
if (!raw) continue
const v = parseFloat(raw.replace(/[^0-9.\-]/g, ""))
if (Number.isNaN(v)) continue
yTickPairs.push({ v, y: toLocal(t).cy })
}
let yPx: number | null = null
if (yTickPairs.length >= 2) {
const sorted = [...yTickPairs].toSorted((a, b) => a.v - b.v)
const lo = sorted[0], hi = sorted[sorted.length - 1]
if (hi.v !== lo.v) {
yPx = lo.y + ((yNum - lo.v) / (hi.v - lo.v)) * (hi.y - lo.y)
}
}
if (yPx === null) {
// Conservative fallback when y-axis ticks cannot be parsed.
const yMax = chartLeoNumericDomainMax(data, xDataKey)
yPx = plot.top + (1 - yNum / yMax) * plot.height
}
// Equality-guard: ResizeObserver + MutationObserver fire on every frame
// of the sidebar's 200ms width transition (and on every Recharts attribute
// mutation during its own enter animation). Without this guard, each
// sub-pixel shift triggers a React re-render of the overlay subtree —
// multiplied by every chart on the page that's worth dozens of renders
// per frame on a wide dashboard. 0.5px tolerance is well below any
// visible misalignment of the dashed-line overlay.
setPos((prev) => {
if (
prev !== null &&
Math.abs(prev.x - xPx) < 0.5 &&
Math.abs(prev.y - (yPx as number)) < 0.5 &&
Math.abs(prev.plotTop - plot.top) < 0.5
) {
return prev
}
return { x: xPx, y: yPx as number, plotTop: plot.top }
})
}
// Recharts mounts/animates after our first paint; measure a few times.
compute()
let raf1 = requestAnimationFrame(() => {
compute()
raf1 = requestAnimationFrame(compute)
})
// rAF-coalesce: ResizeObserver + MutationObserver can each fire many times
// per frame during the sidebar's `transition-[width]` (200ms ease-linear)
// and during Recharts' own enter animation. Without throttling, `compute`
// walks the SVG + parses tick text on every fire — which on the dashboard
// (multiple chart cards × per-chart insight overlays) saturates the main
// thread and makes the sidebar collapse/expand feel sluggish. One sample
// per frame is plenty for sub-pixel anchor positioning. Same pattern as
// `apps/web/hooks/use-sidebar-reflow-zoom.ts`.
const scheduled = rafThrottle(compute)
const ro = new ResizeObserver(scheduled)
ro.observe(parent)
const mo = new MutationObserver(scheduled)
mo.observe(parent, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["width", "height", "transform", "d", "x", "y"],
})
return () => {
cancelAnimationFrame(raf1)
scheduled.cancel()
ro.disconnect()
mo.disconnect()
}
}, [xValue, xDataKey, yNum, data])
return { ref, pos }
}
/**
* HTML overlay on the chart plot (sibling of `ChartContainer`, inside a `relative` wrapper).
*
* Visual structure, top → bottom:
* 1. Chip (`LeoInsightIndicator` in plot-marker layout) — floats above
* 2. Dashed connector line in the kind colour joining chip to dot
* 3. Pulsing dot anchored on the real data point
*
* Positioning is measured from the Recharts SVG at runtime (see
* `useChartAnchorPixelPosition`) so the dot lands on the actual data point
* regardless of chart type, y-domain, or plot margin.
*/
export function ChartLeoPlotInsightOverlay({
data,
xDataKey,
markerLiftPx = 44,
}: {
data: ReadonlyArray>
xDataKey: string
/** @deprecated retained for call-site compatibility. */
insetPct?: { left: number; right: number; top: number; bottom: number }
/** @deprecated retained for call-site compatibility. */
xAxisLabelReservePct?: number
/** @deprecated retained for call-site compatibility. */
markerLiftPct?: number
/** @deprecated retained for call-site compatibility. */
markerLiftExtraPx?: number
/** Vertical distance from dot to the bottom of the floating chip, in px. */
markerLiftPx?: number
}) {
// Lift the chip well clear of the default Recharts tooltip so they never
// fight for the same cursor area on hover.
const effectiveLift = markerLiftPx ?? 56
const bundle = React.useContext(ChartLeoInsightContext)
const anchor = bundle?.insight.anchor
const idx = anchor
? data.findIndex((d) => String(d[xDataKey]) === anchor.xValue)
: -1
const row = idx >= 0 ? (data[idx] as Record) : null
const yNum = row && anchor ? resolveChartLeoAnchorY(row, xDataKey, anchor) : null
// NOTE: Hook must always run (React rules). Pass safe defaults when not ready.
const { ref, pos } = useChartAnchorPixelPosition({
xValue: anchor?.xValue ?? "",
xDataKey,
yNum: yNum ?? 0,
data,
})
if (!bundle || !anchor || idx < 0 || yNum === null || Number.isNaN(yNum)) return null
// Clamp the chip so it never renders above the plot rect.
const chipBottomY = pos
? Math.max((pos.plotTop ?? 0) + 28, pos.y - effectiveLift)
: 0
return (
{pos && (
<>
{/* Dashed connector — chip bottom → ~7px above the dot, brand-coloured. */}
{/* Static brand dot anchored on the real data point */}
{/* Chip trigger — bottom edge meets the top of the dashed connector */}
>
)}
)
}
/** Supplies Leo insight to chart bodies; optional corner control when there is no plot anchor. */
function ChartLeoInsightOverlay({
leoInsight,
chartTitle,
children,
}: {
leoInsight?: ChartLeoInsight | null
chartTitle: string
children: React.ReactNode
}) {
const contextValue = React.useMemo(
() => (leoInsight ? { insight: leoInsight, chartTitle } : null),
[leoInsight, chartTitle],
)
if (!leoInsight || !contextValue) return <>{children}>
const showCorner = !leoInsight.anchor
return (
{showCorner ? (
) : (
children
)}
)
}
/** Screen-reader data fallback for charts — shared with list-page dashboards. */
export function ChartDataTable({
caption,
headers,
rows,
}: {
caption: string
headers: string[]
rows: (string | number)[][]
}) {
return (
{caption}
{headers.map((h) => {h} )}
{rows.map((row, i) => (
{row.map((cell, j) => {cell} )}
))}
)
}
/**
* Keyboard-focusable chart region (arrow keys, Escape) + live announcement when a point is selected.
* Shared by the `/dashboard` gallery and **Data** view dashboards (Placements / Team / Compliance): same
* interaction model; visual differences come from `ChartCard` chrome and per-chart renderers (bar vs pie),
* not from a separate chart implementation.
*/
export function ChartFigure({
label,
summary,
dataLength,
leoInsight,
children,
}: {
label: string
summary: string
dataLength: number
/** Optional Ask-Leo insight context for chart bodies (same as `ChartCard`). */
leoInsight?: ChartLeoInsight | null
children: (activeIndex: number | null) => React.ReactNode
}) {
const [activeIndex, setActiveIndex] = React.useState(null)
const ref = React.useRef(null)
const prevActiveIndexRef = React.useRef(null)
React.useEffect(() => {
const prev = prevActiveIndexRef.current
prevActiveIndexRef.current = activeIndex
if (prev === null || activeIndex !== null) return
const wrapper = ref.current?.querySelector(".recharts-wrapper")
if (!wrapper) return
wrapper.dispatchEvent(
new MouseEvent("mouseleave", { bubbles: true, cancelable: true }),
)
}, [activeIndex])
const navigateKeys = React.useCallback(
(e: React.KeyboardEvent) => {
if (!dataLength) return
if (isEditableTarget(e.target)) return
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
e.preventDefault()
e.stopPropagation()
setActiveIndex((i) => (i === null ? 0 : Math.min(i + 1, dataLength - 1)))
break
case "ArrowLeft":
case "ArrowUp":
e.preventDefault()
e.stopPropagation()
setActiveIndex((i) => (i === null ? dataLength - 1 : Math.max(i - 1, 0)))
break
case "Escape":
e.preventDefault()
e.stopPropagation()
setActiveIndex(null)
ref.current?.blur()
break
default:
break
}
},
[dataLength],
)
/** Clicks on Recharts SVG do not focus this node — focus so Arrow keys work without extra Tab stops. */
function handlePointerDownCapture(e: React.PointerEvent) {
if (!dataLength) return
const root = ref.current
if (!root?.contains(e.target as Node)) return
const el = e.target as HTMLElement | null
if (el?.closest?.("button, a, [role='tab'], [role='option'], input, select, textarea, [contenteditable='true']"))
return
queueMicrotask(() => root.focus())
}
return (
{
if (!ref.current?.contains(e.target as Node)) return
if (isEditableTarget(e.target)) return
if (
e.key === "ArrowRight" ||
e.key === "ArrowDown" ||
e.key === "ArrowLeft" ||
e.key === "ArrowUp" ||
e.key === "Escape"
) {
navigateKeys(e)
}
}}
onPointerDownCapture={handlePointerDownCapture}
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"
>
{children(activeIndex)}
{activeIndex !== null && (
Data point {activeIndex + 1} of {dataLength} selected
)}
)
}
function ChartCardHeader({
title,
description,
variant,
filterOptions,
filter,
onFilter,
}: {
title: string
description: string
variant: ChartCardVariant
filterOptions?: { value: string; label: string }[]
filter?: string
onFilter?: (v: string) => void
}) {
const isSelector = variant === "selector" && Array.isArray(filterOptions) && filterOptions.length > 0
return (
{title}
{description}
{/* Reveal on card hover/focus — pointer-events guarded so the hidden button is not reachable */}
{isSelector && filterOptions && onFilter && (
onFilter(v)}>
{filterOptions.map((opt) => (
{opt.label}
))}
)}
)
}
export function ChartCard({
title,
description,
children,
className = "",
variant = "normal",
trendContent,
filterOptions,
defaultFilter,
onFilterChange,
miniMetrics,
tabOptions,
leoInsight,
}: {
title: string
description: string
children: React.ReactNode | ((filter: string) => React.ReactNode)
className?: string
variant?: ChartCardVariant
/** "tabs" / "metrics-tabs" variant: content shown in the "Trend" tab */
trendContent?: React.ReactNode
/** "selector" variant: options for the filter dropdown */
filterOptions?: { value: string; label: string }[]
defaultFilter?: string
onFilterChange?: (value: string) => void
/** "metrics-tabs" variant: compact KPI strip shown above the chart */
miniMetrics?: MiniMetric[]
/** "tabs" variant: override the default Chart/Trend tabs with custom options.
* The selected value is passed to the children function. */
tabOptions?: { value: string; label: string }[]
/**
* Smart Leo summary: opens a popover + Ask Leo CTA.
* With `anchor`, mount `ChartLeoPlotInsightOverlay` beside `ChartContainer` for on-plot guide + marker; otherwise a corner Insight control is shown.
*/
leoInsight?: ChartLeoInsight | null
}) {
const [filter, setFilter] = React.useState(
() => defaultFilter || filterOptions?.[0]?.value || miniMetrics?.[0]?.label || tabOptions?.[0]?.value || ""
)
// Sync when defaultFilter or first miniMetric changes (React may reuse across ternary branches)
React.useEffect(() => {
const next = defaultFilter || filterOptions?.[0]?.value || miniMetrics?.[0]?.label
if (next) setFilter(next)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultFilter, miniMetrics?.[0]?.label])
const handleFilter = (v: string) => { setFilter(v); onFilterChange?.(v) }
const resolvedChildren =
typeof children === "function" ? children(filter) : children
/* ── Default Chart / Trend tabs (no custom tabOptions) ───────────────────── */
const defaultTabsBlock = (
Chart
Trend
{resolvedChildren}
{trendContent ?? resolvedChildren}
)
if (variant === "tabs") {
/* Custom tab labels (e.g. period picker for key metrics) */
if (tabOptions && tabOptions.length > 0) {
const selectedTab = filter || tabOptions[0].value
return (
{tabOptions.map((tab) => (
{tab.label}
))}
{tabOptions.map((tab) => (
{typeof children === "function" ? children(tab.value) : children}
))}
)
}
return (
{defaultTabsBlock}
)
}
if (variant === "metrics-tabs") {
const metrics = miniMetrics && miniMetrics.length > 0 ? miniMetrics : null
const selectedMetric = filter || metrics?.[0]?.label || ""
return (
{metrics ? (
/* Metrics ARE the tabs — each metric cell is a clickable TabsTrigger */
{metrics.map((m) => {
const isUp = m.trend === "up"
const isDown = m.trend === "down"
const tone = metricTrendTone(m.trend ?? "neutral", m.trendPolarity)
const upClass =
tone === "positive"
? "text-emerald-600"
: tone === "negative"
? "text-destructive"
: "text-muted-foreground"
const downClass =
tone === "positive"
? "text-emerald-600"
: tone === "negative"
? "text-destructive"
: "text-muted-foreground"
return (
{m.label}
{m.value}
{isUp && }
{isDown && }
)
})}
{/* All metric tabs show the same chart — tab selection is a context indicator */}
{metrics.map((m) => (
{resolvedChildren}
))}
) : (
defaultTabsBlock
)}
)
}
/* ── kpi-chart: prominent metric on top, chart below ─────────────────────── */
if (variant === "kpi-chart") {
const kpi = miniMetrics?.[0]
const isUp = kpi?.trend === "up"
const isDown = kpi?.trend === "down"
const tone = metricTrendTone(kpi?.trend ?? "neutral", kpi?.trendPolarity)
const trendClass =
tone === "positive"
? "text-emerald-600"
: tone === "negative"
? "text-destructive"
: "text-muted-foreground"
return (
{kpi && (
{kpi.value}
{isUp && (
trending up
)}
{isDown && (
trending down
)}
{kpi.label}
)}
{resolvedChildren}
)
}
return (
{resolvedChildren}
)
}
/* ════════════════════════════════════════════════════════════════════════════
DATA & CHART COMPONENTS
════════════════════════════════════════════════════════════════════════════ */
/* ── Area ─────────────────────────────────────────────────────────────────── */
const areaCfg: ChartConfig = {
placements: { label: "Placements", color: BRAND },
applications: { label: "Applications", color: CHART_2 },
reviews: { label: "Reviews", color: CHART_4 },
}
const areaData = [
{ month: "Aug", placements: 42, applications: 78, reviews: 31 },
{ month: "Sep", placements: 58, applications: 91, reviews: 44 },
{ month: "Oct", placements: 53, applications: 85, reviews: 39 },
{ month: "Nov", placements: 67, applications: 102, reviews: 52 },
{ month: "Dec", placements: 49, applications: 76, reviews: 37 },
{ month: "Jan", placements: 74, applications: 118, reviews: 60 },
{ month: "Feb", placements: 81, applications: 124, reviews: 68 },
{ month: "Mar", placements: 89, applications: 137, reviews: 72 },
]
function AreaChartContent() {
return (
{(activeIndex) => (
<>
[d.month, d.placements, d.applications, d.reviews])} />
>
)}
)
}
function AreaLineTrendContent() {
return (
{(activeIndex) => (
<>
} />
} />
props.index === activeIndex ? : } />
props.index === activeIndex ? : } />
props.index === activeIndex ? : } />
[d.month, d.placements, d.applications, d.reviews])} />
>
)}
)
}
/* Selector variant — filter data by period */
const areaDataByPeriod: Record = {
"7d": areaData.slice(-2),
"30d": areaData.slice(-4),
"90d": areaData.slice(-6),
"1y": areaData,
}
function AreaSelectorContent({ filter }: { filter: string }) {
const data = areaDataByPeriod[filter] ?? areaData
return (
{(activeIndex) => (
<>
[d.month, d.placements, d.applications, d.reviews])} />
>
)}
)
}
/* ── Donut ─────────────────────────────────────────────────────────────────── */
const donutCfg: ChartConfig = {
confirmed: { label: "Confirmed", color: SUCCESS },
pending: { label: "Pending", color: WARNING },
rejected: { label: "Rejected", color: DESTRUCTIVE },
review: { label: "In Review", color: CHART_1 },
}
const donutDataAll = [
{ name: "confirmed", value: 58, fill: SUCCESS },
{ name: "pending", value: 24, fill: WARNING },
{ name: "rejected", value: 9, fill: DESTRUCTIVE },
{ name: "review", value: 9, fill: CHART_1 },
]
function DonutChartContent({ data = donutDataAll }: { data?: typeof donutDataAll }) {
const total = data.reduce((s, d) => s + d.value, 0)
return (
{(activeIndex) => (
<>
} />
{data.map((d) => | )}
{data.map((d) => (
{donutCfg[d.name]?.label}
{Math.round(d.value / total * 100)}%
))}
{
const raw = donutCfg[d.name]?.label ?? d.name
const label =
typeof raw === "string" || typeof raw === "number" ? String(raw) : String(d.name)
return [label, d.value] as [string, number]
})}
/>
>
)}
)
}
const donutBarTrendData = [
{ month: "Jan", confirmed: 52, pending: 20, rejected: 7 },
{ month: "Feb", confirmed: 60, pending: 18, rejected: 6 },
{ month: "Mar", confirmed: 68, pending: 24, rejected: 9 },
]
/* Donut trend — bar chart version */
function DonutBarTrendContent() {
return (
} />
} />
)
}
/* Donut — selector by program */
const donutByProgram: Record = {
all: donutDataAll,
nursing: [{ name: "confirmed", value: 72, fill: SUCCESS }, { name: "pending", value: 18, fill: WARNING }, { name: "rejected", value: 5, fill: DESTRUCTIVE }, { name: "review", value: 5, fill: CHART_1 }],
pt: [{ name: "confirmed", value: 55, fill: SUCCESS }, { name: "pending", value: 28, fill: WARNING }, { name: "rejected", value: 10, fill: DESTRUCTIVE }, { name: "review", value: 7, fill: CHART_1 }],
ot: [{ name: "confirmed", value: 48, fill: SUCCESS }, { name: "pending", value: 30, fill: WARNING }, { name: "rejected", value: 14, fill: DESTRUCTIVE }, { name: "review", value: 8, fill: CHART_1 }],
pharmacy: [{ name: "confirmed", value: 40, fill: SUCCESS }, { name: "pending", value: 35, fill: WARNING }, { name: "rejected", value: 15, fill: DESTRUCTIVE }, { name: "review", value: 10, fill: CHART_1 }],
}
/* ── Grouped Bar ─────────────────────────────────────────────────────────── */
const barCfg: ChartConfig = {
new: { label: "New", color: BRAND },
returned: { label: "Returned", color: CHART_2 },
}
const barData = [
{ program: "Nursing", new: 34, returned: 22 },
{ program: "PT", new: 28, returned: 18 },
{ program: "OT", new: 21, returned: 14 },
{ program: "SW", new: 19, returned: 11 },
{ program: "Pharm", new: 15, returned: 9 },
{ program: "Rad", new: 12, returned: 7 },
]
function GroupedBarContent() {
return (
{(activeIndex) => (
<>
} />
} />
[d.program, d.new, d.returned])} />
>
)}
)
}
function GroupedBarLineTrend() {
return (
} />
} />
)
}
/* ── Stacked Bar ─────────────────────────────────────────────────────────── */
const stackCfg: ChartConfig = {
approved: { label: "Approved", color: SUCCESS },
pending: { label: "Pending", color: WARNING },
rejected: { label: "Rejected", color: DESTRUCTIVE },
}
const stackData = [
{ month: "Oct", approved: 38, pending: 12, rejected: 4 },
{ month: "Nov", approved: 44, pending: 15, rejected: 6 },
{ month: "Dec", approved: 31, pending: 8, rejected: 3 },
{ month: "Jan", approved: 52, pending: 18, rejected: 7 },
{ month: "Feb", approved: 60, pending: 14, rejected: 5 },
{ month: "Mar", approved: 68, pending: 20, rejected: 8 },
]
function StackedBarContent() {
return (
{(activeIndex) => (
<>
} />
} />
[d.month, d.approved, d.pending, d.rejected])} />
>
)}
)
}
function StackedBarLineTrend() {
return (
} />
} />
)
}
/* ── Line ─────────────────────────────────────────────────────────────────── */
const lineCfg: ChartConfig = {
logins: { label: "Logins", color: BRAND },
submissions: { label: "Submissions", color: CHART_2 },
evaluations: { label: "Evaluations", color: CHART_4 },
}
const lineData = [
{ week: "W1", logins: 148, submissions: 42, evaluations: 29 },
{ week: "W2", logins: 162, submissions: 51, evaluations: 35 },
{ week: "W3", logins: 139, submissions: 38, evaluations: 27 },
{ week: "W4", logins: 175, submissions: 63, evaluations: 48 },
{ week: "W5", logins: 182, submissions: 69, evaluations: 52 },
{ week: "W6", logins: 196, submissions: 75, evaluations: 58 },
{ week: "W7", logins: 211, submissions: 82, evaluations: 63 },
{ week: "W8", logins: 204, submissions: 78, evaluations: 60 },
]
const lineDataByPeriod: Record = {
"7d": lineData.slice(-2),
"30d": lineData.slice(-4),
"90d": lineData.slice(-6),
"1y": lineData,
}
function LineChartContent({ data = lineData }: { data?: typeof lineData }) {
return (
{(activeIndex) => (
<>
} />
} />
props.index === activeIndex ? : } />
props.index === activeIndex ? : } />
props.index === activeIndex ? : } />
[d.week, d.logins, d.submissions, d.evaluations])} />
>
)}
)
}
function LineAreaTrend() {
return (
} />
} />
)
}
/* ── Radial Bar ──────────────────────────────────────────────────────────── */
const radialCfg: ChartConfig = {
nursing: { label: "Nursing", color: BRAND },
pt: { label: "PT", color: CHART_2 },
ot: { label: "OT", color: SUCCESS },
pharmacy: { label: "Pharmacy", color: WARNING },
social: { label: "Social Work", color: CHART_4 },
}
const radialData = [
{ name: "nursing", score: 98, fill: BRAND },
{ name: "pt", score: 94, fill: CHART_2 },
{ name: "ot", score: 91, fill: SUCCESS },
{ name: "pharmacy", score: 87, fill: WARNING },
{ name: "social", score: 82, fill: CHART_4 },
]
function RadialBarContent({ data = radialData }: { data?: typeof radialData }) {
return (
{(activeIndex) => (
<>
} />
{data.map((d) => | )}
{data.map((d) => (
{radialCfg[d.name]?.label}
{d.score}%
))}
{
const raw = radialCfg[d.name]?.label ?? d.name
const label =
typeof raw === "string" || typeof raw === "number" ? String(raw) : String(d.name)
return [label, `${d.score}%`] as [string, string]
})}
/>
>
)}
)
}
const radialLineTrendCfg: ChartConfig = {
score: { label: "Current", color: BRAND },
prev: { label: "Previous", color: CHART_2 },
}
function RadialLineTrend() {
const data = radialData.map((d, i) => ({
name: d.name,
score: d.score,
prev: d.score - [4, 7, 2, 9, 5][i],
}))
return (
} />
} />
)
}
/** Quota radial — ChartFigure, keyboard tooltip sync, sr-only table (same pattern as RadialBarContent). */
function QuotaRadialGalleryContent({ radial }: { radial: StudentScoreRadial }) {
const summary =
`Radial gauge for ${radial.title}. Student score ${formatBandScore(radial.studentScore)}. Class average ${formatBandScore(radial.classAverage)}. Scale ${formatBandScore(radial.scaleMin)} to ${formatBandScore(radial.scaleMax)}. ${radial.caption}.`
return (
{(activeIndex) => (
<>
Class avg{" "}
{formatBandScore(radial.classAverage)}
{" "}
· scale {formatBandScore(radial.scaleMin)}–{formatBandScore(radial.scaleMax)}
>
)}
)
}
/* ── Horizontal Bar ─────────────────────────────────────────────────────── */
const hBarCfg: ChartConfig = {
placements: { label: "Placements", color: BRAND },
}
const hBarData = [
{ site: "City Med", placements: 42 },
{ site: "Westside Hosp", placements: 37 },
{ site: "North Clinic", placements: 31 },
{ site: "Bay Health", placements: 28 },
{ site: "Eastview", placements: 22 },
{ site: "Lakeshore", placements: 18 },
{ site: "Pinehill", placements: 14 },
]
const hBarByPeriod: Record = {
"7d": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 0.35) })),
"30d": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 0.6) })),
"90d": hBarData,
"1y": hBarData.map((d) => ({ ...d, placements: Math.round(d.placements * 4.2) })),
}
function HorizontalBarContent({ data = hBarData }: { data?: typeof hBarData }) {
return (
{(activeIndex) => (
<>
} />
[d.site, d.placements])} />
>
)}
)
}
function HBarLineTrend() {
return (
} />
)
}
/* ── Composed ─────────────────────────────────────────────────────────────── */
const composedCfg: ChartConfig = {
placements: { label: "Placements", color: BRAND },
capacity: { label: "Capacity", color: CHART_3 },
rate: { label: "Fill Rate %", color: CHART_4 },
}
const composedData = [
{ month: "Sep", placements: 44, capacity: 60, rate: 73 },
{ month: "Oct", placements: 53, capacity: 65, rate: 82 },
{ month: "Nov", placements: 67, capacity: 80, rate: 84 },
{ month: "Dec", placements: 49, capacity: 70, rate: 70 },
{ month: "Jan", placements: 74, capacity: 85, rate: 87 },
{ month: "Feb", placements: 81, capacity: 90, rate: 90 },
{ month: "Mar", placements: 89, capacity: 95, rate: 94 },
]
function ComposedChartContent() {
return (
{(activeIndex) => (
<>
} />
} />
props.index === activeIndex ? : } type="monotone" />
[d.month, d.placements, d.capacity, `${d.rate}%`])} />
>
)}
)
}
function ComposedLineTrend() {
return (
} />
} />
)
}
/* ── Radar ───────────────────────────────────────────────────────────────── */
const radarCfg: ChartConfig = {
nursing: { label: "Nursing", color: BRAND },
physical: { label: "PT/OT", color: CHART_2 },
}
const radarData = [
{ skill: "Clinical", nursing: 92, physical: 88 },
{ skill: "Comm.", nursing: 85, physical: 79 },
{ skill: "Critical", nursing: 78, physical: 84 },
{ skill: "Teamwork", nursing: 91, physical: 90 },
{ skill: "Ethics", nursing: 96, physical: 93 },
{ skill: "Technical", nursing: 80, physical: 87 },
]
function RadarChartContent() {
return (
{(activeIndex) => (
<>
} />
} />
[d.skill, d.nursing, d.physical])} />
>
)}
)
}
function RadarBarTrend() {
return (
} />
} />
)
}
/* ── Scatter ─────────────────────────────────────────────────────────────── */
const scatterCfg: ChartConfig = {
nursing: { label: "Nursing", color: BRAND },
pt: { label: "PT", color: CHART_2 },
ot: { label: "OT", color: SUCCESS },
pharmacy: { label: "Pharmacy", color: WARNING },
}
const scatterNursing = [{ x: 80, y: 94, z: 42 }, { x: 65, y: 88, z: 35 }, { x: 55, y: 78, z: 28 }, { x: 90, y: 97, z: 51 }, { x: 70, y: 91, z: 38 }]
const scatterPT = [{ x: 40, y: 85, z: 22 }, { x: 50, y: 90, z: 27 }, { x: 35, y: 80, z: 18 }, { x: 60, y: 93, z: 31 }]
const scatterOT = [{ x: 30, y: 88, z: 16 }, { x: 45, y: 92, z: 24 }, { x: 38, y: 84, z: 19 }]
const scatterPharmacy = [{ x: 25, y: 76, z: 12 }, { x: 35, y: 82, z: 17 }, { x: 20, y: 71, z: 9 }]
function ScatterChartContent() {
return (
} />
} />
)
}
const scatterLineTrendCfg: ChartConfig = {
nursing: { label: "Nursing", color: BRAND },
pt: { label: "PT", color: CHART_2 },
}
const scatterLineTrendData = [
{ month: "Oct", nursing: 88, pt: 80 },
{ month: "Nov", nursing: 91, pt: 82 },
{ month: "Dec", nursing: 89, pt: 79 },
{ month: "Jan", nursing: 93, pt: 84 },
{ month: "Feb", nursing: 95, pt: 87 },
{ month: "Mar", nursing: 94, pt: 85 },
]
function ScatterLineTrend() {
return (
} />
} />
)
}
/* ── Funnel ──────────────────────────────────────────────────────────────── */
const funnelCfg: ChartConfig = {
applied: { label: "Applied", color: BRAND },
screened: { label: "Screened", color: CHART_2 },
matched: { label: "Matched", color: SUCCESS },
placed: { label: "Placed", color: CHART_4 },
completed: { label: "Completed", color: CHART_5 },
}
const funnelData = [
{ name: "Applied", value: 320, fill: BRAND },
{ name: "Screened", value: 240, fill: CHART_2 },
{ name: "Matched", value: 175, fill: SUCCESS },
{ name: "Placed", value: 128, fill: CHART_4 },
{ name: "Completed", value: 98, fill: CHART_5 },
]
const funnelDataByPeriod: Record = {
"7d": funnelData.map((d) => ({ ...d, value: Math.round(d.value * 0.08) })),
"30d": funnelData.map((d) => ({ ...d, value: Math.round(d.value * 0.3) })),
"90d": funnelData,
"1y": funnelData.map((d) => ({ ...d, value: d.value * 4 })),
}
function FunnelChartContent({ data = funnelData }: { data?: typeof funnelData }) {
const summary = `Funnel with ${data.length} stages from ${data[0]?.name ?? ""} to ${data[data.length - 1]?.name ?? ""}.`
return (
{(activeIndex) => (
<>
} />
{data.map((d, i) => (
|
))}
[d.name, d.value])} />
>
)}
)
}
const funnelLineTrendCfg: ChartConfig = {
applied: { label: "Applied", color: BRAND },
placed: { label: "Placed", color: CHART_4 },
completed: { label: "Completed", color: CHART_5 },
}
const funnelLineTrendData = [
{ month: "Oct", applied: 210, placed: 95, completed: 68 },
{ month: "Nov", applied: 245, placed: 108, completed: 82 },
{ month: "Dec", applied: 180, placed: 88, completed: 64 },
{ month: "Jan", applied: 280, placed: 120, completed: 91 },
{ month: "Feb", applied: 300, placed: 124, completed: 95 },
{ month: "Mar", applied: 320, placed: 128, completed: 98 },
]
function FunnelLineTrend() {
return (
} />
} />
)
}
/* ════════════════════════════════════════════════════════════════════════════
Chart rows — shared across variants
════════════════════════════════════════════════════════════════════════════ */
const CHART_GALLERY_LEO_DONUT: ChartLeoInsight = {
headline: "Confirmed placements dominate the current pipeline",
explanation:
"87% of placements are already confirmed, with only 9% pending and 4% in review. This is a healthy distribution suggesting strong conversion from applications to confirmed offers.",
kind: "spike",
delta: { value: "+12%", label: "vs. last month" },
bullets: [
"Confirmed count has grown steadily across nursing, PT, and OT programs.",
"Rejection rate remains low at 1% — applications are well-qualified.",
],
}
const CHART_GALLERY_LEO_APPLICATIONS: ChartLeoInsight = {
headline: "Nursing program leads application volume",
explanation:
"Nursing consistently attracts the most new applicants, with 34 this period. PT and OT follow closely. Returned applications suggest strong re-engagement.",
kind: "trend",
delta: { value: "+8%", label: "new vs. prior period" },
bullets: [
"Nursing: 34 new, 22 returned — highest volume and strong re-engagement.",
"PT and OT: steady demand — balanced load across clinical programs.",
],
}
const CHART_GALLERY_LEO_LINE: ChartLeoInsight = {
headline: "Portal activity peaks mid-week",
explanation:
"Login, submission, and evaluation activity cluster around Tuesday–Thursday, with weekends showing predictable dips. This pattern is consistent and expected for an academic schedule.",
kind: "trend",
delta: { value: "—", label: "stable pattern" },
bullets: [
"Logins peak at ~450 on Wednesdays.",
"Submissions highest Monday–Friday, near-zero on weekends.",
],
}
const CHART_GALLERY_LEO_COMPLIANCE: ChartLeoInsight = {
headline: "PT/OT programs lead compliance scoring",
explanation:
"PT and OT average 88–89% compliance, outpacing Nursing (82%) and Pharmacy (76%). Radiology lags at 71% — may need targeted support.",
kind: "dip",
delta: { value: "-8%", label: "Radiology vs. PT/OT" },
bullets: [
"PT/OT: consistent excellence across all 6 dimensions.",
"Pharmacy: scoring gaps in documentation and timeliness.",
"Radiology: needs support in scheduling and follow-up processes.",
],
}
const CHART_GALLERY_LEO_HORIZONTAL: ChartLeoInsight = {
headline: "Large clinical sites carry most placements",
explanation:
"The three largest sites (University Hospital, Metro Clinic, Regional Center) account for 58% of all placements. Mid-size sites are under-utilized.",
kind: "anomaly",
delta: { value: "+22%", label: "top 3 sites total" },
bullets: [
"University Hospital: 156 placements (28% of total).",
"Capacity constraints may limit placement growth at smaller sites.",
],
}
const CHART_GALLERY_LEO_COMPOSED: ChartLeoInsight = {
headline: "Site capacity is healthy; fill rates peak Q2",
explanation:
"Most sites run 85–92% capacity utilization. Fill rate (placements / capacity) averages 78%, with spring months (Feb–Mar) consistently hitting 82%+.",
kind: "spike",
delta: { value: "+6%", label: "fill rate increase" },
bullets: [
"March shows the strongest fill rate at 84%.",
"Only 2 sites are below 70% utilization — opportunity to rebalance.",
],
}
const CHART_GALLERY_LEO_RADAR: ChartLeoInsight = {
headline: "Nursing and PT/OT competencies are well-balanced",
explanation:
"Both programs score 80+ on all six dimensions. Nursing edges slightly on patient care; PT/OT lead in mobility and assessment. Ready for expanded placements.",
kind: "trend",
delta: { value: "—", label: "strong across programs" },
bullets: [
"6-dimension average: Nursing 84%, PT/OT 86%.",
"Lowest dimension: patient care (Nursing 79%) — room to develop.",
],
}
const CHART_GALLERY_LEO_SCATTER: ChartLeoInsight = {
headline: "Application-to-placement funnel is healthy",
explanation:
"Applications feed steadily into offers; offer-to-confirmation conversion hovers around 72%. A small number of dropouts from offer-to-start, typical for clinical placements.",
kind: "trend",
delta: { value: "+4%", label: "confirmation rate" },
bullets: [
"Applications → Offers: 63% convert (typical for competitive placements).",
"Offers → Confirmed: 72% accept (strong acceptance rate).",
],
}
const CHART_GALLERY_LEO_FUNNEL: ChartLeoInsight = {
headline: "Funnel shape is expected; strong at top of pipe",
explanation:
"4,200 applications narrow to 842 offers (20% funnel rate) and 604 confirmed placements (72% offer acceptance). Losses are proportional—no anomalous drops.",
kind: "trend",
delta: { value: "+8%", label: "application volume" },
bullets: [
"Application → Offer: drop-off is typical for screening.",
"Offer → Confirmed: acceptance rate of 72% is healthy.",
],
}
const CHART_GALLERY_LEO_QUOTA: ChartLeoInsight = {
headline: "Student performance tracking and cohort comparison",
explanation:
"Track individual student progress against class averages and scale benchmarks. Identify outliers above or below cohort norms.",
kind: "anomaly",
bullets: [
"Performance visualized on a consistent scale across cohorts.",
"Class average provides immediate context for comparison.",
],
}
const CHART_GALLERY_LEO_TRENDS: ChartLeoInsight = {
headline: "December dips across placements, applications, and reviews",
explanation:
"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.",
kind: "dip",
delta: { value: "-24%", label: "vs. November" },
bullets: [
"Placements are 18% below the 6-month trailing average.",
"Reviews dropped sharply in the last 2 weeks of the month.",
"Same pattern appeared in Dec '24 — seasonal signal is plausible.",
],
anchor: {
xValue: "Dec",
yDataKeys: ["placements", "applications", "reviews"],
yCombine: "max",
},
}
const CHART_GALLERY_LEO_REVIEWS: ChartLeoInsight = {
headline: "December is the low point in review throughput",
explanation:
"Totals drop before recovering — worth confirming whether fewer submissions arrived or reviewers were out. Pending and rejected slices still matter once volume returns.",
kind: "dip",
delta: { value: "-31%", label: "vs. November total" },
bullets: [
"Approved reviews fell from 68 to 47 month-over-month.",
"Pending queue grew by 9 items — backlog forming.",
"Two reviewers were OOO for most of the last two weeks.",
],
anchor: {
xValue: "Dec",
yDataKeys: ["approved", "pending", "rejected"],
yCombine: "sum",
},
}
function ChartRows({ v }: { v: ChartCardVariant }) {
const isTabs = v === "tabs"
const isSel = v === "selector"
const isMT = v === "metrics-tabs"
const isKpi = v === "kpi-chart"
return (
<>
{/* Row 1 · Area (2/3) + Donut (1/3) */}
{isSel ? (
{(f) => }
) : (
}
tabOptions={isTabs ? [
{ value: "overview", label: "Overview" },
{ value: "by-program", label: "By Program" },
{ value: "trend", label: "Trend" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Placements", value: "89", trend: "up" },
{ label: "Fill rate", value: "94%", trend: "up" },
{ label: "Avg. weeks", value: "6.2", trend: "neutral" },
] : undefined}>
{isTabs
? (tab: string) => tab === "trend" ?
:
:
}
)}
{isSel ? (
{(f) => }
) : (
}
tabOptions={isTabs ? [
{ value: "current", label: "Current Cycle" },
{ value: "previous", label: "Previous Cycle" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Placed", value: "128", trend: "up" },
{ label: "Pending", value: "23", trend: "down" },
] : undefined}>
{isTabs
? (tab: string) =>
: }
)}
{/* Row 1b · Quota suite — one ChartCard per metric + radial (ChartFigure on radial only) */}
{DASHBOARD_STUDENT_SCORES.metrics.map((m) => (
))}
{/* Row 2 · Grouped Bar + Stacked Bar */}
{isSel ? (
{() => }
) : (
}
tabOptions={isTabs ? [
{ value: "all", label: "All Students" },
{ value: "new", label: "New" },
{ value: "trend", label: "Trend" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Total", value: "320", trend: "up" },
{ label: "New", value: "78%", trend: "up" },
{ label: "Returning", value: "22%", trend: "neutral" },
] : undefined}>
{isTabs
? (tab: string) => tab === "trend" ? :
: }
)}
{isSel ? (
{() => }
) : (
}
tabOptions={isTabs ? [
{ value: "status", label: "By Status" },
{ value: "reviewer", label: "By Reviewer" },
{ value: "trend", label: "Trend" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Approved", value: "68", trend: "up" },
{ label: "Pending", value: "14", trend: "down" },
{ label: "Rejected", value: "6", trend: "neutral" },
] : undefined}>
{isTabs
? (tab: string) => tab === "trend" ? :
: }
)}
{/* Row 3 · Line (2/3) + Radial (1/3) */}
{isSel ? (
{(f) => }
) : (
}
tabOptions={isTabs ? [
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
{ value: "trend", label: "Trend" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Logins", value: "1.2k", trend: "up" },
{ label: "Submissions", value: "340", trend: "up" },
{ label: "Evals", value: "88", trend: "neutral" },
] : undefined}>
{isTabs
? (tab: string) => tab === "trend" ? :
: }
)}
{isSel ? (
{() => }
) : (
}
tabOptions={isTabs ? [
{ value: "current", label: "Current" },
{ value: "historical", label: "Historical" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Avg. score", value: "91%", trend: "up" },
{ label: "At risk", value: "3", trend: "down" },
] : undefined}>
{isTabs
? (tab: string) => tab === "historical" ? :
: }
)}
{/* Row 4 · H-Bar (1/3) + Composed (2/3) */}
{isSel ? (
{(f) => }
) : (
}
tabOptions={isTabs ? [
{ value: "by-facility", label: "By Facility" },
{ value: "by-capacity", label: "By Capacity" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Sites", value: "7", trend: "up" },
{ label: "Capacity", value: "94%", trend: "up" },
] : undefined}>
{isTabs
? () =>
: }
)}
{isSel ? (
{() => }
) : (
}
tabOptions={isTabs ? [
{ value: "overlay", label: "Overlay" },
{ value: "comparison", label: "Side by Side" },
{ value: "trend", label: "Trend" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Fill rate", value: "94%", trend: "up" },
{ label: "Capacity", value: "95", trend: "up" },
{ label: "Placed", value: "89", trend: "up" },
] : undefined}>
{isTabs
? (tab: string) => tab === "trend" ? :
: }
)}
{/* Row 5 · Radar (1/3) + Scatter (2/3) */}
{isSel ? (
{() => }
) : (
}
tabOptions={isTabs ? [
{ value: "radar", label: "Radar" },
{ value: "breakdown", label: "Breakdown" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Avg.", value: "88%", trend: "up" },
{ label: "Top", value: "Clinical", trend: "neutral" },
] : undefined}>
{isTabs
? (tab: string) => tab === "breakdown" ? :
: }
)}
{isSel ? (
{() => }
) : (
}
tabOptions={isTabs ? [
{ value: "scatter", label: "Scatter" },
{ value: "ranking", label: "Ranking" },
{ value: "trend", label: "Trend" },
] : undefined}
miniMetrics={(isMT || isKpi) ? [
{ label: "Sites", value: "12", trend: "up" },
{ label: "Avg. rate", value: "87%", trend: "up" },
{ label: "Students", value: "320", trend: "up" },
] : undefined}>
{isTabs
? (tab: string) => tab === "trend" ? :
: }
)}
{/* Row 6 · Funnel full width */}
{isSel ? (
{(f) => }
) : (
}
miniMetrics={(isMT || isKpi) ? [
{ label: "Applied", value: "320", trend: "up" },
{ label: "Placed", value: "128", trend: "up" },
{ label: "Completed", value: "98", trend: "up" },
{ label: "Drop-off", value: "69%", trend: "down" },
] : undefined}>
)}
>
)
}
/* ════════════════════════════════════════════════════════════════════════════
Main export
════════════════════════════════════════════════════════════════════════════ */
export function ChartsOverview({ variant = "normal" }: { variant?: ChartCardVariant }) {
return (
)
}