fix(admissions): render trend with Chart.js instead of scaled SVG
The hand-rolled SVG sparkline used px font sizes inside a 520-wide viewBox that stretched to the full card width, so labels ballooned ~4x and collided — and with 10+ years of real data the per-point labels and year ticks overlapped badly, while the oversized chart stretched the "this year" view. Replace it with a Chart.js line chart (AdmissionsTrendChart) in a fixed 200px wrapper, matching PerformanceChart: responsive px fonts, auto-skipping x ticks, auto-scaled y-axis clamped to 0-100, emphasised latest point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<svg
|
||||
className={styles.admissionsChart}
|
||||
viewBox={`0 0 ${W} ${H}`}
|
||||
role="img"
|
||||
aria-label={`First-choice offer rate from ${formatAcademicYear(pts[0].year)} to ${formatAcademicYear(pts[pts.length - 1].year)}`}
|
||||
>
|
||||
{gridVals.map((gv) => (
|
||||
<g key={gv}>
|
||||
<line x1={padL} y1={y(gv)} x2={W - padR} y2={y(gv)} className={styles.admissionsGrid} />
|
||||
<text x={padL - 8} y={y(gv) + 4} textAnchor="end" className={styles.admissionsAxis}>{gv}%</text>
|
||||
</g>
|
||||
))}
|
||||
<polyline points={polyline} fill="none" className={styles.admissionsLine} strokeLinecap="round" strokeLinejoin="round" />
|
||||
{pts.map((p, i) => {
|
||||
const isLast = i === pts.length - 1;
|
||||
return (
|
||||
<g key={p.year}>
|
||||
<circle cx={x(i)} cy={y(p.v)} r={isLast ? 6 : 5} className={isLast ? styles.admissionsDotLast : styles.admissionsDot} />
|
||||
<text x={x(i)} y={y(p.v) - 9} textAnchor="middle" className={styles.admissionsPtLabel} data-last={isLast}>{Math.round(p.v)}%</text>
|
||||
<text x={x(i)} y={H - 12} textAnchor="middle" className={styles.admissionsAxis}>{formatAcademicYear(p.year)}</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface SchoolDetailViewProps {
|
||||
schoolInfo: School;
|
||||
yearlyData: SchoolResult[];
|
||||
@@ -1003,7 +948,7 @@ export function SchoolDetailView({
|
||||
{showAdmissionsTrend && (
|
||||
<div className={styles.admissionsViewTrend} hidden={admissionsView !== 'trend'}>
|
||||
<div className={styles.admissionsChartCap}>First-choice offer rate</div>
|
||||
<OfferRateTrend history={admissionsHistory} />
|
||||
<AdmissionsTrendChart history={admissionsHistory} />
|
||||
<p className={styles.admissionsTrendSummary}>
|
||||
This year ({formatAcademicYear(admissions.year)}),{' '}
|
||||
{admissions.first_preference_applications != null && (
|
||||
|
||||
Reference in New Issue
Block a user