|
|
|
@@ -16,6 +16,7 @@ import { ChartOptions, ChartDataset } from 'chart.js';
|
|
|
|
|
import '@/lib/chartSetup';
|
|
|
|
|
import type { SchoolResult } from '@/lib/types';
|
|
|
|
|
import { formatAcademicYear } from '@/lib/utils';
|
|
|
|
|
import { fillAcademicYears } from '@/lib/compareChartData';
|
|
|
|
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
|
|
|
import { track } from '@/lib/analytics';
|
|
|
|
|
import styles from './PerformanceChart.module.css';
|
|
|
|
@@ -35,8 +36,6 @@ interface PerformanceChartProps {
|
|
|
|
|
nationalByYear?: NationalByYear[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const COVID_YEARS = new Set([201920, 202021]);
|
|
|
|
|
|
|
|
|
|
// Mobile chip definitions: which datasets render when each chip is active.
|
|
|
|
|
// `series` keys reference the dataset labels so we can filter cleanly.
|
|
|
|
|
type ChipId = 'expected' | 'higher' | 'progress' | 'attainment8' | 'em_pass' | 'progress8';
|
|
|
|
@@ -67,21 +66,33 @@ export function PerformanceChart({
|
|
|
|
|
nationalByYear,
|
|
|
|
|
}: PerformanceChartProps) {
|
|
|
|
|
const sortedData = [...data].sort((a, b) => a.year - b.year);
|
|
|
|
|
const years = sortedData.map(d => formatAcademicYear(d.year));
|
|
|
|
|
|
|
|
|
|
// Gap-honest year axis: every academic year between the first and last data
|
|
|
|
|
// point appears, so cancelled/unpublished years (2019/20, 2020/21, and — for
|
|
|
|
|
// KS2 — 2021/22) render as real gaps rather than compressed time. School
|
|
|
|
|
// lines never bridge these gaps (spanGaps:false below).
|
|
|
|
|
const axisYears = fillAcademicYears(sortedData.map(d => d.year));
|
|
|
|
|
const byYear = new Map(sortedData.map(d => [d.year, d]));
|
|
|
|
|
const col = (key: keyof SchoolResult): (number | null)[] =>
|
|
|
|
|
axisYears.map(y => {
|
|
|
|
|
const v = byYear.get(y)?.[key];
|
|
|
|
|
return typeof v === 'number' ? v : null;
|
|
|
|
|
});
|
|
|
|
|
const years = axisYears.map(formatAcademicYear);
|
|
|
|
|
|
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
|
|
|
|
|
|
// ── Build per-year national averages ─────────────────────────────────
|
|
|
|
|
const natRefRwm: (number | null)[] = sortedData.map(d => {
|
|
|
|
|
// ── Build per-year national averages (aligned to the filled axis) ────
|
|
|
|
|
const natRefRwm: (number | null)[] = axisYears.map(y => {
|
|
|
|
|
if (nationalByYear) {
|
|
|
|
|
const match = nationalByYear.find(n => n.year === d.year);
|
|
|
|
|
const match = nationalByYear.find(n => n.year === y);
|
|
|
|
|
return match?.primary?.rwm_expected_pct ?? null;
|
|
|
|
|
}
|
|
|
|
|
return nationalRwmAvg ?? null;
|
|
|
|
|
});
|
|
|
|
|
const natRefAtt8: (number | null)[] = sortedData.map(d => {
|
|
|
|
|
const natRefAtt8: (number | null)[] = axisYears.map(y => {
|
|
|
|
|
if (nationalByYear) {
|
|
|
|
|
const match = nationalByYear.find(n => n.year === d.year);
|
|
|
|
|
const match = nationalByYear.find(n => n.year === y);
|
|
|
|
|
return match?.secondary?.attainment_8_score ?? null;
|
|
|
|
|
}
|
|
|
|
|
return nationalAtt8Avg ?? null;
|
|
|
|
@@ -107,10 +118,11 @@ export function PerformanceChart({
|
|
|
|
|
return `${arrow} Reading, Writing & Maths peaked at ${bestPct}% (${formatAcademicYear(best.year)}), currently ${latestPct}%`;
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
const hasCovidGap = isSecondary
|
|
|
|
|
? false
|
|
|
|
|
: COVID_YEARS.size > 0 &&
|
|
|
|
|
[...COVID_YEARS].some(y => !sortedData.find(d => d.year === y));
|
|
|
|
|
// A gap year is any filled axis year the school has no results row for —
|
|
|
|
|
// exactly the cancelled/unpublished years the fill introduced. Drives the
|
|
|
|
|
// honest note below (KS2 and KS4 have different gap stories).
|
|
|
|
|
const gapYears = axisYears.filter(y => !byYear.has(y));
|
|
|
|
|
const hasGap = gapYears.length > 0;
|
|
|
|
|
|
|
|
|
|
// ── Datasets (full set; mobile filters them via the active chip) ─────
|
|
|
|
|
const refLineStyle = {
|
|
|
|
@@ -120,13 +132,14 @@ export function PerformanceChart({
|
|
|
|
|
borderDash: [6, 4] as number[],
|
|
|
|
|
pointRadius: 0,
|
|
|
|
|
tension: 0,
|
|
|
|
|
spanGaps: false as const,
|
|
|
|
|
order: 10,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const allDatasets: ChartDataset<'line'>[] = isSecondary ? [
|
|
|
|
|
{
|
|
|
|
|
label: 'Attainment 8',
|
|
|
|
|
data: sortedData.map(d => d.attainment_8_score),
|
|
|
|
|
data: col('attainment_8_score'),
|
|
|
|
|
borderColor: '#2d7d7d',
|
|
|
|
|
backgroundColor: 'rgba(45,125,125,0.08)',
|
|
|
|
|
borderWidth: 2.5,
|
|
|
|
@@ -137,7 +150,7 @@ export function PerformanceChart({
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'English & Maths Grade 4+',
|
|
|
|
|
data: sortedData.map(d => d.english_maths_standard_pass_pct),
|
|
|
|
|
data: col('english_maths_standard_pass_pct'),
|
|
|
|
|
borderColor: '#c9a227',
|
|
|
|
|
backgroundColor: 'rgba(201,162,39,0.08)',
|
|
|
|
|
borderWidth: 1.5,
|
|
|
|
@@ -147,7 +160,7 @@ export function PerformanceChart({
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Progress 8',
|
|
|
|
|
data: sortedData.map(d => d.progress_8_score),
|
|
|
|
|
data: col('progress_8_score'),
|
|
|
|
|
borderColor: 'rgb(139,92,246)',
|
|
|
|
|
backgroundColor: 'rgba(139,92,246,0.08)',
|
|
|
|
|
borderWidth: 1.5,
|
|
|
|
@@ -165,7 +178,7 @@ export function PerformanceChart({
|
|
|
|
|
] : [
|
|
|
|
|
{
|
|
|
|
|
label: 'Reading, Writing & Maths expected %',
|
|
|
|
|
data: sortedData.map(d => d.rwm_expected_pct),
|
|
|
|
|
data: col('rwm_expected_pct'),
|
|
|
|
|
borderColor: '#2d7d7d',
|
|
|
|
|
backgroundColor: 'rgba(45,125,125,0.08)',
|
|
|
|
|
borderWidth: 2.5,
|
|
|
|
@@ -176,7 +189,7 @@ export function PerformanceChart({
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Exceeding expected level',
|
|
|
|
|
data: sortedData.map(d => d.rwm_high_pct),
|
|
|
|
|
data: col('rwm_high_pct'),
|
|
|
|
|
borderColor: '#c9a227',
|
|
|
|
|
backgroundColor: 'rgba(201,162,39,0.08)',
|
|
|
|
|
borderWidth: 1.5,
|
|
|
|
@@ -192,7 +205,7 @@ export function PerformanceChart({
|
|
|
|
|
} as ChartDataset<'line'>] : []),
|
|
|
|
|
{
|
|
|
|
|
label: 'Reading progress',
|
|
|
|
|
data: sortedData.map(d => d.reading_progress),
|
|
|
|
|
data: col('reading_progress'),
|
|
|
|
|
borderColor: 'rgb(59,130,246)',
|
|
|
|
|
backgroundColor: 'rgba(59,130,246,0.08)',
|
|
|
|
|
borderWidth: 1.5,
|
|
|
|
@@ -203,7 +216,7 @@ export function PerformanceChart({
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Writing progress',
|
|
|
|
|
data: sortedData.map(d => d.writing_progress),
|
|
|
|
|
data: col('writing_progress'),
|
|
|
|
|
borderColor: 'rgb(139,92,246)',
|
|
|
|
|
backgroundColor: 'rgba(139,92,246,0.08)',
|
|
|
|
|
borderWidth: 1.5,
|
|
|
|
@@ -214,7 +227,7 @@ export function PerformanceChart({
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Maths progress',
|
|
|
|
|
data: sortedData.map(d => d.maths_progress),
|
|
|
|
|
data: col('maths_progress'),
|
|
|
|
|
borderColor: 'rgb(236,72,153)',
|
|
|
|
|
backgroundColor: 'rgba(236,72,153,0.08)',
|
|
|
|
|
borderWidth: 1.5,
|
|
|
|
@@ -275,6 +288,8 @@ export function PerformanceChart({
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
interaction: { mode: 'index', intersect: false },
|
|
|
|
|
// Never bridge missing years — cancelled/unpublished years are real gaps.
|
|
|
|
|
datasets: { line: { spanGaps: false } },
|
|
|
|
|
plugins: {
|
|
|
|
|
legend: {
|
|
|
|
|
position: 'top',
|
|
|
|
@@ -326,6 +341,7 @@ export function PerformanceChart({
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
interaction: { mode: 'index', intersect: false },
|
|
|
|
|
datasets: { line: { spanGaps: false } },
|
|
|
|
|
plugins: {
|
|
|
|
|
legend: { display: false },
|
|
|
|
|
title: { display: false },
|
|
|
|
@@ -353,7 +369,10 @@ export function PerformanceChart({
|
|
|
|
|
},
|
|
|
|
|
x: {
|
|
|
|
|
grid: { display: false },
|
|
|
|
|
ticks: { font: { size: 10 }, autoSkip: false },
|
|
|
|
|
// With the gap-honest axis (more year labels) autoSkip keeps the
|
|
|
|
|
// phone axis readable; the broken line still shows where a year is
|
|
|
|
|
// missing even when its tick label is skipped.
|
|
|
|
|
ticks: { font: { size: 10 }, autoSkip: true, maxTicksLimit: 5, maxRotation: 0 },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
@@ -416,9 +435,11 @@ export function PerformanceChart({
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{hasCovidGap && (
|
|
|
|
|
{hasGap && (
|
|
|
|
|
<p className={styles.covidNote}>
|
|
|
|
|
* No data for 2019/20 or 2020/21 — national assessments were cancelled due to COVID-19.
|
|
|
|
|
{isSecondary
|
|
|
|
|
? "School-level GCSE figures for 2019/20 and 2020/21 weren't published (COVID grading) where the line breaks — the England average is shown where available."
|
|
|
|
|
: "No national tests were held in 2019/20 and 2020/21 (COVID), and DfE didn't publish school-level figures for 2021/22 — the England average is shown for those years where available."}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|