feat(compare): academics strips with England anchors and More measures
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user