2026-07-13 23:54:20 +01:00
|
|
|
/**
|
|
|
|
|
* Explore trends — the full grouped metric catalogue (nothing from the old
|
2026-07-15 07:29:24 +01:00
|
|
|
* compare page is lost; spec §4's tier 3) driving the year-by-year chart with
|
|
|
|
|
* its England reference line. Matches the mockup: a measure picker and the
|
|
|
|
|
* chart only (no data table).
|
2026-07-13 23:54:20 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import dynamic from 'next/dynamic';
|
|
|
|
|
|
|
|
|
|
import type { ComparisonData, MetricDefinition, NationalAverages, School } from '@/lib/types';
|
|
|
|
|
import { track } from '@/lib/analytics';
|
2026-07-15 07:29:24 +01:00
|
|
|
import { Section } from './sectionShared';
|
2026-07-13 23:54:20 +01:00
|
|
|
import styles from './TrendsExplorer.module.css';
|
|
|
|
|
|
|
|
|
|
const ComparisonChart = dynamic(
|
|
|
|
|
() => import('../ComparisonChart').then((m) => m.ComparisonChart),
|
|
|
|
|
{ ssr: false },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const PRIMARY_OPTGROUPS: { label: string; category: string }[] = [
|
|
|
|
|
{ label: 'Expected Standard', category: 'expected' },
|
|
|
|
|
{ label: 'Higher Standard', category: 'higher' },
|
|
|
|
|
{ label: 'Progress Scores', category: 'progress' },
|
|
|
|
|
{ label: 'Average Scores', category: 'average' },
|
|
|
|
|
{ label: 'Gender Performance', category: 'gender' },
|
|
|
|
|
{ label: 'Equity (Disadvantaged)', category: 'equity' },
|
|
|
|
|
{ label: 'School Context', category: 'context' },
|
|
|
|
|
{ label: 'Absence', category: 'absence' },
|
|
|
|
|
{ label: '3-Year Trends', category: 'trends' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const SECONDARY_OPTGROUPS: { label: string; category: string }[] = [
|
|
|
|
|
{ label: 'GCSE Performance', category: 'gcse' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const PRIMARY_CATEGORIES = PRIMARY_OPTGROUPS.map((g) => g.category);
|
|
|
|
|
export const SECONDARY_CATEGORIES = SECONDARY_OPTGROUPS.map((g) => g.category);
|
|
|
|
|
|
|
|
|
|
export function TrendsExplorer({
|
|
|
|
|
schools,
|
|
|
|
|
data,
|
|
|
|
|
metrics,
|
2026-07-13 23:58:00 +01:00
|
|
|
metric,
|
|
|
|
|
onMetricChange,
|
2026-07-13 23:54:20 +01:00
|
|
|
isPrimaryPhase,
|
|
|
|
|
nationalAverages,
|
|
|
|
|
}: {
|
|
|
|
|
schools: School[];
|
|
|
|
|
data: Record<string, ComparisonData>;
|
|
|
|
|
metrics: MetricDefinition[];
|
2026-07-13 23:58:00 +01:00
|
|
|
/** Controlled: the page owns the metric so the URL contract survives. */
|
|
|
|
|
metric: string;
|
|
|
|
|
onMetricChange: (metric: string) => void;
|
2026-07-13 23:54:20 +01:00
|
|
|
isPrimaryPhase: boolean;
|
|
|
|
|
nationalAverages?: NationalAverages;
|
|
|
|
|
}) {
|
|
|
|
|
const allowedCategories = isPrimaryPhase ? PRIMARY_CATEGORIES : SECONDARY_CATEGORIES;
|
|
|
|
|
const optgroups = isPrimaryPhase ? PRIMARY_OPTGROUPS : SECONDARY_OPTGROUPS;
|
|
|
|
|
const filteredMetrics = metrics.filter((m) => allowedCategories.includes(m.category));
|
|
|
|
|
const metricDef = metrics.find((m) => m.key === metric);
|
|
|
|
|
const metricLabel = metricDef?.label || metric;
|
|
|
|
|
|
|
|
|
|
const nationalByYear: Record<number, number | null> = {};
|
|
|
|
|
for (const entry of nationalAverages?.by_year ?? []) {
|
|
|
|
|
const block = isPrimaryPhase ? entry.primary : entry.secondary;
|
|
|
|
|
nationalByYear[entry.year] = block?.[metric] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMetricChange = (next: string) => {
|
|
|
|
|
track('compare_metric_changed', { metric: next, phase: isPrimaryPhase ? 'primary' : 'secondary' });
|
2026-07-13 23:58:00 +01:00
|
|
|
onMetricChange(next);
|
2026-07-13 23:54:20 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Section
|
|
|
|
|
title="Explore trends"
|
|
|
|
|
how="The full year-by-year explorer for any measure, with the England average as a dashed reference line where official figures exist."
|
|
|
|
|
>
|
|
|
|
|
<details className={styles.explore} open>
|
|
|
|
|
<summary>Year-by-year trends</summary>
|
|
|
|
|
<div className={styles.inner}>
|
|
|
|
|
<div className={styles.picker}>
|
|
|
|
|
<label htmlFor="trends-metric-select">Measure:</label>
|
|
|
|
|
<select
|
|
|
|
|
id="trends-metric-select"
|
|
|
|
|
value={metric}
|
|
|
|
|
onChange={(e) => handleMetricChange(e.target.value)}
|
|
|
|
|
>
|
|
|
|
|
{optgroups.map(({ label, category }) => {
|
|
|
|
|
const group = filteredMetrics.filter((m) => m.category === category);
|
|
|
|
|
if (group.length === 0) return null;
|
|
|
|
|
return (
|
|
|
|
|
<optgroup key={category} label={label}>
|
|
|
|
|
{group.map((m) => (
|
|
|
|
|
<option key={m.key} value={m.key}>
|
|
|
|
|
{m.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</optgroup>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</select>
|
|
|
|
|
{metricDef?.description && <span className={styles.desc}>{metricDef.description}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{metric.includes('progress') && (
|
|
|
|
|
<p className={styles.progressNote}>
|
|
|
|
|
Progress scores measure pupils' progress from KS1 to KS2. A score of 0 equals the
|
2026-07-15 07:29:24 +01:00
|
|
|
national average. DfE stopped publishing KS2 progress after 2022/23 (no KS1 baseline).
|
2026-07-13 23:54:20 +01:00
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className={styles.chartBox}>
|
|
|
|
|
<ComparisonChart
|
|
|
|
|
comparisonData={data}
|
|
|
|
|
schools={schools}
|
|
|
|
|
metric={metric}
|
|
|
|
|
metricLabel={metricLabel}
|
|
|
|
|
nationalByYear={nationalByYear}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
</Section>
|
|
|
|
|
);
|
|
|
|
|
}
|