/** * SchoolDetailView Component * Displays comprehensive school information with performance charts */ 'use client'; import { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/navigation'; import dynamic from 'next/dynamic'; import { useComparison } from '@/hooks/useComparison'; import { SchoolHeroMap, type SchoolHeroMapHandle } from './SchoolHeroMap'; import { MetricTooltip } from './MetricTooltip'; import type { School, SchoolResult, AbsenceData, OfstedInspection, SchoolCensus, SchoolAdmissions, SenDetail, Phonics, SchoolDeprivation, SchoolFinance, NationalAverages, } from '@/lib/types'; import { formatPercentage, formatProgress, formatAcademicYear, isProposedToClose, ofstedLegacyAreas, isSpecialSchool, } from '@/lib/utils'; import { DeltaChip } from './DeltaChip'; import { summariseAdmissions } from '@/lib/compareLogic'; const PerformanceChart = dynamic( () => import('./PerformanceChart').then((m) => m.PerformanceChart), { 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'; const OFSTED_LABELS: Record = { 1: 'Outstanding', 2: 'Good', 3: 'Requires Improvement', 4: 'Inadequate', }; const RC_LABELS: Record = { 1: 'Exceptional', 2: 'Strong', 3: 'Expected standard', 4: 'Needs attention', 5: 'Urgent improvement', }; const RC_CATEGORIES = [ { key: 'rc_inclusion' as const, label: 'Inclusion' }, { key: 'rc_curriculum_teaching' as const, label: 'Curriculum & Teaching' }, { key: 'rc_achievement' as const, label: 'Achievement' }, { key: 'rc_attendance_behaviour' as const, label: 'Attendance & Behaviour' }, { key: 'rc_personal_development' as const, label: 'Personal Development' }, { key: 'rc_leadership_governance' as const, label: 'Leadership & Governance' }, { key: 'rc_early_years' as const, label: 'Early Years' }, { key: 'rc_sixth_form' as const, label: 'Sixth Form' }, ]; function progressClass(val: number | null | undefined): string { if (val == null) return ''; if (val > 0) return styles.progressPositive; if (val < 0) return styles.progressNegative; return ''; } interface SchoolDetailViewProps { schoolInfo: School; yearlyData: SchoolResult[]; absenceData: AbsenceData | null; ofsted: OfstedInspection | null; census: SchoolCensus | null; admissions: SchoolAdmissions | null; admissionsHistory: SchoolAdmissions[]; senDetail: SenDetail | null; phonics: Phonics | null; deprivation: SchoolDeprivation | null; finance: SchoolFinance | null; } export function SchoolDetailView({ schoolInfo, yearlyData, absenceData, ofsted, census, admissions, admissionsHistory, senDetail, phonics, deprivation, finance, }: SchoolDetailViewProps) { const router = useRouter(); const { addSchool, removeSchool, isSelected } = useComparison(); const isInComparison = isSelected(schoolInfo.urn); const [activeSection, setActiveSection] = useState(''); const [admissionsView, setAdmissionsView] = useState<'year' | 'trend'>('year'); // Trend toggle only appears with ≥2 years carrying an offer rate. const admissionsOfferYears = admissionsHistory.filter((h) => h.first_preference_offer_pct != null).length; const showAdmissionsTrend = admissionsOfferYears >= 2; // Banded interpretation of the first-choice offer rate ("More than half of // first choices missed out" etc.) — the same banding the compare screen // uses, so a low offer rate reads as how severe it actually is. const admissionsSummary = summariseAdmissions(admissions); // Only the section links scroll horizontally; Back and "All" stay pinned. const sectionLinksRef = useRef(null); const [sectionNavAtEnd, setSectionNavAtEnd] = useState(false); // Carry the "Add to Compare" CTA into the sticky bar once the hero's button leaves. const heroActionsRef = useRef(null); const [heroCtaVisible, setHeroCtaVisible] = useState(true); // Hero map — the "View on map" link opens its fullscreen view. const heroMapRef = useRef(null); // "All ▾" jump menu listing every section. const [sectionsOpen, setSectionsOpen] = useState(false); // Back returns to wherever the user came from; deep-links (no in-app history) // fall back to search so the button never dead-ends or leaves the site. const handleBack = () => { if (typeof window !== 'undefined' && window.history.length > 1) { router.back(); } else { router.push('/search'); } }; const scrollToTop = () => { if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' }); }; useEffect(() => { const el = sectionLinksRef.current; if (!el) return; const update = () => { const overflow = el.scrollWidth - el.clientWidth; // No overflow → treat as "at end" so the fade is hidden. if (overflow <= 1) { setSectionNavAtEnd(true); return; } setSectionNavAtEnd(el.scrollLeft >= overflow - 2); }; update(); el.addEventListener('scroll', update, { passive: true }); window.addEventListener('resize', update); return () => { el.removeEventListener('scroll', update); window.removeEventListener('resize', update); }; }, []); // Track whether the hero's "Add to Compare" button is still on screen. useEffect(() => { const el = heroActionsRef.current; if (!el) return; const obs = new IntersectionObserver( ([entry]) => setHeroCtaVisible(entry.isIntersecting), { rootMargin: '-64px 0px 0px 0px' }, ); obs.observe(el); return () => obs.disconnect(); }, []); // Close the "All ▾" menu on Escape. useEffect(() => { if (!sectionsOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setSectionsOpen(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [sectionsOpen]); const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null; // Phase detection. All-through schools cover BOTH key stages, so they are // neither "pure primary" nor "pure secondary": isSecondary stays true (they // have KS4 data) but isAllThrough gates the primary-only content (phonics, // KS2 trend) back on and switches phase-specific copy to an all-ages framing. const phase = schoolInfo.phase ?? ''; const isAllThrough = phase.toLowerCase() === 'all-through'; const isSecondary = phase.toLowerCase().includes('secondary') || isAllThrough; const isPrimary = !isSecondary; // Primary-stage content shows for pure-primary AND all-through schools. const showPrimaryContent = isPrimary || isAllThrough; // National averages (fetched dynamically so they stay current) const [nationalAvg, setNationalAvg] = useState(null); useEffect(() => { fetch('/api/national-averages') .then(r => r.ok ? r.json() : null) .then(data => { if (data) setNationalAvg(data); }) .catch(() => {}); }, []); const primaryAvg = nationalAvg?.primary ?? {}; const secondaryAvg = nationalAvg?.secondary ?? {}; const handleComparisonToggle = () => { if (isInComparison) { removeSchool(schoolInfo.urn); track('compare_school_removed', { urn: schoolInfo.urn, from: 'detail' }); } else { addSchool(schoolInfo); track('compare_school_added', { urn: schoolInfo.urn, from: 'detail' }); } }; // Page-view event with funnel attribution. Fires once per mount. useEffect(() => { track('school_viewed', { urn: schoolInfo.urn, phase: phase || 'unknown', local_authority: schoolInfo.local_authority || 'unknown', from: getNavigationSource(), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [schoolInfo.urn]); const deprivationDesc = (decile: number) => { if (decile <= 3) return `This school is in one of England's most deprived areas (decile ${decile}/10). Many pupils may face additional challenges at home.`; if (decile <= 7) return `This school is in an area with average levels of deprivation (decile ${decile}/10).`; return `This school is in one of England's less deprived areas (decile ${decile}/10).`; }; // Gender split availability (only meaningful for Mixed schools with census data) const isMixedSchool = schoolInfo.gender === 'Mixed' || schoolInfo.gender == null; const hasGenderSplit = isMixedSchool && census?.female_pupils != null && census?.male_pupils != null && (census.female_pupils + census.male_pupils) > 0; // Guard for Pupils & Inclusion — only show if at least one metric is available const hasInclusionData = (latestResults?.disadvantaged_pct != null) || (latestResults?.eal_pct != null) || (latestResults?.sen_support_pct != null) || senDetail != null || hasGenderSplit; const hasSchoolLife = absenceData != null || census?.class_size_avg != null; const hasPhonics = phonics != null && phonics.year1_phonics_pct != null; const hasDeprivation = deprivation != null && deprivation.idaci_decile != null; const hasFinance = finance != null && finance.per_pupil_spend != null; const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null; // Determine whether this school has KS2 or KS4 results to show const hasKS2Results = latestResults != null && latestResults.rwm_expected_pct != null; const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null; const hasAnyResults = hasKS2Results || hasKS4Results; // Special schools / PRUs / AP: their pupils sit the same tests but very few // reach the mainstream "expected standard", so a 0% headline and an England // comparison portray them as failing against a benchmark that doesn't fit. const isSpecial = isSpecialSchool(schoolInfo); // Belt-and-braces: a whole-row zero attainment (special or suppressed cohort) // is a placeholder, not a real mainstream result — suppress its comparison // too. A single 0 (e.g. 0% exceeding at a normal school) stays comparable. const ks2Placeholder = latestResults != null && latestResults.rwm_expected_pct === 0 && (latestResults.reading_expected_pct ?? 0) === 0 && (latestResults.writing_expected_pct ?? 0) === 0 && (latestResults.maths_expected_pct ?? 0) === 0; const ks4Placeholder = latestResults != null && latestResults.attainment_8_score === 0; // Whether to drop the England-average deltas / national markers / "below" // framing on the attainment measures. const suppressKs2Comparison = isSpecial || ks2Placeholder; const suppressKs4Comparison = isSpecial || ks4Placeholder; // Build section nav items dynamically — only sections with data. // Order is engagement-led (from section_nav_used analytics): the most-sought // sections — results, admissions, inclusion, history — sit near the top, // after the recognised Ofsted badge; low-demand context sections stay last. const navItems: { id: string; label: string }[] = []; if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' }); if (hasAnyResults) navItems.push({ id: 'results', label: isAllThrough ? 'Results' : isSecondary ? 'GCSEs' : 'SATs' }); if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' }); if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' }); if (yearlyData.length > 0) navItems.push({ id: 'history', label: 'History' }); if (hasPhonics && showPrimaryContent) navItems.push({ id: 'phonics', label: 'Phonics' }); if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' }); if (hasDeprivation) navItems.push({ id: 'local-area', label: 'Local Area' }); if (hasFinance) navItems.push({ id: 'finances', label: 'Finances' }); // Track active section as user scrolls useEffect(() => { const ids = navItems.map(n => n.id); if (!ids.length) return; const observers: IntersectionObserver[] = []; const ratioMap: Record = {}; const pickActive = () => { const top = Object.entries(ratioMap).sort((a, b) => b[1] - a[1])[0]; setActiveSection(top?.[1] > 0 ? top[0] : ''); }; ids.forEach(id => { const el = document.getElementById(id); if (!el) return; ratioMap[id] = 0; const obs = new IntersectionObserver( ([entry]) => { ratioMap[id] = entry.intersectionRatio; pickActive(); }, { threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0], rootMargin: '-56px 0px 0px 0px' }, ); obs.observe(el); observers.push(obs); }); return () => observers.forEach(o => o.disconnect()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [navItems.map(n => n.id).join(',')]); // A report card is identified by the presence of report-card area // judgements, NOT by `framework` — the API sets `framework` to the raw // event grouping (e.g. "Schools - S5") even for report-card schools, so // the old `framework === 'ReportCard'` test never matched and report cards // were rendered as legacy ratings dated to a pre-Nov-2025 inspection. const isReportCard = !!( ofsted?.report_card && Object.keys(ofsted.report_card).length > 0 ); // A report card is dated by its own inspection (rc_inspection_date); the // legacy inspection_date belongs to an older inspection and must never // date a report card (report cards exist only from Nov 2025). const ofstedInspectedDate = isReportCard ? ofsted?.rc_inspection_date ?? null : ofsted?.inspection_date ?? null; // ── Ofsted: detect if all OEIF sub-grades match the overall ─────────── const oeifAreas = ofsted ? ofstedLegacyAreas(ofsted) : []; const oeifAllSameGrade = !!ofsted && !isReportCard && oeifAreas.length >= 3 && oeifAreas.every((a) => a.value === ofsted.overall_effectiveness); // Label shown in the mobile "section" menu button — the section in view. const activeNavLabel = (navItems.find((n) => n.id === activeSection) ?? navItems[0])?.label ?? ''; return (
{/* Standalone back link, above the header — returns to wherever the user came from. Scrolls away with the page (the sticky bar keeps a "back to top" control in its place). */} {/* Header — the location map band blends down into the school title. */}
{hasLocation && ( )}

{schoolInfo.school_name}

{schoolInfo.local_authority && ( {schoolInfo.local_authority} )} {schoolInfo.school_type && ( {schoolInfo.school_type} )} {isAllThrough && ( All-through (primary & secondary) )} {schoolInfo.gender && schoolInfo.gender !== 'Mixed' && ( {schoolInfo.gender}'s school )}
{isProposedToClose(schoolInfo) && (
⚠ Proposed to close — this school is proposed for closure, check with the local authority before applying.
)} {schoolInfo.address && (

{schoolInfo.address}{schoolInfo.postcode && `, ${schoolInfo.postcode}`} {hasLocation && ( <> {' · '} )}

)}
{schoolInfo.headteacher_name && ( Headteacher: {schoolInfo.headteacher_name} )} {schoolInfo.website && ( School website ↗ )} {(() => { const total = census?.total_pupils ?? latestResults?.total_pupils ?? null; if (total == null) return null; return ( Pupils: {total.toLocaleString()} {schoolInfo.capacity != null && ` (capacity: ${schoolInfo.capacity})`} ); })()} {schoolInfo.trust_name && ( Part of {schoolInfo.trust_name} )}
{/* Sticky Section Navigation — docks under the global header */} {/* Ofsted Rating / Report Card */} {ofsted && (

{isReportCard ? 'Ofsted Report Card' : 'Ofsted Rating'} {ofstedInspectedDate && ( Inspected {new Date(ofstedInspectedDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })} )} Ofsted reports ↗

{isReportCard ? ( /* ── New Report Card layout ── */ <>

From November 2025, Ofsted replaced single overall grades with Report Cards rating schools across several areas.

{ofsted.rc_safeguarding_met != null && (
Safeguarding
{ofsted.rc_safeguarding_met ? 'Met' : 'Not met'}
)} {RC_CATEGORIES.map(({ key, label }) => { const value = ofsted[key] as number | null; return value != null ? (
{label}
{RC_LABELS[value]}
) : null; })}
) : ( /* ── Old OEIF layout ── */ <>
{ofsted.overall_effectiveness ? OFSTED_LABELS[ofsted.overall_effectiveness] : 'Not rated'} {ofsted.previous_overall != null && ofsted.previous_overall !== ofsted.overall_effectiveness && ( Previously: {OFSTED_LABELS[ofsted.previous_overall]} )}

{ofsted.grade_source === 'ungraded_carried_forward' ? 'This overall grade is carried forward from an earlier inspection — Ofsted has since visited without issuing a new overall grade. From September 2024, Ofsted no longer makes an overall effectiveness judgement.' : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'}

{oeifAllSameGrade ? (

Rated {OFSTED_LABELS[ofsted.overall_effectiveness!]} across all inspected areas — Quality of Teaching, Behaviour, Pupils' Development and Leadership.

) : (
{oeifAreas.map(({ label, value }) => (
{label}
{OFSTED_LABELS[value]}
))}
)} )}
)} {/* Results Section (SATs for primary, GCSEs for secondary) */} {hasAnyResults && latestResults && (

{isAllThrough ? 'SATs & GCSE Results' : isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})

{isSpecial ? (isSecondary ? 'GCSE results for Year 11 pupils.' : 'End-of-primary-school tests taken by Year 6 pupils.') : isAllThrough ? 'KS2 SATs (end of Year 6) and GCSE results (Year 11) — this school covers both. England averages shown for comparison.' : isSecondary ? 'GCSE results for Year 11 pupils. England averages shown for comparison.' : 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}

{/* Special schools sit the same national assessments but teach pupils with SEND, so very few reach the mainstream "expected standard". Explain that up front and drop the England comparison below, so a 0% headline never reads as a failing grade against a benchmark that doesn't fit the school. */} {isSpecial && (
This is a special school. Its pupils have special educational needs and work towards individual targets. They sit the same national tests, but very few reach the mainstream “expected standard” these measures report, so a comparison with the England average isn’t a meaningful guide to the school. Where available, the progress pupils make (below) is a fairer measure.
)} {/* ── Primary / KS2 content ── */} {hasKS2Results && ( <> {isAllThrough && (

Primary — KS2 SATs (Year 6)

)}
{latestResults.rwm_expected_pct !== null && (
Reading, Writing & Maths combined
{formatPercentage(latestResults.rwm_expected_pct)} {!suppressKs2Comparison && primaryAvg.rwm_expected_pct != null && ( )}
{!suppressKs2Comparison && primaryAvg.rwm_expected_pct != null && (
England avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%
)}
)} {latestResults.rwm_high_pct !== null && (
Exceeding expected level (Reading, Writing & Maths)
{formatPercentage(latestResults.rwm_high_pct)} {!suppressKs2Comparison && primaryAvg.rwm_high_pct != null && ( )}
{!suppressKs2Comparison && primaryAvg.rwm_high_pct != null && (
England avg: {primaryAvg.rwm_high_pct.toFixed(0)}%
)}
)}
{!suppressKs2Comparison && latestResults.rwm_expected_pct != null && latestResults.reading_expected_pct != null && latestResults.writing_expected_pct != null && latestResults.maths_expected_pct != null && (
Why is combined lower? A pupil is only counted if they met the bar in{' '} all three subjects. Some passed reading but not writing; some passed writing but not maths.
Reading {latestResults.reading_expected_pct.toFixed(0)}% · Writing {latestResults.writing_expected_pct.toFixed(0)}% · Maths {latestResults.maths_expected_pct.toFixed(0)}% All three {latestResults.rwm_expected_pct.toFixed(0)}%
)} {/* All-zero placeholder rows (special / suppressed) would render as three empty bars against the national markers — misleading, so skip the chart. For a special school with some non-zero subjects, keep the bars but drop the national markers. */} {!ks2Placeholder && ( )} {/* Progress scores row */} {(latestResults.reading_progress != null || latestResults.writing_progress != null || latestResults.maths_progress != null) && (

Progress Scores

{latestResults.reading_progress != null && (
Reading {formatProgress(latestResults.reading_progress)}
)} {latestResults.writing_progress != null && (
Writing {formatProgress(latestResults.writing_progress)}
)} {latestResults.maths_progress != null && (
Maths {formatProgress(latestResults.maths_progress)}
)}
)} {(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && (

Progress scores measure how much pupils improved compared to similar schools nationally. Above 0 = better than average, below 0 = below average.

)} )} {/* ── Secondary / KS4 content ── */} {hasKS4Results && ( <> {isAllThrough && (

Secondary — GCSEs (Year 11)

)}
{latestResults.attainment_8_score !== null && (
Attainment 8
{latestResults.attainment_8_score.toFixed(1)}
{!suppressKs4Comparison && secondaryAvg.attainment_8_score != null && (
England avg: {secondaryAvg.attainment_8_score.toFixed(1)}
)}
)} {latestResults.progress_8_score !== null && (
Progress 8
{formatProgress(latestResults.progress_8_score)}
0 = national average
)} {latestResults.english_maths_standard_pass_pct !== null && (
English & Maths Grade 4+
{formatPercentage(latestResults.english_maths_standard_pass_pct)}
{!suppressKs4Comparison && secondaryAvg.english_maths_standard_pass_pct != null && (
England avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%
)}
)} {latestResults.english_maths_strong_pass_pct !== null && (
English & Maths Grade 5+
{formatPercentage(latestResults.english_maths_strong_pass_pct)}
{!suppressKs4Comparison && secondaryAvg.english_maths_strong_pass_pct != null && (
England avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%
)}
)}
{/* EBacc */} {(latestResults.ebacc_entry_pct !== null || latestResults.ebacc_standard_pass_pct !== null) && ( <>

English Baccalaureate (EBacc)

{latestResults.ebacc_entry_pct !== null && (
Pupils entered for EBacc {formatPercentage(latestResults.ebacc_entry_pct)}
)} {latestResults.ebacc_standard_pass_pct !== null && (
EBacc Grade 4+ {formatPercentage(latestResults.ebacc_standard_pass_pct)}
)} {latestResults.ebacc_strong_pass_pct !== null && (
EBacc Grade 5+ {formatPercentage(latestResults.ebacc_strong_pass_pct)}
)}
)} )}
)} {/* Admissions */} {admissions && (

Admissions{!showAdmissionsTrend && ` (${formatAcademicYear(admissions.year)})`}

{showAdmissionsTrend && (
)}
{/* This-year Q&A */} {/* Multi-year trend */} {showAdmissionsTrend && ( )}
)} {/* Pupils & Inclusion */} {hasInclusionData && (

Pupils & Inclusion

{latestResults?.disadvantaged_pct != null && (
Eligible for pupil premium
{formatPercentage(latestResults.disadvantaged_pct)} {primaryAvg.disadvantaged_pct != null && ( )}
Pupils from disadvantaged backgrounds{primaryAvg.disadvantaged_pct != null ? ` · England avg: ${primaryAvg.disadvantaged_pct.toFixed(0)}%` : ''}
)} {latestResults?.eal_pct != null && (
English as an additional language
{formatPercentage(latestResults.eal_pct)} {primaryAvg.eal_pct != null && ( )}
{primaryAvg.eal_pct != null && (
England avg: {primaryAvg.eal_pct.toFixed(0)}%
)}
)} {latestResults?.sen_support_pct != null && (
Pupils receiving SEN support
{formatPercentage(latestResults.sen_support_pct)} {primaryAvg.sen_support_pct != null && ( )}
{primaryAvg.sen_support_pct != null && (
England avg: {primaryAvg.sen_support_pct.toFixed(0)}%
)}
)} {hasGenderSplit && (() => { const female = census!.female_pupils!; const male = census!.male_pupils!; const girlsPct = Math.round((female / (female + male)) * 100); const boysPct = 100 - girlsPct; return (
Boys and girls
{girlsPct}% girls · {boysPct}% boys
{female.toLocaleString()} girls, {male.toLocaleString()} boys
); })()}
{senDetail && ( <>

Types of additional needs supported

What proportion of pupils with additional needs have each type of support need.

{[ { label: 'Speech & Language', pct: senDetail.primary_need_speech_pct }, { label: 'Autism (ASD)', pct: senDetail.primary_need_autism_pct }, { label: 'Learning Difficulties', pct: senDetail.primary_need_mld_pct }, { label: 'Specific Learning (e.g. Dyslexia)', pct: senDetail.primary_need_spld_pct }, { label: 'Social, Emotional & Mental Health', pct: senDetail.primary_need_semh_pct }, { label: 'Physical / Sensory', pct: senDetail.primary_need_physical_pct }, ].filter(n => n.pct != null).map(({ label, pct }) => (
{label}
{pct}%
))}
)}
)} {/* Results Over Time (merged: chart + historical table) */} {yearlyData.length > 0 && (

Results Over Time

{isAllThrough ? ( // All-through: KS2 and KS4 trends are on different scales and have // different gap stories, so render them as two stacked charts // rather than crowding 8+ series onto one axis. <> {hasKS2Results && ( <>

Primary — KS2 SATs

)} {hasKS4Results && ( <>

Secondary — GCSEs

)} ) : (
)} {yearlyData.length > 1 && (
View raw year-by-year data
{isAllThrough ? ( <> ) : isSecondary ? ( <> ) : ( <> )} {yearlyData.map((result) => ( {isAllThrough ? ( <> ) : isSecondary ? ( <> ) : ( <> )} ))}
YearRWM (expected %) Exceeding (%) Attainment 8 Progress 8 English & Maths Grade 4+Attainment 8 Progress 8 English & Maths Grade 4+ English & Maths Grade 5+Reading, Writing & Maths (expected %) Exceeding expected (%) Reading Progress Writing Progress Maths Progress
{formatAcademicYear(result.year)}{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'} {result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'} {result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'} {result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'} {result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'} {result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'} {result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'} {result.english_maths_strong_pass_pct !== null ? formatPercentage(result.english_maths_strong_pass_pct) : '-'}{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'} {result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'} {result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'} {result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'} {result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}
)}
)} {/* Year 1 Phonics — primary-stage metric (pure primary + all-through) */} {hasPhonics && showPrimaryContent && phonics && (

Year 1 Phonics ({formatAcademicYear(phonics.year)})

Phonics is a key early reading skill. Children are tested at the end of Year 1.

Passed the phonics check
{formatPercentage(phonics.year1_phonics_pct)}
Phonics is a key early reading skill tested at end of Year 1
{phonics.year2_phonics_pct != null && (
Year 2 pupils who retook and passed
{formatPercentage(phonics.year2_phonics_pct)}
)}
)} {/* School Life */} {hasSchoolLife && (

School Life

{census?.class_size_avg != null && (
Average class size
{census.class_size_avg.toFixed(1)}
Average number of pupils per class
)} {absenceData?.overall_absence_rate != null && (
Days missed (overall absence)
{formatPercentage(absenceData.overall_absence_rate)}
{primaryAvg.overall_absence_pct != null && (
England avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%
)}
)} {absenceData?.persistent_absence_rate != null && (
Regularly missing school
{formatPercentage(absenceData.persistent_absence_rate)}
{primaryAvg.persistent_absence_pct != null && (
England avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%
)}
)}
)} {/* Local Area Context */} {hasDeprivation && deprivation && (

Local Area Context

{Array.from({ length: 10 }, (_, i) => (
))}
Most deprived Least deprived

{deprivationDesc(deprivation.idaci_decile!)}

)} {/* Finances */} {hasFinance && finance && (

School Finances ({formatAcademicYear(finance.year)})

Per-pupil spending shows how much the school has to spend on each child's education.

Total spend per pupil per year
£{Math.round(finance.per_pupil_spend!).toLocaleString()}
How much the school has to spend on each pupil annually
{finance.teacher_cost_pct != null && (
Share of budget spent on teachers
{finance.teacher_cost_pct.toFixed(1)}%
)} {finance.staff_cost_pct != null && (
Share of budget spent on all staff
{finance.staff_cost_pct.toFixed(1)}%
)}
)}
); }