293 lines
10 KiB
TypeScript
293 lines
10 KiB
TypeScript
/**
|
||||
|
|
* How children do academically — tier-1 dot strips anchored on official
|
|||
|
|
* England averages, tier-2 "More measures" one tap away, equity row against
|
|||
|
|
* the computed state-school benchmark. Copy verbatim from the reviewed
|
|||
|
|
* mockups; teacher-assessed measures are labelled as such.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { latestValues, verdict } from '@/lib/compareLogic';
|
|||
|
|
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
|
|||
|
|
import { DotStrip } from '@/components/DotStrip';
|
|||
|
|
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
|||
|
|
import styles from './CompareAcademics.module.css';
|
|||
|
|
|
|||
|
|
interface StripSpec {
|
|||
|
|
label: string;
|
|||
|
|
metric: string;
|
|||
|
|
anchorKey?: string;
|
|||
|
|
tip?: string;
|
|||
|
|
min?: number;
|
|||
|
|
max?: number;
|
|||
|
|
unit?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const TIER1_PRIMARY: StripSpec[] = [
|
|||
|
|
{
|
|||
|
|
label: 'Reading, writing & maths — expected standard',
|
|||
|
|
metric: 'rwm_expected_pct',
|
|||
|
|
anchorKey: 'rwm_expected_pct',
|
|||
|
|
tip: '% of Year 6 pupils reaching the expected standard in reading, writing and maths.',
|
|||
|
|
},
|
|||
|
|
{ label: 'Reading', metric: 'reading_expected_pct', anchorKey: 'reading_expected_pct' },
|
|||
|
|
{
|
|||
|
|
label: 'Writing (teacher-assessed)',
|
|||
|
|
metric: 'writing_expected_pct',
|
|||
|
|
anchorKey: 'writing_expected_pct',
|
|||
|
|
tip: 'Writing is assessed by teachers, not tested.',
|
|||
|
|
},
|
|||
|
|
{ label: 'Maths', metric: 'maths_expected_pct', anchorKey: 'maths_expected_pct' },
|
|||
|
|
{
|
|||
|
|
label: 'Working at a higher standard than expected',
|
|||
|
|
metric: 'rwm_high_pct',
|
|||
|
|
anchorKey: 'rwm_high_pct',
|
|||
|
|
tip: 'A high score in the reading and maths tests plus “greater depth” in teacher-assessed writing.',
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const TIER2_PRIMARY: StripSpec[] = [
|
|||
|
|
{
|
|||
|
|
label: 'Grammar, punctuation & spelling — expected standard',
|
|||
|
|
metric: 'gps_expected_pct',
|
|||
|
|
anchorKey: 'gps_expected_pct',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'Science — expected standard (teacher-assessed)',
|
|||
|
|
metric: 'science_expected_pct',
|
|||
|
|
anchorKey: 'science_expected_pct',
|
|||
|
|
tip: 'Teacher-assessed, like writing — there has been no KS2 science test since 2009, so comparisons are indicative.',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'Average scaled score — reading',
|
|||
|
|
metric: 'reading_avg_score',
|
|||
|
|
anchorKey: 'reading_avg_score',
|
|||
|
|
min: 100,
|
|||
|
|
max: 120,
|
|||
|
|
unit: '',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'Average scaled score — maths',
|
|||
|
|
metric: 'maths_avg_score',
|
|||
|
|
anchorKey: 'maths_avg_score',
|
|||
|
|
min: 100,
|
|||
|
|
max: 120,
|
|||
|
|
unit: '',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: 'Average scaled score — grammar, punctuation & spelling',
|
|||
|
|
metric: 'gps_avg_score',
|
|||
|
|
anchorKey: 'gps_avg_score',
|
|||
|
|
min: 100,
|
|||
|
|
max: 120,
|
|||
|
|
unit: '',
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function Strip({
|
|||
|
|
spec,
|
|||
|
|
data,
|
|||
|
|
urns,
|
|||
|
|
schoolNames,
|
|||
|
|
national,
|
|||
|
|
}: {
|
|||
|
|
spec: StripSpec;
|
|||
|
|
data: Record<string, ComparisonData>;
|
|||
|
|
urns: number[];
|
|||
|
|
schoolNames: string[];
|
|||
|
|
national: Record<string, number> | undefined;
|
|||
|
|
}) {
|
|||
|
|
const values = latestValues(data, urns, spec.metric).map((v) =>
|
|||
|
|
v != null ? Math.round(v) : null,
|
|||
|
|
);
|
|||
|
|
const anchorValue = spec.anchorKey ? national?.[spec.anchorKey] : undefined;
|
|||
|
|
const anchor =
|
|||
|
|
anchorValue != null
|
|||
|
|
? { value: anchorValue, label: `England ${Math.round(anchorValue)}${spec.unit ?? '%'}` }
|
|||
|
|
: null;
|
|||
|
|
if (values.every((v) => v == null)) return null;
|
|||
|
|
return (
|
|||
|
|
<DotStrip
|
|||
|
|
label={spec.label}
|
|||
|
|
values={values}
|
|||
|
|
schoolNames={schoolNames}
|
|||
|
|
anchor={anchor}
|
|||
|
|
min={spec.min ?? 0}
|
|||
|
|
max={spec.max ?? 100}
|
|||
|
|
unit={spec.unit ?? '%'}
|
|||
|
|
tip={spec.tip}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function CompareAcademics({
|
|||
|
|
schools,
|
|||
|
|
data,
|
|||
|
|
nationalAverages,
|
|||
|
|
benchmarks,
|
|||
|
|
}: {
|
|||
|
|
schools: School[];
|
|||
|
|
data: Record<string, ComparisonData>;
|
|||
|
|
nationalAverages?: NationalAverages;
|
|||
|
|
benchmarks?: Benchmarks;
|
|||
|
|
}) {
|
|||
|
|
const urns = schools.map((school) => school.urn);
|
|||
|
|
const schoolNames = schools.map((school) => school.school_name);
|
|||
|
|
const isSecondary = schools.some(
|
|||
|
|
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (isSecondary) {
|
|||
|
|
const att8 = latestValues(data, urns, 'attainment_8_score');
|
|||
|
|
const banding = urns.map((urn) => {
|
|||
|
|
const rows = data[String(urn)]?.yearly_data ?? [];
|
|||
|
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|||
|
|
if (rows[i].progress_8_banding) return rows[i].progress_8_banding as string;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
});
|
|||
|
|
const grade5 = latestValues(data, urns, 'english_maths_strong_pass_pct');
|
|||
|
|
const ebacc = latestValues(data, urns, 'ebacc_entry_pct');
|
|||
|
|
const att8Anchor = nationalAverages?.secondary?.attainment_8_score;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Section
|
|||
|
|
title="How students do academically"
|
|||
|
|
how="GCSE results (latest year). Attainment 8 averages performance across eight subjects; Progress 8 shows how much progress students make compared with similar students nationally — the wording is DfE's own banding."
|
|||
|
|
>
|
|||
|
|
<SectionGrid schools={schools}>
|
|||
|
|
<RowLabel tip="Average Attainment 8 score across eight GCSE subjects.">Attainment 8</RowLabel>
|
|||
|
|
{schools.map((school, i) => (
|
|||
|
|
<Cell key={school.urn} school={school} index={i}>
|
|||
|
|
{att8[i] != null ? (
|
|||
|
|
<>
|
|||
|
|
<span className={s.big}>{(att8[i] as number).toFixed(1)}</span>
|
|||
|
|
{att8Anchor != null && (
|
|||
|
|
<span className={s.small}>England average {att8Anchor.toFixed(1)}</span>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<span className={s.small}>No data</span>
|
|||
|
|
)}
|
|||
|
|
</Cell>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
<RowLabel tip="DfE's own plain-English Progress 8 label.">Progress 8</RowLabel>
|
|||
|
|
{schools.map((school, i) => (
|
|||
|
|
<Cell key={school.urn} school={school} index={i}>
|
|||
|
|
{banding[i] ? (
|
|||
|
|
<Chip
|
|||
|
|
tone={
|
|||
|
|
/well above|above/i.test(banding[i] as string)
|
|||
|
|
? 'good'
|
|||
|
|
: /well below|below/i.test(banding[i] as string)
|
|||
|
|
? 'warn'
|
|||
|
|
: 'neutral'
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
{banding[i]}
|
|||
|
|
</Chip>
|
|||
|
|
) : (
|
|||
|
|
<span className={s.small}>No data</span>
|
|||
|
|
)}
|
|||
|
|
</Cell>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
<RowLabel tip="% achieving grade 5 or above in both English and maths GCSEs.">
|
|||
|
|
Grade 5+ in English & maths
|
|||
|
|
</RowLabel>
|
|||
|
|
{schools.map((school, i) => (
|
|||
|
|
<Cell key={school.urn} school={school} index={i}>
|
|||
|
|
{grade5[i] != null ? `${Math.round(grade5[i] as number)}%` : <span className={s.small}>No data</span>}
|
|||
|
|
</Cell>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
<RowLabel tip="% entering the English Baccalaureate subject combination.">EBacc entry</RowLabel>
|
|||
|
|
{schools.map((school, i) => (
|
|||
|
|
<Cell key={school.urn} school={school} index={i}>
|
|||
|
|
{ebacc[i] != null ? `${Math.round(ebacc[i] as number)}%` : <span className={s.small}>No data</span>}
|
|||
|
|
</Cell>
|
|||
|
|
))}
|
|||
|
|
</SectionGrid>
|
|||
|
|
</Section>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const national = nationalAverages?.primary;
|
|||
|
|
const disadvantaged = latestValues(data, urns, 'rwm_expected_disadvantaged_pct');
|
|||
|
|
const disadvantagedAnchor = benchmarks?.primary?.disadvantaged_rwm_expected_pct ?? null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Section
|
|||
|
|
title="How children do academically"
|
|||
|
|
how="Results from national tests and teacher assessments at the end of Year 6 — writing is assessed by teachers, not tested. Each line runs from 0–100%; the grey tick marks the England average, so dots to its right are above average."
|
|||
|
|
>
|
|||
|
|
<div className={s.card}>
|
|||
|
|
{TIER1_PRIMARY.map((spec) => (
|
|||
|
|
<Strip
|
|||
|
|
key={spec.metric}
|
|||
|
|
spec={spec}
|
|||
|
|
data={data}
|
|||
|
|
urns={urns}
|
|||
|
|
schoolNames={schoolNames}
|
|||
|
|
national={national}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
<details className={styles.moreMeasures}>
|
|||
|
|
<summary>More measures — grammar, punctuation & spelling, science, average scaled scores</summary>
|
|||
|
|
{TIER2_PRIMARY.map((spec) => (
|
|||
|
|
<Strip
|
|||
|
|
key={spec.metric}
|
|||
|
|
spec={spec}
|
|||
|
|
data={data}
|
|||
|
|
urns={urns}
|
|||
|
|
schoolNames={schoolNames}
|
|||
|
|
national={national}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
<p className={styles.stripNote}>
|
|||
|
|
The scaled-score strips show the 100–120 window of the full 80–120 range; 100 is the
|
|||
|
|
expected standard. Where an England tick is missing, the official figure isn't in
|
|||
|
|
our dataset yet.
|
|||
|
|
</p>
|
|||
|
|
</details>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{disadvantaged.some((v) => v != null) && (
|
|||
|
|
<SectionGrid schools={schools}>
|
|||
|
|
<RowLabel tip="% of disadvantaged pupils (free school meals in the last 6 years, or looked after by the local authority) reaching the expected standard. Benchmark computed across state schools in our dataset. Based on smaller pupil groups, so a single pupil can move a school's figure noticeably.">
|
|||
|
|
Children from lower-income families
|
|||
|
|
</RowLabel>
|
|||
|
|
{schools.map((school, i) => {
|
|||
|
|
const value = disadvantaged[i];
|
|||
|
|
return (
|
|||
|
|
<Cell key={school.urn} school={school} index={i}>
|
|||
|
|
{value != null ? (
|
|||
|
|
<>
|
|||
|
|
<span className={s.big} style={{ fontSize: '1.1rem' }}>
|
|||
|
|
{Math.round(value)}%
|
|||
|
|
</span>{' '}
|
|||
|
|
{disadvantagedAnchor != null && (
|
|||
|
|
<Chip tone={verdict(value, disadvantagedAnchor, 5) === 'below' ? 'warn' : 'good'}>
|
|||
|
|
{verdict(value, disadvantagedAnchor, 5) === 'above' &&
|
|||
|
|
`Well above the ${Math.round(disadvantagedAnchor)}% state-school average`}
|
|||
|
|
{verdict(value, disadvantagedAnchor, 5) === 'close' &&
|
|||
|
|
`Around the ${Math.round(disadvantagedAnchor)}% state-school average`}
|
|||
|
|
{verdict(value, disadvantagedAnchor, 5) === 'below' &&
|
|||
|
|
`Below the ${Math.round(disadvantagedAnchor)}% state-school average`}
|
|||
|
|
</Chip>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<span className={s.small}>No data</span>
|
|||
|
|
)}
|
|||
|
|
</Cell>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</SectionGrid>
|
|||
|
|
)}
|
|||
|
|
</Section>
|
|||
|
|
);
|
|||
|
|
}
|