copy(web): replace em dashes in public copy #155

Merged
tudor merged 1 commits from copy/remove-em-dashes into main 2026-09-24 21:11:15 +00:00
39 changed files with 183 additions and 111 deletions

No files matched your search

@@ -104,7 +104,7 @@ describe('CompareAdmissions', () => {
render(<CompareAdmissions schools={[grammar]} data={data} isSecondary={true} />); render(<CompareAdmissions schools={[grammar]} data={data} isSecondary={true} />);
expect( expect(
screen.getByText(/Entry is by entrance test — the school is selective/), screen.getByText(/Entry is by entrance test\. The school is selective/),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.queryByText(/non-faith primaries/)).toBeNull(); expect(screen.queryByText(/non-faith primaries/)).toBeNull();
}); });
@@ -102,7 +102,7 @@ describe('CutoffMapPanel', () => {
// Explanation is supporting text, not part of the bold verdict line. // Explanation is supporting text, not part of the bold verdict line.
expect(result.querySelector('[class*="cutoffCheckHeadline"]')!.textContent) expect(result.querySelector('[class*="cutoffCheckHeadline"]')!.textContent)
.not.toMatch(/measurement error/); .not.toMatch(/measurement error/);
expect(result).not.toHaveTextContent(/^\S+ away — inside/); expect(result).not.toHaveTextContent(/^\S+ away, inside/);
}); });
it('surfaces a postcode the geocoder cannot find', async () => { it('surfaces a postcode the geocoder cannot find', async () => {
@@ -0,0 +1,72 @@
import fs from 'fs';
import path from 'path';
import ts from 'typescript';
/**
* Keeps the em dash out of public copy.
*
* Every visitor-facing sentence was rewritten to use a colon, comma, full stop
* or parentheses instead. A new one slips back in unnoticed, because it reads
* fine to whoever wrote it, so this walks the source with the TypeScript parser
* and checks every string literal, template chunk and JSX text node. Comments
* are not nodes the walk visits, so they stay free to use it.
*
* Two uses are allowed:
* - a lone dash standing in for a missing value in a table cell or stat slot,
* which is a data convention rather than prose;
* - the message of a thrown Error, which only a developer reads.
*/
const ROOT = path.join(__dirname, '..', '..');
const SCANNED = ['app', 'components', 'lib'];
const SKIPPED = [path.join(ROOT, 'app', '(payload)')];
function sources(dir: string): string[] {
if (SKIPPED.includes(dir)) return [];
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return sources(full);
return /\.tsx?$/.test(entry.name) ? [full] : [];
});
}
function isErrorMessage(node: ts.Node): boolean {
for (let p = node.parent; p; p = p.parent) {
if (ts.isNewExpression(p) && /Error$/.test(p.expression.getText())) return true;
if (ts.isStatement(p)) return false;
}
return false;
}
const EMPTY_VALUE = /^(—|&mdash;)$/;
function dashedCopy(file: string): string[] {
const text = fs.readFileSync(file, 'utf8');
const kind = file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
const found: string[] = [];
const visit = (node: ts.Node) => {
if (
ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isJsxText(node)
|| ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)
) {
const copy = node.getText().replace(/\s+/g, ' ').trim();
const bare = node.text.trim();
if (/—|&mdash;/.test(copy) && !EMPTY_VALUE.test(bare) && !isErrorMessage(node)) {
const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
found.push(`${path.relative(ROOT, file)}:${line + 1} ${copy}`);
}
}
ts.forEachChild(node, visit);
};
visit(sf);
return found;
}
describe('public copy', () => {
it('uses no em dashes', () => {
const files = SCANNED.flatMap((dir) => sources(path.join(ROOT, dir)));
expect(files.length).toBeGreaterThan(50);
expect(files.flatMap(dashedCopy)).toEqual([]);
});
});
@@ -108,7 +108,7 @@ describe('all-through schools', () => {
it('labels the KS2 block explicitly', () => { it('labels the KS2 block explicitly', () => {
renderSchoolDetail(allThroughFixture); renderSchoolDetail(allThroughFixture);
expect(screen.getAllByText(/Primary — KS2 SATs/).length).toBeGreaterThan(0); expect(screen.getAllByText(/Primary: KS2 SATs/).length).toBeGreaterThan(0);
}); });
}); });
@@ -122,7 +122,7 @@ describe('compareToCutoff', () => {
it('reads back the reported case in one unit', () => { it('reads back the reported case in one unit', () => {
expect(compareToCutoff(69, 270, 2026).headline) expect(compareToCutoff(69, 270, 2026).headline)
.toBe('0.04 miles away — inside the September 2026 cut-off of 0.17 miles.'); .toBe('0.04 miles away, inside the September 2026 cut-off of 0.17 miles.');
}); });
it('calls a clearly nearer home inside, and names the year', () => { it('calls a clearly nearer home inside, and names the year', () => {
+1 -1
View File
@@ -33,7 +33,7 @@ export async function generateMetadata(
// Deliberately not the homepage's phrase. Two pages chasing "compare // Deliberately not the homepage's phrase. Two pages chasing "compare
// schools" is how a site competes with itself; this one takes the tool // schools" is how a site competes with itself; this one takes the tool
// phrasing instead. // phrasing instead.
title: 'School Comparison Tool — Up to Five at Once | schoolcompare', title: 'School Comparison Tool: Up to Five Schools at Once | schoolcompare',
description: description:
'Put up to five English schools in one table: SATs and GCSE results against the England average, Ofsted grades, and the distance places were offered.', 'Put up to five English schools in one table: SATs and GCSE results against the England average, Ofsted grades, and the distance places were offered.',
keywords: keywords:
+3 -3
View File
@@ -54,7 +54,7 @@ export const metadata: Metadata = {
template: '%s | schoolcompare', template: '%s | schoolcompare',
}, },
description: description:
'Put five English schools on one screen — SATs, GCSE results, Ofsted grades, and how close you had to live to get a place. Free, no sign-up.', 'Put five English schools on one screen: SATs, GCSE results, Ofsted grades, and how close you had to live to get a place. Free, no sign-up.',
keywords: 'school comparison, KS2 results, KS4 results, primary school, secondary school, England schools, SATs results, GCSE results', keywords: 'school comparison, KS2 results, KS4 results, primary school, secondary school, England schools, SATs results, GCSE results',
authors: [{ name: 'schoolcompare' }], authors: [{ name: 'schoolcompare' }],
manifest: '/manifest.json', manifest: '/manifest.json',
@@ -69,7 +69,7 @@ export const metadata: Metadata = {
type: 'website', type: 'website',
title: 'Compare Schools Side by Side | schoolcompare', title: 'Compare Schools Side by Side | schoolcompare',
description: description:
'Put five English schools on one screen — SATs, GCSE results, Ofsted grades, and how close you had to live to get a place.', 'Put five English schools on one screen: SATs, GCSE results, Ofsted grades, and how close you had to live to get a place.',
/* /*
* Declared, not inherited. * Declared, not inherited.
* *
@@ -93,7 +93,7 @@ export const metadata: Metadata = {
card: 'summary_large_image', card: 'summary_large_image',
title: 'Compare Schools Side by Side | schoolcompare', title: 'Compare Schools Side by Side | schoolcompare',
description: description:
'Put five English schools on one screen — SATs, GCSE results, Ofsted grades, and how close you had to live to get a place.', 'Put five English schools on one screen: SATs, GCSE results, Ofsted grades, and how close you had to live to get a place.',
// The card is summary_large_image; claiming that and supplying no image // The card is summary_large_image; claiming that and supplying no image
// is worse than claiming a summary card. // is worse than claiming a summary card.
images: ['/opengraph-image'], images: ['/opengraph-image'],
+1 -1
View File
@@ -49,7 +49,7 @@ export const metadata: Metadata = {
*/ */
title: { absolute: 'Compare Schools Side by Side | schoolcompare' }, title: { absolute: 'Compare Schools Side by Side | schoolcompare' },
description: description:
'Put five English schools on one screen — SATs, GCSE results, Ofsted grades, and how close you had to live to get a place. Free, no sign-up.', 'Put five English schools on one screen: SATs, GCSE results, Ofsted grades, and how close you had to live to get a place. Free, no sign-up.',
// This page reads eleven search params. They filter a result set; they do // This page reads eleven search params. They filter a result set; they do
// not make a new document. Collapsing every combination onto "/" stops the // not make a new document. Collapsing every combination onto "/" stops the
// homepage competing with itself for its own head terms. // homepage competing with itself for its own head terms.
@@ -56,7 +56,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
// absolute: the root layout's template appends '| schoolcompare' to a // absolute: the root layout's template appends '| schoolcompare' to a
// plain string, and this title already carries it. Without this every // plain string, and this title already carries it. Without this every
// place title read '... | schoolcompare | schoolcompare'. // place title read '... | schoolcompare | schoolcompare'.
title: { absolute: `Schools in ${name} — Compare ${count} Schools | schoolcompare` }, title: { absolute: `Schools in ${name}: Compare ${count} Schools | schoolcompare` },
description: description:
`Every school in ${name}, with SATs and GCSE results, Ofsted grades, the local ` `Every school in ${name}, with SATs and GCSE results, Ofsted grades, the local `
+ `average against England, and how close you had to live to get a place.`, + `average against England, and how close you had to live to get a place.`,
@@ -41,7 +41,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
// "Local Authority" stays in the title for the same reason it is on the // "Local Authority" stays in the title for the same reason it is on the
// bare authority page: 67 town names collide with an authority name, and // bare authority page: 67 town names collide with an authority name, and
// a reader landing on both needs to know which set each covers. // a reader landing on both needs to know which set each covers.
title: { absolute: `${word} Schools in ${name} — Local Authority | schoolcompare` }, title: { absolute: `${word} Schools in ${name} (Local Authority) | schoolcompare` },
description: description:
`Every ${phase} school in the ${name} local authority, with results, Ofsted ` `Every ${phase} school in the ${name} local authority, with results, Ofsted `
+ `grades and the authority average against England.`, + `grades and the authority average against England.`,
@@ -43,7 +43,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { name, count } = detail.place; const { name, count } = detail.place;
return { return {
title: { absolute: `Schools in ${name} — Local Authority | schoolcompare` }, title: { absolute: `Schools in ${name} (Local Authority) | schoolcompare` },
description: description:
`All ${count} schools in the ${name} local authority, with SATs and GCSE results, ` `All ${count} schools in the ${name} local authority, with SATs and GCSE results, `
+ `Ofsted grades and the authority average against England.`, + `Ofsted grades and the authority average against England.`,
+1 -1
View File
@@ -11,7 +11,7 @@ import { ImageResponse } from 'next/og';
* and a suggestion of the illustration's rolling landscape along the bottom. * and a suggestion of the illustration's rolling landscape along the bottom.
* It states the promise rather than decorating it. * It states the promise rather than decorating it.
*/ */
export const alt = 'schoolcompare — find the school where your child will flourish'; export const alt = 'schoolcompare: find the school where your child will flourish';
export const size = { width: 1200, height: 630 }; export const size = { width: 1200, height: 630 };
export const contentType = 'image/png'; export const contentType = 'image/png';
+8 -8
View File
@@ -64,11 +64,11 @@ interface Step {
const SECONDARY_STEPS: Step[] = [ const SECONDARY_STEPS: Step[] = [
{ {
title: 'Check entry criteria', title: 'Check entry criteria',
body: 'Look at each school\'s admissions policy — catchment areas, faith criteria, sibling priority, and aptitude tests vary widely.', body: 'Look at each school\'s admissions policy. Catchment areas, faith criteria, sibling priority and aptitude tests vary widely.',
callouts: [ callouts: [
{ {
title: 'Selective schools register earlier', title: 'Selective schools register earlier',
body: 'Grammar and other selective schools need separate entrance-test registration months earlier — often by September, well before the 31 October deadline.', body: 'Grammar and other selective schools need separate entrance-test registration months earlier, often by September and well before the 31 October deadline.',
}, },
{ {
title: 'Faith schools may need an extra form', title: 'Faith schools may need an extra form',
@@ -86,26 +86,26 @@ const SECONDARY_STEPS: Step[] = [
{ {
date: '31 October', date: '31 October',
title: 'Application deadline', title: 'Application deadline',
body: 'Submit your ranked list of schools — most councils allow three to six preferences (London boroughs typically six). Councils treat all preferences equally, so list schools in the genuine order you want them, not strategically.', body: 'Submit your ranked list of schools. Most councils allow three to six preferences (London boroughs typically six). Councils treat all preferences equally, so list schools in the genuine order you want them, not strategically.',
highlight: 'deadline', highlight: 'deadline',
tool: { label: 'Compare performance to order your preferences', href: '/rankings' }, tool: { label: 'Compare performance to order your preferences', href: '/rankings' },
}, },
{ {
date: '1 March', date: '1 March',
title: 'National Offer Day', title: 'National Offer Day',
body: 'Your allocated school appears on the council\'s online admissions portal and is sent by email. Release times are set by each council — often from late afternoon, though some publish overnight — so check your council\'s page rather than refreshing at midnight.', body: 'Your allocated school appears on the council\'s online admissions portal and is sent by email. Release times are set by each council (often from late afternoon, though some publish overnight), so check your council\'s page rather than refreshing at midnight.',
highlight: 'offer', highlight: 'offer',
tool: { label: 'Look up your allocated school', href: '/' }, tool: { label: 'Look up your allocated school', href: '/' },
}, },
{ {
date: '~15 March', date: '~15 March',
title: 'Accept or decline', title: 'Accept or decline',
body: 'Respond by the deadline your council gives — typically around 15 March. Accepting does not prevent you from keeping a place on a waiting list for a preferred school.', body: 'Respond by the deadline your council gives, typically around 15 March. Accepting does not prevent you from keeping a place on a waiting list for a preferred school.',
tool: { label: 'Weigh your offer against your other choices', href: '/compare' }, tool: { label: 'Weigh your offer against your other choices', href: '/compare' },
}, },
{ {
title: 'Appeals', title: 'Appeals',
body: 'If unsuccessful, you can appeal within 20 school days of the refusal letter. Secondary appeals consider whether prejudice to the school outweighs your case — success rates vary.', body: 'If unsuccessful, you can appeal within 20 school days of the refusal letter. Secondary appeals consider whether prejudice to the school outweighs your case, and success rates vary.',
tool: { label: 'Gather performance evidence for your case', href: '/rankings' }, tool: { label: 'Gather performance evidence for your case', href: '/rankings' },
}, },
]; ];
@@ -163,7 +163,7 @@ interface Tip {
const TIPS: Tip[] = [ const TIPS: Tip[] = [
{ {
heading: 'Equal preference rule', heading: 'Equal preference rule',
body: 'Councils consider every school you list before making any offer, then give you the highest-ranked one you qualify for. Your order only decides which qualifying school you get — you can\'t game it, so list schools in the order you genuinely want them.', body: 'Councils consider every school you list before making any offer, then give you the highest-ranked one you qualify for. Your order only decides which qualifying school you get. You can\'t game it, so list schools in the order you genuinely want them.',
}, },
{ {
heading: 'Late applications go to the back', heading: 'Late applications go to the back',
@@ -259,7 +259,7 @@ export function AdmissionsView() {
</span> </span>
<h1 className={styles.heroTitle}>School Admissions Guide</h1> <h1 className={styles.heroTitle}>School Admissions Guide</h1>
<p className={styles.heroSub}> <p className={styles.heroSub}>
Everything parents need to know about applying for a school place in England — from opening dates to National Offer Day, with live countdowns to every key milestone. Everything parents need to know about applying for a school place in England, from opening dates to National Offer Day, with live countdowns to every key milestone.
</p> </p>
</section> </section>
+3 -3
View File
@@ -258,15 +258,15 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
// published by DfE but aren't in our dataset yet. // published by DfE but aren't in our dataset yet.
<p className={styles.chartNote}> <p className={styles.chartNote}>
School-level GCSE figures for 2019/20 and 2020/21 weren&apos;t published (COVID School-level GCSE figures for 2019/20 and 2020/21 weren&apos;t published (COVID
grading), and more recent years aren&apos;t in our dataset yet where lines break — the grading), and more recent years aren&apos;t in our dataset yet. Where lines break, the
England average is shown where available. England average is shown if available.
</p> </p>
) : ( ) : (
!isSecondary && !isSecondary &&
built.showUnpublished202122Note && ( built.showUnpublished202122Note && (
<p className={styles.chartNote}> <p className={styles.chartNote}>
No national tests were held in 2019/20 and 2020/21 (COVID), and DfE didn&apos;t publish No national tests were held in 2019/20 and 2020/21 (COVID), and DfE didn&apos;t publish
school-level figures for 2021/22 — the England average is shown for that year. school-level figures for 2021/22. The England average is shown for that year.
</p> </p>
) )
)} )}
+3 -3
View File
@@ -263,7 +263,7 @@ export function ComparisonView({
<header className={styles.header}> <header className={styles.header}>
<h1>Compare Schools</h1> <h1>Compare Schools</h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Add schools to your comparison basket to see them side by side — inspection results, Add schools to your comparison basket to see them side by side: inspection results,
academics, admissions and community. academics, admissions and community.
</p> </p>
</header> </header>
@@ -300,8 +300,8 @@ export function ComparisonView({
<div> <div>
<h1>Compare Schools</h1> <h1>Compare Schools</h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
{selectedSchools.length} school{selectedSchools.length !== 1 ? 's' : ''} side by side {selectedSchools.length} school{selectedSchools.length !== 1 ? 's' : ''} side by side,
— each number anchored against the England average so you can tell at a glance each number anchored against the England average so you can tell at a glance
what&apos;s typical and what stands out. what&apos;s typical and what stands out.
</p> </p>
</div> </div>
+1 -1
View File
@@ -47,7 +47,7 @@ export function EditorialSection({
<p> <p>
School performance data in England is rich but fragmented. The Department for Education and Ofsted School performance data in England is rich but fragmented. The Department for Education and Ofsted
publish Key Stage 2 SATs, GCSE attainment, inspection outcomes, progress scores, admissions figures publish Key Stage 2 SATs, GCSE attainment, inspection outcomes, progress scores, admissions figures
and demographics — each in its own table, each with its own jargon. and demographics, each in its own table with its own jargon.
</p> </p>
<p> <p>
schoolcompare brings it all into one place. Every school page shows performance against the national schoolcompare brings it all into one place. Every school page shows performance against the national
+1 -1
View File
@@ -323,7 +323,7 @@ export function FilterBar({
{isHero && ( {isHero && (
<> <>
<p className={styles.searchHint}> <p className={styles.searchHint}>
Search by <strong>school name</strong> — or use your{" "} Search by <strong>school name</strong>, or use your{" "}
<strong>postcode</strong> for the nearest schools. <strong>postcode</strong> for the nearest schools.
</p> </p>
{onNearMe && ( {onNearMe && (
+3 -3
View File
@@ -185,7 +185,7 @@ const VALUE_PROPS: ValueProp[] = [
icon: <HeartIcon />, icon: <HeartIcon />,
tintClass: styles.propIconContext, tintClass: styles.propIconContext,
title: 'Beyond the numbers', title: 'Beyond the numbers',
body: 'SEN support, pupil premium and attendance — not just results.', body: 'Not just results: SEN support, pupil premium and attendance too.',
}, },
{ {
icon: <CalendarIcon />, icon: <CalendarIcon />,
@@ -478,8 +478,8 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
every line above the fold costs. */} every line above the fold costs. */}
<span className={styles.heroDescriptionFull}> <span className={styles.heroDescriptionFull}>
{' '}Key Stage 2 SATs, GCSE results, Ofsted grades, progress scores {' '}Key Stage 2 SATs, GCSE results, Ofsted grades, progress scores
and admissions data{coverageLabel && <> for <strong>{coverageLabel} schools</strong></>} — side by and admissions data{coverageLabel && <> for <strong>{coverageLabel} schools</strong></>}, side by
side, in one place. side in one place.
</span> </span>
</p> </p>
+3 -3
View File
@@ -47,7 +47,7 @@ export function HowItWorksSection() {
<div className={styles.sectionHead}> <div className={styles.sectionHead}>
<p className={styles.sectionKicker}>Every school page</p> <p className={styles.sectionKicker}>Every school page</p>
<h2 className={styles.sectionHeading}>What you&apos;ll see on every school</h2> <h2 className={styles.sectionHeading}>What you&apos;ll see on every school</h2>
<p className={styles.sectionAside}>Primary or secondary — the page adapts to the phase</p> <p className={styles.sectionAside}>The page adapts to primary or secondary</p>
</div> </div>
<div className={styles.hiwGrid}> <div className={styles.hiwGrid}>
{/* Card 1 — Performance */} {/* Card 1 — Performance */}
@@ -123,7 +123,7 @@ export function HowItWorksSection() {
<div className={styles.hiwCardBody}> <div className={styles.hiwCardBody}>
<div className={styles.hiwStep}>Judgement</div> <div className={styles.hiwStep}>Judgement</div>
<div className={styles.hiwTitle}>Ofsted at a glance</div> <div className={styles.hiwTitle}>Ofsted at a glance</div>
<p className={styles.hiwDesc}>Since November 2025, Ofsted rates each area of school life on a five-point scale rather than one overall word. We show every area and the inspection date — and, for schools not yet reinspected, the older overall grade they still carry.</p> <p className={styles.hiwDesc}>Since November 2025, Ofsted rates each area of school life on a five-point scale rather than one overall word. We show every area, the inspection date and, for schools not yet reinspected, the older overall grade they still carry.</p>
</div> </div>
</div> </div>
@@ -152,7 +152,7 @@ export function HowItWorksSection() {
{/* One verb for one feature. The site previously called this {/* One verb for one feature. The site previously called this
"compare" in the nav, "shortlist" in the footer and "pin" here, "compare" in the nav, "shortlist" in the footer and "pin" here,
which reads as three separate things it does not have. */} which reads as three separate things it does not have. */}
<p className={styles.hiwDesc}>Compare up to five schools and every metric aligns in the same columns — works for primary and secondary alike.</p> <p className={styles.hiwDesc}>Compare up to five schools and every metric aligns in the same columns. It works for primary and secondary alike.</p>
</div> </div>
</div> </div>
</div> </div>
+4 -4
View File
@@ -114,7 +114,7 @@ export function PerformanceChart({
const delta = latest.rwm_expected_pct! - prev.rwm_expected_pct!; const delta = latest.rwm_expected_pct! - prev.rwm_expected_pct!;
const arrow = delta > 1 ? '↑' : delta < -1 ? '↓' : '→'; const arrow = delta > 1 ? '↑' : delta < -1 ? '↓' : '→';
if (best.year === latest.year) { if (best.year === latest.year) {
return `${arrow} Best year on record — ${latestPct}% met the expected standard in Reading, Writing & Maths`; return `${arrow} Best year on record: ${latestPct}% met the expected standard in Reading, Writing & Maths`;
} }
return `${arrow} Reading, Writing & Maths peaked at ${bestPct}% (${formatAcademicYear(best.year)}), currently ${latestPct}%`; return `${arrow} Reading, Writing & Maths peaked at ${bestPct}% (${formatAcademicYear(best.year)}), currently ${latestPct}%`;
})(); })();
@@ -451,15 +451,15 @@ export function PerformanceChart({
{hasGap && ( {hasGap && (
<p className={styles.covidNote}> <p className={styles.covidNote}>
{isSecondary {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." ? "School-level GCSE figures for 2019/20 and 2020/21 weren't published (COVID grading). Where the line breaks, the England average is shown if 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."} : "No national tests were held in 2019/20 and 2020/21 (COVID), and DfE didn't publish school-level figures for 2021/22. Where available, the England average is shown for those years."}
</p> </p>
)} )}
{/* Desktop-only hint about toggling progress in the legend */} {/* Desktop-only hint about toggling progress in the legend */}
{!isSecondary && ( {!isSecondary && (
<p className={styles.chartHint}> <p className={styles.chartHint}>
Progress scores (Reading, Writing, Maths) are hidden by default — click them in the legend to show. Progress scores (Reading, Writing, Maths) are hidden by default. Click them in the legend to show them.
</p> </p>
)} )}
</div> </div>
+2 -2
View File
@@ -119,7 +119,7 @@ export function RankingsView({
<h1>School Rankings</h1> <h1>School Rankings</h1>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Schools ranked by {metricLabel.toLowerCase()} Schools ranked by {metricLabel.toLowerCase()}
{!selectedArea && rankings.length > 0 && <span className={styles.limitNote}> — showing top {rankings.length}</span>} {!selectedArea && rankings.length > 0 && <span className={styles.limitNote}> (showing top {rankings.length})</span>}
</p> </p>
</header> </header>
@@ -145,7 +145,7 @@ export function RankingsView({
{isProgressScore && ( {isProgressScore && (
<p className={styles.progressHint}> <p className={styles.progressHint}>
Progress scores: 0 = national average. Positive = above average. Progress scores: 0 = national average. Positive = above average.
{isPrimary && ' KS2 progress isn’t published for 2023/24 or 2024/25 (there’s no key stage 1 baseline) — pick an earlier year to rank by it.'} {isPrimary && ' KS2 progress isn’t published for 2023/24 or 2024/25 because there’s no key stage 1 baseline. Pick an earlier year to rank by it.'}
</p> </p>
)} )}
+2 -2
View File
@@ -33,14 +33,14 @@ export function SpecialSchoolNote({ school }: { school: { school_type?: string |
<> <>
<strong>This is a special school.</strong> Its pupils have special educational needs and <strong>This is a special school.</strong> Its pupils have special educational needs and
work towards individual targets. They sit the same national assessments, but very few work towards individual targets. They sit the same national assessments, but very few
reach the mainstream &ldquo;expected standard&rdquo; these measures report — so a reach the mainstream &ldquo;expected standard&rdquo; these measures report, so a
comparison with the England average isn&rsquo;t a meaningful guide to the school. comparison with the England average isn&rsquo;t a meaningful guide to the school.
</> </>
)} )}
{kind === 'pru' && ( {kind === 'pru' && (
<> <>
<strong>This is a pupil referral unit.</strong> It teaches pupils educated outside a <strong>This is a pupil referral unit.</strong> It teaches pupils educated outside a
mainstream school — for example after exclusion, or for medical or behavioural reasons. mainstream school, for example after exclusion or for medical or behavioural reasons.
The mainstream &ldquo;expected standard&rdquo; and the England-average comparison The mainstream &ldquo;expected standard&rdquo; and the England-average comparison
aren&rsquo;t a meaningful guide to the school. aren&rsquo;t a meaningful guide to the school.
</> </>
@@ -26,7 +26,7 @@ interface StripSpec {
const TIER1_PRIMARY: StripSpec[] = [ const TIER1_PRIMARY: StripSpec[] = [
{ {
label: 'Reading, writing & maths — expected standard', label: 'Reading, writing & maths: expected standard',
metric: 'rwm_expected_pct', metric: 'rwm_expected_pct',
anchorKey: 'rwm_expected_pct', anchorKey: 'rwm_expected_pct',
tip: '% of Year 6 pupils reaching the expected standard in reading, writing and maths.', tip: '% of Year 6 pupils reaching the expected standard in reading, writing and maths.',
@@ -49,18 +49,18 @@ const TIER1_PRIMARY: StripSpec[] = [
const TIER2_PRIMARY: StripSpec[] = [ const TIER2_PRIMARY: StripSpec[] = [
{ {
label: 'Grammar, punctuation & spelling — expected standard', label: 'Grammar, punctuation & spelling: expected standard',
metric: 'gps_expected_pct', metric: 'gps_expected_pct',
anchorKey: 'gps_expected_pct', anchorKey: 'gps_expected_pct',
}, },
{ {
label: 'Science — expected standard (teacher-assessed)', label: 'Science: expected standard (teacher-assessed)',
metric: 'science_expected_pct', metric: 'science_expected_pct',
anchorKey: '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.', tip: 'Teacher-assessed, like writing. There has been no KS2 science test since 2009, so comparisons are indicative.',
}, },
{ {
label: 'Average scaled score — reading', label: 'Average scaled score: reading',
metric: 'reading_avg_score', metric: 'reading_avg_score',
anchorKey: 'reading_avg_score', anchorKey: 'reading_avg_score',
min: 100, min: 100,
@@ -68,7 +68,7 @@ const TIER2_PRIMARY: StripSpec[] = [
unit: '', unit: '',
}, },
{ {
label: 'Average scaled score — maths', label: 'Average scaled score: maths',
metric: 'maths_avg_score', metric: 'maths_avg_score',
anchorKey: 'maths_avg_score', anchorKey: 'maths_avg_score',
min: 100, min: 100,
@@ -76,7 +76,7 @@ const TIER2_PRIMARY: StripSpec[] = [
unit: '', unit: '',
}, },
{ {
label: 'Average scaled score — grammar, punctuation & spelling', label: 'Average scaled score: grammar, punctuation & spelling',
metric: 'gps_avg_score', metric: 'gps_avg_score',
anchorKey: 'gps_avg_score', anchorKey: 'gps_avg_score',
min: 100, min: 100,
@@ -193,7 +193,7 @@ export function CompareAcademics({
return ( return (
<Section <Section
title="How students do academically" 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." 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}> <SectionGrid schools={schools}>
<RowLabel tip="Average Attainment 8 score across eight GCSE subjects.">Attainment 8</RowLabel> <RowLabel tip="Average Attainment 8 score across eight GCSE subjects.">Attainment 8</RowLabel>
@@ -230,7 +230,7 @@ export function CompareAcademics({
</Chip> </Chip>
) : p8NotPublished[i] ? ( ) : p8NotPublished[i] ? (
<span className={s.small}> <span className={s.small}>
Not published — this GCSE year group sat no KS2 tests (COVID), so DfE has no Not published: this GCSE year group sat no KS2 tests (COVID), so DfE has no
baseline to measure progress from baseline to measure progress from
</span> </span>
) : ( ) : (
@@ -308,7 +308,7 @@ export function CompareAcademics({
return ( return (
<Section <Section
title="How children do academically" 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." 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}> <div className={s.card}>
{TIER1_PRIMARY.map((spec) => ( {TIER1_PRIMARY.map((spec) => (
@@ -324,7 +324,7 @@ export function CompareAcademics({
))} ))}
<details className={styles.moreMeasures}> <details className={styles.moreMeasures}>
<summary>More measures — grammar, punctuation &amp; spelling, science, average scaled scores</summary> <summary>More measures: grammar, punctuation &amp; spelling, science, average scaled scores</summary>
{TIER2_PRIMARY.map((spec) => ( {TIER2_PRIMARY.map((spec) => (
<Strip <Strip
key={spec.metric} key={spec.metric}
@@ -49,17 +49,17 @@ export function CompareAdmissions({
how={ how={
<> <>
From the most recent admissions round ({entryLabel}). &quot;First choice&quot; means From the most recent admissions round ({entryLabel}). &quot;First choice&quot; means
families who ranked the school top of their application form — officially a &quot;first families who ranked the school top of their application form (officially a &quot;first
preference&quot;. Schools never see your ranking: places are decided only by the preference&quot;). Schools never see your ranking: places are decided only by the
school&apos;s admission criteria, so listing a school lower down never hurts your chances. school&apos;s admission criteria, so listing a school lower down never hurts your chances.
These are National Offer Day offers — waiting lists and appeals can change the final These are National Offer Day offers. Waiting lists and appeals can change the final
intake. intake.
</> </>
} }
> >
<SectionGrid schools={schools}> <SectionGrid schools={schools}>
<Measure <Measure
tip="How many application forms named the school at any preference rank — not the number of families competing head-to-head for a place." tip="How many application forms named the school at any preference rank. This is not the number of families competing head-to-head for a place."
label="Interest in the school" label="Interest in the school"
> >
{schools.map((school, i) => { {schools.map((school, i) => {
@@ -126,7 +126,7 @@ export function CompareAdmissions({
// Selective schools: the entrance test decides, whatever the // Selective schools: the entrance test decides, whatever the
// offer percentage looks like — never the distance template. // offer percentage looks like — never the distance template.
text = text =
'Entry is by entrance test — the school is selective; distance and preference rank don’t decide places.'; 'Entry is by entrance test. The school is selective, so distance and preference rank don’t decide places.';
} else if (summary.firstPrefPct >= 100) { } else if (summary.firstPrefPct >= 100) {
text = `Every family who put ${school.school_name} first got a place.`; text = `Every family who put ${school.school_name} first got a place.`;
} else if (summary.firstPrefPct >= 90) { } else if (summary.firstPrefPct >= 90) {
@@ -134,9 +134,9 @@ export function CompareAdmissions({
} else if (a?.oversubscribed) { } else if (a?.oversubscribed) {
text = isSecondary text = isSecondary
? faith ? faith
? 'More first-choice applications than places — check the school’s admission criteria (faith-based criteria may apply).' ? 'More first-choice applications than places. Check the school’s admission criteria (faith-based criteria may apply).'
: 'More first-choice applications than places — check the school’s admission criteria (catchment or distance often decides, but criteria vary).' : 'More first-choice applications than places. Check the school’s admission criteria (catchment or distance often decides, but criteria vary).'
: 'More first-choice applications than places — check the school’s admission criteria (for most non-faith primaries, distance decides).'; : 'More first-choice applications than places. Check the school’s admission criteria (for most non-faith primaries, distance decides).';
} else { } else {
text = `${summary.firstPrefPct}% of first-choice families received an offer.`; text = `${summary.firstPrefPct}% of first-choice families received an offer.`;
} }
@@ -70,7 +70,7 @@ export function CompareAtAGlance({
: benchmarks?.primary?.median_pupils; : benchmarks?.primary?.median_pupils;
return ( return (
<Section title="At a glance" how="The short version — each row below is explained in its own section further down."> <Section title="At a glance" how="The short version. Each row below is explained in its own section further down.">
<SectionGrid schools={schools}> <SectionGrid schools={schools}>
<Measure label="Latest Ofsted inspection"> <Measure label="Latest Ofsted inspection">
{schools.map((school, i) => { {schools.map((school, i) => {
@@ -56,12 +56,12 @@ export function CompareCommunity({
how={ how={
<> <>
The school&apos;s community, from the latest school census. State-school averages are The school&apos;s community, from the latest school census. State-school averages are
computed from our dataset and shown for context — there&apos;s no &ldquo;right&rdquo; computed from our dataset and shown for context. There&apos;s no &ldquo;right&rdquo;
number here. number here.
{anyAllThrough && ( {anyAllThrough && (
<> <>
{' '} {' '}
For all-through schools these figures cover the whole school, all ages — not just For all-through schools these figures cover the whole school and all ages, not just
the {isSecondary ? 'secondary' : 'primary'} phase. the {isSecondary ? 'secondary' : 'primary'} phase.
</> </>
)} )}
@@ -79,7 +79,7 @@ export function CompareCommunity({
if (pupils != null && capacity != null && capacity > 0) { if (pupils != null && capacity != null && capacity > 0) {
capNote = capNote =
pupils >= capacity pupils >= capacity
? `${capacity.toLocaleString('en-GB')} places — at or above capacity` ? `${capacity.toLocaleString('en-GB')} places (at or above capacity)`
: `of ${capacity.toLocaleString('en-GB')} places (${Math.round((pupils / capacity) * 100)}% full)`; : `of ${capacity.toLocaleString('en-GB')} places (${Math.round((pupils / capacity) * 100)}% full)`;
} }
return ( return (
@@ -111,7 +111,7 @@ export function CompareCommunity({
</Measure> </Measure>
<Measure <Measure
tip="% of pupils eligible for free school meals — a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset." tip="% of pupils eligible for free school meals, a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset."
label="Free school meals" label="Free school meals"
> >
{schools.map((school, i) => { {schools.map((school, i) => {
@@ -145,7 +145,7 @@ export function CompareCommunity({
</Measure> </Measure>
<Measure <Measure
tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision — often a strength, not a warning sign. State-school average computed from our dataset." tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision, which is often a strength rather than a warning sign. State-school average computed from our dataset."
label="Extra learning support (SEN)" label="Extra learning support (SEN)"
> >
{schools.map((school, i) => { {schools.map((school, i) => {
@@ -47,7 +47,7 @@ function ResultCell({ display }: { display: OfstedDisplay }) {
return ( return (
<> <>
<strong style={{ fontSize: '0.9rem' }}>Report card</strong> <strong style={{ fontSize: '0.9rem' }}>Report card</strong>
<span className={s.small}>New-style inspection — no overall grade is given</span> <span className={s.small}>New-style inspection: no overall grade is given</span>
</> </>
); );
} }
@@ -127,7 +127,7 @@ function JudgementDetailCell({
if (published.length === 0) { if (published.length === 0) {
return ( return (
<span className={s.small}> <span className={s.small}>
We don&apos;t hold area-by-area detail for this inspection — see {schoolName}&apos;s We don&apos;t hold area-by-area detail for this inspection. See {schoolName}&apos;s
Ofsted page for the full report. Ofsted page for the full report.
</span> </span>
); );
@@ -39,7 +39,7 @@ export function AddToCompareButton({ school }: { school: NearbySchool }) {
aria-pressed={selected} aria-pressed={selected}
> >
{selected ? '✓ Added to compare' : '+ Add to compare'} {selected ? '✓ Added to compare' : '+ Add to compare'}
<span className={styles.srOnly}> — {school.school_name}</span> <span className={styles.srOnly}>: {school.school_name}</span>
</button> </button>
); );
} }
@@ -175,7 +175,7 @@ export function AdmissionsSection({
<><strong>{admissions.first_preference_applications}</strong> families put it first for </> <><strong>{admissions.first_preference_applications}</strong> families put it first for </>
)} )}
{admissions.places_offered != null && <><strong>{admissions.places_offered}</strong> places</>} {admissions.places_offered != null && <><strong>{admissions.places_offered}</strong> places</>}
{admissions.total_applications != null && ` — ${admissions.total_applications.toLocaleString()} applications in total`}. {admissions.total_applications != null && ` (${admissions.total_applications.toLocaleString()} applications in total)`}.
</p> </p>
</> </>
); );
@@ -189,7 +189,7 @@ export function DestinationsView({
data-group={card ?? 'none'} data-group={card ?? 'none'}
className={`${styles.segment} ${styles[seg.category]} ${dimmed(card) ? styles.dim : ''}`} className={`${styles.segment} ${styles[seg.category]} ${dimmed(card) ? styles.dim : ''}`}
style={{ width: `${seg.widthPct}%` }} style={{ width: `${seg.widthPct}%` }}
title={`${CATEGORY_LABELS[seg.category]} — ${seg.labelPct}% (${seg.pupils} pupils)`} title={`${CATEGORY_LABELS[seg.category]}: ${seg.labelPct}% (${seg.pupils} pupils)`}
> >
{seg.widthPct >= 9 ? `${seg.labelPct}%` : ''} {seg.widthPct >= 9 ? `${seg.labelPct}%` : ''}
</div> </div>
@@ -254,7 +254,7 @@ export function DestinationsView({
<p className={styles.footnote}> <p className={styles.footnote}>
Shares are rounded and may not add up to 100%. A pupil counted under a Shares are rounded and may not add up to 100%. A pupil counted under a
school sixth form may have moved to a different school&apos;s sixth school sixth form may have moved to a different school&apos;s sixth
form — the published data records the type of place, not which one. form. The published data records the type of place, not which one.
</p> </p>
</div> </div>
); );
+2 -2
View File
@@ -129,7 +129,7 @@ export function GcseSection({
that comparison isn't meaningful. */} that comparison isn't meaningful. */}
{!suppressComparison && latestResults.attainment_8_score != null && ( {!suppressComparison && latestResults.attainment_8_score != null && (
<div className={styles.att8Viz}> <div className={styles.att8Viz}>
<div className={styles.att8VizLabel}>Attainment 8 — school vs national</div> <div className={styles.att8VizLabel}>Attainment 8: school vs national</div>
<div className={styles.att8VizTrack}> <div className={styles.att8VizTrack}>
<div <div
className={styles.att8VizFill} className={styles.att8VizFill}
@@ -155,7 +155,7 @@ export function GcseSection({
{/* Progress 8 number line with CI */} {/* Progress 8 number line with CI */}
{latestResults.progress_8_score != null && !p8Suspended && ( {latestResults.progress_8_score != null && !p8Suspended && (
<div className={styles.p8Viz}> <div className={styles.p8Viz}>
<div className={styles.p8VizLabel}>Progress 8 — relative to national baseline (0)</div> <div className={styles.p8VizLabel}>Progress 8: relative to national baseline (0)</div>
{(() => { {(() => {
const p8 = latestResults.progress_8_score!; const p8 = latestResults.progress_8_score!;
const lo = latestResults.progress_8_lower_ci ?? p8; const lo = latestResults.progress_8_lower_ci ?? p8;
@@ -37,7 +37,7 @@ export function HistorySection({
<> <>
{hasKS2Results && ( {hasKS2Results && (
<> <>
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs</h3> <h3 className={styles.subSectionTitle}>Primary: KS2 SATs</h3>
<div className={styles.chartContainer}> <div className={styles.chartContainer}>
<PerformanceChart <PerformanceChart
data={yearlyData} data={yearlyData}
@@ -51,7 +51,7 @@ export function HistorySection({
)} )}
{hasKS4Results && ( {hasKS4Results && (
<> <>
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs</h3> <h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary: GCSEs</h3>
<div className={styles.chartContainer}> <div className={styles.chartContainer}>
<PerformanceChart <PerformanceChart
data={yearlyData} data={yearlyData}
@@ -114,14 +114,14 @@ export function OfstedSection({
</div> </div>
<p className={styles.ofstedDisclaimer}> <p className={styles.ofstedDisclaimer}>
{ofsted.grade_source === 'ungraded_carried_forward' {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.' ? '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.'
: isSecondary : isSecondary
? 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.' ? 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections.'
: 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'} : 'From September 2024, Ofsted no longer makes an overall effectiveness judgement in inspections of state-funded schools.'}
</p> </p>
{oeifAllSameGrade ? ( {oeifAllSameGrade ? (
<p className={styles.ofstedAllSame}> <p className={styles.ofstedAllSame}>
Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness!]}</strong> across all inspected areas — Quality of Teaching, Behaviour, Pupils&apos; Development and Leadership. Rated <strong>{OFSTED_LABELS[ofsted.overall_effectiveness!]}</strong> across all inspected areas: Quality of Teaching, Behaviour, Pupils&apos; Development and Leadership.
</p> </p>
) : ( ) : (
<div className={`${styles.metricsGrid} ${styles.gradeGrid}`}> <div className={`${styles.metricsGrid} ${styles.gradeGrid}`}>
@@ -41,7 +41,7 @@ export function ResultsSection({
? 'GCSE results for Year 11 pupils.' ? 'GCSE results for Year 11 pupils.'
: 'End-of-primary-school tests taken by Year 6 pupils.') : 'End-of-primary-school tests taken by Year 6 pupils.')
: isAllThrough : isAllThrough
? 'KS2 SATs (end of Year 6) and GCSE results (Year 11) — this school covers both. England averages shown for comparison.' ? 'This school covers both KS2 SATs (end of Year 6) and GCSE results (Year 11). England averages shown for comparison.'
: isSecondary : isSecondary
? 'GCSE results for Year 11 pupils. England averages shown for comparison.' ? '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.'} : 'End-of-primary-school tests taken by Year 6 pupils. England averages shown for comparison.'}
@@ -56,7 +56,7 @@ export function ResultsSection({
{hasKS2Results && ( {hasKS2Results && (
<> <>
{isAllThrough && ( {isAllThrough && (
<h3 className={styles.subSectionTitle}>Primary — KS2 SATs (Year 6)</h3> <h3 className={styles.subSectionTitle}>Primary: KS2 SATs (Year 6)</h3>
)} )}
<div className={styles.heroStatGrid}> <div className={styles.heroStatGrid}>
{latestResults.rwm_expected_pct !== null && ( {latestResults.rwm_expected_pct !== null && (
@@ -208,7 +208,7 @@ export function ResultsSection({
{hasKS4Results && ( {hasKS4Results && (
<> <>
{isAllThrough && ( {isAllThrough && (
<h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary — GCSEs (Year 11)</h3> <h3 className={styles.subSectionTitle} style={{ marginTop: '1.5rem' }}>Secondary: GCSEs (Year 11)</h3>
)} )}
<div className={styles.metricsGrid}> <div className={styles.metricsGrid}>
{latestResults.attainment_8_score !== null && ( {latestResults.attainment_8_score !== null && (
@@ -229,8 +229,8 @@ export function SchoolDetailShell({
</div> </div>
{isProposedToClose(schoolInfo) && ( {isProposedToClose(schoolInfo) && (
<div className={styles.closingStrip} role="note"> <div className={styles.closingStrip} role="note">
<strong>⚠ Proposed to close</strong> — this school is proposed for closure, <strong>⚠ Proposed to close.</strong> Check with the local authority before
check with the local authority before applying. applying.
</div> </div>
)} )}
{schoolInfo.address && ( {schoolInfo.address && (
@@ -46,10 +46,10 @@ export function SecondaryAdmissionsSection({
{admissionsTag && ( {admissionsTag && (
<div className={`${styles.admissionsTypeBadge} ${admissionsTag === 'Selective' ? styles.admissionsSelective : styles.admissionsFaith}`}> <div className={`${styles.admissionsTypeBadge} ${admissionsTag === 'Selective' ? styles.admissionsSelective : styles.admissionsFaith}`}>
<strong>{admissionsTag}</strong>{' '} <strong>{admissionsTag}:</strong>{' '}
{admissionsTag === 'Selective' {admissionsTag === 'Selective'
? '— Entry to this school is by selective examination (e.g. 11+).' ? 'entry to this school is by selective examination (e.g. 11+).'
: `— This school has a faith-based admissions priority (${schoolInfo.religious_denomination}).`} : `this school has a faith-based admissions priority (${schoolInfo.religious_denomination}).`}
</div> </div>
)} )}
@@ -32,7 +32,7 @@ export interface CutoffDisplay {
/** What the cut-off measures, and what it does not. Identical on both templates. */ /** What the cut-off measures, and what it does not. Identical on both templates. */
export const CUTOFF_NOTE = export const CUTOFF_NOTE =
'The furthest home offered a place, after higher priorities such as siblings, ' + 'The furthest home offered a place, after higher priorities such as siblings, ' +
'faith and EHCP were applied. It is not a fixed catchment — it moves every year ' + 'faith and EHCP were applied. It is not a fixed catchment. It moves every year ' +
'with the number of applications.'; 'with the number of applications.';
/** Straight-line, because that is how councils measure it. */ /** Straight-line, because that is how councils measure it. */
@@ -54,7 +54,7 @@ export function describeCutoff(
entryYear: formatEntryYear(distance.year), entryYear: formatEntryYear(distance.year),
routeNote: routeNote:
routes > 1 routes > 1
? `Furthest of ${routes} admission routes at this school — the one that ` + ? `Furthest of ${routes} admission routes at this school. The one that ` +
'applies to your child may have had a shorter cut-off.' 'applies to your child may have had a shorter cut-off.'
: null, : null,
}; };
@@ -122,10 +122,10 @@ export function compareToCutoff(
const cutoffPhrase = `the September ${year} cut-off of ${cutoffLabel}`; const cutoffPhrase = `the September ${year} cut-off of ${cutoffLabel}`;
const headline = const headline =
verdict === 'inside' verdict === 'inside'
? `${label} away — inside ${cutoffPhrase}.` ? `${label} away, inside ${cutoffPhrase}.`
: verdict === 'outside' : verdict === 'outside'
? `${label} away — beyond ${cutoffPhrase}.` ? `${label} away, beyond ${cutoffPhrase}.`
: `${label} away — too close to ${cutoffPhrase} to call.`; : `${label} away, too close to ${cutoffPhrase} to call.`;
const detail = const detail =
verdict === 'too-close' verdict === 'too-close'
@@ -156,11 +156,11 @@ export function compareToCutoff(
*/ */
export const CUTOFF_CHECK_CAVEAT = export const CUTOFF_CHECK_CAVEAT =
'Distance is the last criterion applied. Places go first to children in care, ' 'Distance is the last criterion applied. Places go first to children in care, '
+ 'EHCP places, siblings and — at faith schools — on faith criteria, so a home ' + 'EHCP places and siblings, and at faith schools by faith criteria, so a home '
+ 'inside the distance can still miss out. Figures are straight-line distances ' + 'inside the distance can still miss out. Figures are straight-line distances '
+ 'and not a catchment boundary; some authorities measure a walking route ' + 'and not a catchment boundary; some authorities measure a walking route '
+ "instead, which is always longer for the same home. Next year's cut-off " + "instead, which is always longer for the same home. Next year's cut-off "
+ "depends on next year's applicants — always check the school's own " + "depends on next year's applicants, so always check the school's own "
+ 'admissions policy.'; + 'admissions policy.';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+14 -14
View File
@@ -16,32 +16,32 @@ export const METRIC_EXPLANATIONS: Record<string, MetricExplanation> = {
detail: 'The national average is around 60%. Higher means more pupils reached the expected level.', detail: 'The national average is around 60%. Higher means more pupils reached the expected level.',
}, },
rwm_high_pct: { rwm_high_pct: {
label: 'Higher Standard — Reading, Writing & Maths', label: 'Higher Standard: Reading, Writing & Maths',
plain: '% of pupils exceeding the expected level in all three subjects', plain: '% of pupils exceeding the expected level in all three subjects',
detail: 'A more demanding threshold. The national average is around 8%.', detail: 'A more demanding threshold. The national average is around 8%.',
}, },
reading_expected_pct: { reading_expected_pct: {
label: 'Reading — Expected Standard', label: 'Reading: Expected Standard',
plain: '% of pupils achieving the expected standard in reading at age 11', plain: '% of pupils achieving the expected standard in reading at age 11',
}, },
writing_expected_pct: { writing_expected_pct: {
label: 'Writing — Expected Standard', label: 'Writing: Expected Standard',
plain: '% of pupils achieving the expected standard in writing at age 11', plain: '% of pupils achieving the expected standard in writing at age 11',
}, },
maths_expected_pct: { maths_expected_pct: {
label: 'Maths — Expected Standard', label: 'Maths: Expected Standard',
plain: '% of pupils achieving the expected standard in maths at age 11', plain: '% of pupils achieving the expected standard in maths at age 11',
}, },
reading_high_pct: { reading_high_pct: {
label: 'Reading — Higher Standard', label: 'Reading: Higher Standard',
plain: '% of pupils exceeding the expected level in reading', plain: '% of pupils exceeding the expected level in reading',
}, },
writing_high_pct: { writing_high_pct: {
label: 'Writing — Higher Standard', label: 'Writing: Higher Standard',
plain: '% of pupils exceeding the expected level in writing', plain: '% of pupils exceeding the expected level in writing',
}, },
maths_high_pct: { maths_high_pct: {
label: 'Maths — Higher Standard', label: 'Maths: Higher Standard',
plain: '% of pupils exceeding the expected level in maths', plain: '% of pupils exceeding the expected level in maths',
}, },
gps_expected_pct: { gps_expected_pct: {
@@ -90,7 +90,7 @@ export const METRIC_EXPLANATIONS: Record<string, MetricExplanation> = {
detail: 'Lower is better. Persistent absence can significantly affect attainment.', detail: 'Lower is better. Persistent absence can significantly affect attainment.',
}, },
rwm_expected_disadvantaged_pct: { rwm_expected_disadvantaged_pct: {
label: 'Disadvantaged Pupils — Reading, Writing & Maths', label: 'Disadvantaged Pupils: Reading, Writing & Maths',
plain: '% of disadvantaged pupils achieving the expected standard in all three subjects', plain: '% of disadvantaged pupils achieving the expected standard in all three subjects',
}, },
disadvantaged_gap: { disadvantaged_gap: {
@@ -105,7 +105,7 @@ export const METRIC_EXPLANATIONS: Record<string, MetricExplanation> = {
}, },
sen_ehcp_pct: { sen_ehcp_pct: {
label: 'Education, Health and Care Plan (EHCP)', label: 'Education, Health and Care Plan (EHCP)',
plain: '% of pupils with a formal EHCP — a legal plan for pupils with significant additional needs', plain: '% of pupils with a formal EHCP (a legal plan for pupils with significant additional needs)',
}, },
eal_pct: { eal_pct: {
label: 'English as an Additional Language', label: 'English as an Additional Language',
@@ -128,26 +128,26 @@ export const METRIC_EXPLANATIONS: Record<string, MetricExplanation> = {
detail: '0 = national average. Positive means better-than-expected progress; negative means lower-than-expected. A score above +0.5 is considered well above average.', detail: '0 = national average. Positive means better-than-expected progress; negative means lower-than-expected. A score above +0.5 is considered well above average.',
}, },
english_maths_standard_pass_pct: { english_maths_standard_pass_pct: {
label: 'English & Maths — Grade 4+ (Standard Pass)', label: 'English & Maths: Grade 4+ (Standard Pass)',
plain: '% of pupils achieving at least a grade 4 in both English and Maths', plain: '% of pupils achieving at least a grade 4 in both English and Maths',
detail: 'Grade 4 is the minimum "standard pass". Employers and colleges often require grade 4 in English and Maths.', detail: 'Grade 4 is the minimum "standard pass". Employers and colleges often require grade 4 in English and Maths.',
}, },
english_maths_strong_pass_pct: { english_maths_strong_pass_pct: {
label: 'English & Maths — Grade 5+ (Strong Pass)', label: 'English & Maths: Grade 5+ (Strong Pass)',
plain: '% of pupils achieving at least a grade 5 in both English and Maths', plain: '% of pupils achieving at least a grade 5 in both English and Maths',
detail: 'Grade 5 is a "strong pass". Many sixth forms and universities expect grade 5 in English and Maths.', detail: 'Grade 5 is a "strong pass". Many sixth forms and universities expect grade 5 in English and Maths.',
}, },
ebacc_entry_pct: { ebacc_entry_pct: {
label: 'EBacc Entry', label: 'EBacc Entry',
plain: '% of pupils who entered the English Baccalaureate — a set of GCSE subjects covering English, Maths, Sciences, a Language, and Humanities', plain: '% of pupils who entered the English Baccalaureate (a set of GCSE subjects covering English, Maths, Sciences, a Language and Humanities)',
detail: 'EBacc entry keeps academic options open post-16. It is not a separate qualification.', detail: 'EBacc entry keeps academic options open post-16. It is not a separate qualification.',
}, },
ebacc_standard_pass_pct: { ebacc_standard_pass_pct: {
label: 'EBacc — Grade 4+ (Standard Pass)', label: 'EBacc: Grade 4+ (Standard Pass)',
plain: '% of pupils achieving grade 4 or above across all EBacc subjects', plain: '% of pupils achieving grade 4 or above across all EBacc subjects',
}, },
ebacc_strong_pass_pct: { ebacc_strong_pass_pct: {
label: 'EBacc — Grade 5+ (Strong Pass)', label: 'EBacc: Grade 5+ (Strong Pass)',
plain: '% of pupils achieving grade 5 or above across all EBacc subjects', plain: '% of pupils achieving grade 5 or above across all EBacc subjects',
}, },
ebacc_avg_score: { ebacc_avg_score: {
+2 -2
View File
@@ -116,7 +116,7 @@ export function formatWithSuppression(value: number | null | undefined): { displ
return { display: formatPercentage(value), suppressed: false }; return { display: formatPercentage(value), suppressed: false };
} }
export const SUPPRESSED_TOOLTIP = 'Data not available — may be suppressed to protect small cohorts.'; export const SUPPRESSED_TOOLTIP = 'Data not available. It may be suppressed to protect small cohorts.';
/** /**
* Format a progress score (can be negative) * Format a progress score (can be negative)
@@ -750,7 +750,7 @@ export function buildSchoolSummary(
if (admissions.first_preference_offer_pct != null) { if (admissions.first_preference_offer_pct != null) {
const pct = Math.round(admissions.first_preference_offer_pct); const pct = Math.round(admissions.first_preference_offer_pct);
parts.push( parts.push(
`oversubscribed — ${pct}% of first-choice applicants are offered a place`, `oversubscribed (${pct}% of first-choice applicants are offered a place)`,
); );
} else { } else {
parts.push('oversubscribed'); parts.push('oversubscribed');