Files
school_compare/nextjs-app/components/AdmissionsTrendChart.tsx
T

92 lines
2.8 KiB
TypeScript
Raw Normal View History

'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 0100.
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 },
// Headroom so a point sitting on the y-max ceiling (e.g. 100%) isn't
// clipped by the top of the plot area.
layout: { padding: { top: 8 } },
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,
clip: false as const,
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 (
<div className={styles.wrapper}>
<Line data={data} options={options} />
</div>
);
}