diff --git a/mockups/admissions-chartjs-check.html b/mockups/admissions-chartjs-check.html
new file mode 100644
index 0000000..860bcdb
--- /dev/null
+++ b/mockups/admissions-chartjs-check.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+Admissions trend — Chart.js render check
+
+
+
+
+
+
+
+
+
How Hard to Get Into This School
+
+
+
First-choice offer rate
+
+
This year (2026/27), 30 families put it first for 30 places — 101 applications in total.
+
+
+
+
+
diff --git a/nextjs-app/components/AdmissionsTrendChart.module.css b/nextjs-app/components/AdmissionsTrendChart.module.css
new file mode 100644
index 0000000..2140d63
--- /dev/null
+++ b/nextjs-app/components/AdmissionsTrendChart.module.css
@@ -0,0 +1,10 @@
+.wrapper {
+ width: 100%;
+ height: 200px;
+}
+
+@media (max-width: 640px) {
+ .wrapper {
+ height: 180px;
+ }
+}
diff --git a/nextjs-app/components/AdmissionsTrendChart.tsx b/nextjs-app/components/AdmissionsTrendChart.tsx
new file mode 100644
index 0000000..cd80661
--- /dev/null
+++ b/nextjs-app/components/AdmissionsTrendChart.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+/**
+ * AdmissionsTrendChart
+ * Compact line chart of the first-choice offer rate across admissions years.
+ * Renders nothing unless at least two years carry an offer-rate value.
+ */
+
+import { Line } from 'react-chartjs-2';
+import { ChartOptions } from 'chart.js';
+import '@/lib/chartSetup';
+import { formatAcademicYear } from '@/lib/utils';
+import type { SchoolAdmissions } from '@/lib/types';
+import styles from './AdmissionsTrendChart.module.css';
+
+export default function AdmissionsTrendChart({ history }: { history: SchoolAdmissions[] }) {
+ const pts = history.filter((h) => h.first_preference_offer_pct != null);
+ if (pts.length < 2) return null;
+
+ const labels = pts.map((p) => formatAcademicYear(p.year));
+ const values = pts.map((p) => p.first_preference_offer_pct as number);
+ const lastIdx = pts.length - 1;
+
+ // Auto-scale with headroom so variation is visible, clamped to 0–100.
+ const lo = Math.min(...values);
+ const hi = Math.max(...values);
+ const padded = Math.max(5, Math.round((hi - lo) * 0.25));
+ const yMin = Math.max(0, Math.floor((lo - padded) / 5) * 5);
+ const yMax = Math.min(100, Math.ceil((hi + padded) / 5) * 5);
+
+ const options: ChartOptions<'line'> = {
+ responsive: true,
+ maintainAspectRatio: false,
+ interaction: { mode: 'index', intersect: false },
+ plugins: {
+ legend: { display: false },
+ title: { display: false },
+ tooltip: {
+ backgroundColor: 'rgba(26,22,18,0.92)',
+ padding: 10,
+ titleFont: { size: 12 },
+ bodyFont: { size: 12 },
+ callbacks: {
+ label: (ctx) => (ctx.parsed.y == null ? '' : `First-choice offers: ${Math.round(ctx.parsed.y)}%`),
+ },
+ },
+ },
+ scales: {
+ y: {
+ min: yMin,
+ max: yMax,
+ grid: { color: 'rgba(0,0,0,0.05)' },
+ ticks: { font: { size: 11 }, maxTicksLimit: 5, callback: (v) => `${v}%` },
+ },
+ x: {
+ grid: { display: false },
+ ticks: { font: { size: 11 }, autoSkip: true, maxRotation: 0, autoSkipPadding: 16 },
+ },
+ },
+ };
+
+ const data = {
+ labels,
+ datasets: [
+ {
+ label: 'First-choice offer rate',
+ data: values,
+ borderColor: '#e07256',
+ backgroundColor: 'rgba(224,114,86,0.10)',
+ borderWidth: 2.5,
+ tension: 0.3,
+ fill: true,
+ pointRadius: pts.map((_, i) => (i === lastIdx ? 5 : 3)),
+ pointBackgroundColor: '#e07256',
+ pointBorderColor: '#fff',
+ pointBorderWidth: pts.map((_, i) => (i === lastIdx ? 2 : 0)),
+ pointHoverRadius: 6,
+ },
+ ],
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/nextjs-app/components/SchoolDetailView.module.css b/nextjs-app/components/SchoolDetailView.module.css
index aabce08..9c03702 100644
--- a/nextjs-app/components/SchoolDetailView.module.css
+++ b/nextjs-app/components/SchoolDetailView.module.css
@@ -1381,49 +1381,6 @@
margin-bottom: 0.5rem;
}
-.admissionsChart {
- width: 100%;
- height: auto;
- display: block;
-}
-
-.admissionsGrid {
- stroke: var(--border-color, #e5dfd5);
- stroke-width: 1;
-}
-
-.admissionsAxis {
- font-size: 12.5px;
- fill: var(--text-muted, #6d685f);
- font-family: var(--font-dm-sans), "DM Sans", sans-serif;
-}
-
-.admissionsLine {
- stroke: var(--accent-coral, #e07256);
- stroke-width: 3;
-}
-
-.admissionsDot {
- fill: var(--accent-coral, #e07256);
-}
-
-.admissionsDotLast {
- fill: var(--accent-coral, #e07256);
- stroke: var(--bg-card, #fff);
- stroke-width: 2;
-}
-
-.admissionsPtLabel {
- font-size: 13px;
- font-weight: 700;
- fill: var(--text-primary, #1a1612);
- font-family: var(--font-dm-sans), "DM Sans", sans-serif;
-}
-
-.admissionsPtLabel[data-last="true"] {
- fill: var(--accent-coral-dark, #c45a3f);
-}
-
.admissionsTrendSummary {
font-size: 1rem;
color: var(--text-secondary, #5c564d);
diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx
index 57633c8..7a37e36 100644
--- a/nextjs-app/components/SchoolDetailView.tsx
+++ b/nextjs-app/components/SchoolDetailView.tsx
@@ -28,6 +28,7 @@ const PerformanceChart = dynamic(
{ ssr: false },
);
const SatsChart = dynamic(() => import('./SatsChart'), { ssr: false });
+const AdmissionsTrendChart = dynamic(() => import('./AdmissionsTrendChart'), { ssr: false });
import { track, getNavigationSource } from '@/lib/analytics';
import styles from './SchoolDetailView.module.css';
@@ -58,62 +59,6 @@ function progressClass(val: number | null | undefined): string {
return '';
}
-/**
- * Compact SVG sparkline of the first-choice offer rate across admissions years.
- * Renders nothing unless at least two years carry an offer-rate value.
- */
-function OfferRateTrend({ history }: { history: SchoolAdmissions[] }) {
- const pts = history
- .filter((h) => h.first_preference_offer_pct != null)
- .map((h) => ({ year: h.year, v: h.first_preference_offer_pct as number }));
- if (pts.length < 2) return null;
-
- const W = 520, H = 118;
- const padL = 44, padR = 20, padT = 16, padB = 34;
- const plotW = W - padL - padR, plotH = H - padT - padB;
-
- const values = pts.map((p) => p.v);
- let lo = Math.max(0, Math.floor(Math.min(...values) / 10) * 10);
- let hi = Math.min(100, Math.ceil(Math.max(...values) / 10) * 10);
- // Guarantee a minimum span so small year-to-year moves aren't exaggerated.
- if (hi - lo < 30) {
- hi = Math.min(100, lo + 30);
- if (hi - lo < 30) lo = Math.max(0, hi - 30);
- }
-
- const x = (i: number) => padL + (plotW * i) / (pts.length - 1);
- const y = (v: number) => padT + plotH * (1 - (v - lo) / (hi - lo));
- const gridVals = [hi, Math.round((hi + lo) / 2), lo];
- const polyline = pts.map((p, i) => `${x(i)},${y(p.v)}`).join(' ');
-
- return (
-
- );
-}
-
interface SchoolDetailViewProps {
schoolInfo: School;
yearlyData: SchoolResult[];
@@ -1003,7 +948,7 @@ export function SchoolDetailView({
{showAdmissionsTrend && (
First-choice offer rate
-
+
This year ({formatAcademicYear(admissions.year)}),{' '}
{admissions.first_preference_applications != null && (