Merge pull request 'fix(compare): expert should-fixes S1-S4, S6 (S5/S7 closed by verification)' (#55) from fix/expert-signoff-shouldfixes into main
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 53s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 1s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 45s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 13s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 53s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 14s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 1s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 45s
Reviewed-on: #55
This commit was merged in pull request #55.
This commit is contained in:
@@ -73,6 +73,15 @@ describe('buildCompareChart', () => {
|
||||
expect(eng.data[chart.years.indexOf(202122)]).toBe(58.7);
|
||||
});
|
||||
|
||||
it('lists England-only years so the component can caption dashed-only stretches', () => {
|
||||
const chart = buildCompareChart(THREE_SCHOOLS, SCHOOL_LIST, 'rwm_expected_pct', {
|
||||
202122: 58.7,
|
||||
});
|
||||
expect(chart.englandOnlyYears).toEqual([202122]);
|
||||
const none = buildCompareChart(THREE_SCHOOLS, SCHOOL_LIST, 'rwm_expected_pct');
|
||||
expect(none.englandOnlyYears).toEqual([]);
|
||||
});
|
||||
|
||||
it('flags the unpublished 2021/22 school-level year when England has data but schools do not', () => {
|
||||
const withNational = buildCompareChart(THREE_SCHOOLS, SCHOOL_LIST, 'rwm_expected_pct', {
|
||||
202122: 58.7,
|
||||
|
||||
@@ -177,6 +177,16 @@ describe('summariseAdmissions', () => {
|
||||
expect(s.chip).toEqual({ tone: 'warn', text: 'Over 1 in 4 first choices missed out' });
|
||||
});
|
||||
|
||||
it('60% → "About 1 in 3 first choices missed out"', () => {
|
||||
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 60 }));
|
||||
expect(s.chip).toEqual({ tone: 'warn', text: 'About 1 in 3 first choices missed out' });
|
||||
});
|
||||
|
||||
it('44% (selective-scale demand) → "More than half of first choices missed out"', () => {
|
||||
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 43.69 }));
|
||||
expect(s.chip).toEqual({ tone: 'warn', text: 'More than half of first choices missed out' });
|
||||
});
|
||||
|
||||
it('100% → "All first choices offered"', () => {
|
||||
const s = summariseAdmissions(admissions({ first_preference_offer_pct: 100 }));
|
||||
expect(s.chip).toEqual({ tone: 'good', text: 'All first choices offered' });
|
||||
|
||||
@@ -38,13 +38,15 @@ interface ComparisonChartProps {
|
||||
/** Official England figure per academic year for this metric — renders a
|
||||
* dashed grey reference line when provided. */
|
||||
nationalByYear?: Record<number, number | null | undefined>;
|
||||
/** KS4 metrics get a different (honest) gap caption than KS2. */
|
||||
isSecondary?: boolean;
|
||||
}
|
||||
|
||||
// One shape per basket slot (MAX_SCHOOLS = 5) — secondary encoding so
|
||||
// converging lines stay tellable apart without relying on hue alone.
|
||||
const POINT_STYLES: PointStyle[] = ['circle', 'triangle', 'rect', 'rectRot', 'star'];
|
||||
|
||||
export function ComparisonChart({ comparisonData, schools, metric, metricLabel, nationalByYear }: ComparisonChartProps) {
|
||||
export function ComparisonChart({ comparisonData, schools, metric, metricLabel, nationalByYear, isSecondary = false }: ComparisonChartProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const [focusedUrn, setFocusedUrn] = useState<number | null>(null);
|
||||
|
||||
@@ -168,7 +170,7 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
|
||||
display: true,
|
||||
title: {
|
||||
display: !isMobile,
|
||||
text: kind === 'percentage' ? 'Percentage (%)' : kind === 'progress' ? 'Progress Score' : 'Value',
|
||||
text: kind === 'percentage' ? 'Percentage (%)' : kind === 'progress' ? 'Progress Score' : 'Score',
|
||||
font: {
|
||||
size: 12,
|
||||
weight: 'bold',
|
||||
@@ -240,11 +242,23 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
|
||||
<div className={styles.canvasBox}>
|
||||
<Line data={chartData} options={options} aria-label={`${metricLabel} comparison chart`} />
|
||||
</div>
|
||||
{built.showUnpublished202122Note && (
|
||||
{isSecondary && built.englandOnlyYears.length > 0 ? (
|
||||
// KS4's honest story differs from KS2's: 2019/20–2020/21 school-level
|
||||
// GCSE results weren't published (COVID grading); later years WERE
|
||||
// published by DfE but aren't in our dataset yet.
|
||||
<p className={styles.chartNote}>
|
||||
School-level GCSE figures for 2019/20 and 2020/21 weren't published (COVID
|
||||
grading), and more recent years aren't in our dataset yet where lines break — the
|
||||
England average is shown where available.
|
||||
</p>
|
||||
) : (
|
||||
!isSecondary &&
|
||||
built.showUnpublished202122Note && (
|
||||
<p className={styles.chartNote}>
|
||||
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 that year.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -365,12 +365,18 @@ export function ComparisonView({
|
||||
style={{ '--school-count': activeSchools.length } as CSSProperties}
|
||||
aria-label="Schools in this comparison"
|
||||
>
|
||||
{/* Fills the 200px label rail on desktop (hidden on mobile). */}
|
||||
{/* Fills the 200px label rail on desktop (hidden on mobile).
|
||||
All-through schools must not be miscounted as "primary
|
||||
schools"/"secondary schools" — mixed baskets get "· primary
|
||||
view" phrasing instead. */}
|
||||
<div className={styles.barCaption}>
|
||||
<span className={styles.barCaptionEyebrow}>Comparing</span>
|
||||
<span className={styles.barCaptionCount}>
|
||||
{activeSchools.length} {comparePhase} school
|
||||
{activeSchools.length === 1 ? '' : 's'}
|
||||
{activeSchools.every((sch) =>
|
||||
sch.phase?.toLowerCase().includes(comparePhase),
|
||||
)
|
||||
? `${activeSchools.length} ${comparePhase} school${activeSchools.length === 1 ? '' : 's'}`
|
||||
: `${activeSchools.length} schools · ${comparePhase} view`}
|
||||
</span>
|
||||
</div>
|
||||
{activeSchools.map((school, index) => (
|
||||
@@ -390,7 +396,13 @@ export function ComparisonView({
|
||||
<span className={styles.chipNameShort}>{shortName(school.school_name)}</span>
|
||||
</a>
|
||||
<span className={styles.chipMeta}>
|
||||
{[school.local_authority, school.school_type].filter(Boolean).join(' · ')}
|
||||
{[
|
||||
/all.?through/i.test(school.phase ?? '') ? 'All-through' : null,
|
||||
school.local_authority,
|
||||
school.school_type,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -148,6 +148,17 @@ export function CompareAcademics({
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// DfE stopped publishing Progress 8 from 2024/25: those GCSE year groups
|
||||
// sat no KS2 tests (COVID), so there is no baseline to measure progress
|
||||
// from. A bare "No data" reads as a gap on our side — say why. Judged
|
||||
// PER SCHOOL on its own latest data year: a school whose data simply
|
||||
// stops earlier (an unrelated gap) must not borrow the COVID explanation
|
||||
// from a neighbour that does have 2024/25 data.
|
||||
const p8NotPublished = urns.map((urn) => {
|
||||
const rows = data[String(urn)]?.yearly_data ?? [];
|
||||
const y = rows.length ? Math.trunc(rows[rows.length - 1].year) : 0;
|
||||
return y >= 202425;
|
||||
});
|
||||
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;
|
||||
@@ -189,6 +200,11 @@ export function CompareAcademics({
|
||||
>
|
||||
{banding[i]}
|
||||
</Chip>
|
||||
) : p8NotPublished[i] ? (
|
||||
<span className={s.small}>
|
||||
Not published — this GCSE year group sat no KS2 tests (COVID), so DfE has no
|
||||
baseline to measure progress from
|
||||
</span>
|
||||
) : (
|
||||
<span className={s.small}>No data</span>
|
||||
)}
|
||||
@@ -218,6 +234,24 @@ export function CompareAcademics({
|
||||
const national = nationalAverages?.primary;
|
||||
const disadvantaged = latestValues(data, urns, 'rwm_expected_disadvantaged_pct');
|
||||
const disadvantagedAnchor = benchmarks?.primary?.disadvantaged_rwm_expected_pct ?? null;
|
||||
// Cohort size behind the disadvantaged figure (spec §8.5): these are small
|
||||
// groups where single pupils move the percentage — show roughly how many
|
||||
// pupils the figure rests on. Taken from the SAME yearly row that supplies
|
||||
// the displayed percentage: resolving eligible_pupils and the
|
||||
// disadvantaged share independently could mix years and misstate the
|
||||
// cohort behind the figure.
|
||||
const cohorts = urns.map((urn) => {
|
||||
const rows = data[String(urn)]?.yearly_data ?? [];
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
const row = rows[i];
|
||||
if (row.rwm_expected_disadvantaged_pct != null) {
|
||||
if (row.eligible_pupils == null || row.disadvantaged_pct == null) return null;
|
||||
const cohort = Math.round((row.eligible_pupils * row.disadvantaged_pct) / 100);
|
||||
return cohort > 0 ? cohort : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<Section
|
||||
@@ -270,6 +304,9 @@ export function CompareAcademics({
|
||||
<span className={s.big} style={{ fontSize: '1.1rem' }}>
|
||||
{Math.round(value)}%
|
||||
</span>{' '}
|
||||
{cohorts[i] != null && (
|
||||
<span className={s.small}>of ~{cohorts[i]} disadvantaged pupils</span>
|
||||
)}{' '}
|
||||
{disadvantagedAnchor != null && (
|
||||
<Chip tone={verdict(value, disadvantagedAnchor, 5) === 'below' ? 'warn' : 'good'}>
|
||||
{verdict(value, disadvantagedAnchor, 5) === 'above' &&
|
||||
|
||||
@@ -170,8 +170,14 @@ export function CompareAtAGlance({
|
||||
{schools.map((school, i) => {
|
||||
const census = data[String(school.urn)]?.census;
|
||||
const pupils = census?.total_pupils ?? school.total_pupils ?? null;
|
||||
// An all-through school's roll covers every age group, so judging
|
||||
// it against the single-phase median ("Much larger than average")
|
||||
// is meaningless — label the roll honestly instead.
|
||||
const isAllThrough = /all.?through/i.test(school.phase ?? '');
|
||||
let sizeNote: string | null = null;
|
||||
if (pupils != null && medianPupils != null) {
|
||||
if (isAllThrough) {
|
||||
sizeNote = 'Whole-school roll (all-through, all ages)';
|
||||
} else if (pupils != null && medianPupils != null) {
|
||||
if (pupils >= medianPupils * 1.5) sizeNote = 'Much larger than average';
|
||||
else if (pupils >= medianPupils * 1.1) sizeNote = 'Larger than average';
|
||||
else if (pupils <= medianPupils * 0.66) sizeNote = 'Much smaller than average';
|
||||
|
||||
@@ -48,10 +48,25 @@ export function CompareCommunity({
|
||||
);
|
||||
};
|
||||
|
||||
const anyAllThrough = schools.some((school) => /all.?through/i.test(school.phase ?? ''));
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Who goes there"
|
||||
how="The school's community, from the latest school census. State-school averages are computed from our dataset and shown for context — there's no “right” number here."
|
||||
how={
|
||||
<>
|
||||
The school's community, from the latest school census. State-school averages are
|
||||
computed from our dataset and shown for context — there's no “right”
|
||||
number here.
|
||||
{anyAllThrough && (
|
||||
<>
|
||||
{' '}
|
||||
For all-through schools these figures cover the whole school, all ages — not just
|
||||
the {isSecondary ? 'secondary' : 'primary'} phase.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SectionGrid schools={schools}>
|
||||
<Measure label="Pupils on roll">
|
||||
|
||||
@@ -119,6 +119,7 @@ export function TrendsExplorer({
|
||||
metric={metric}
|
||||
metricLabel={metricLabel}
|
||||
nationalByYear={nationalByYear}
|
||||
isSecondary={!isPrimaryPhase}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,10 @@ export interface CompareChart {
|
||||
/** True when England published a 2021/22 figure but no school has one —
|
||||
* the UI shows: "DfE didn't publish school-level figures for 2021/22". */
|
||||
showUnpublished202122Note: boolean;
|
||||
/** Years where the England overlay has a value but no school does — the
|
||||
* chart shows a dashed-line-only stretch that needs explaining (KS2 and
|
||||
* KS4 have different honest explanations, so the component owns the copy). */
|
||||
englandOnlyYears: number[];
|
||||
}
|
||||
|
||||
export function buildCompareChart(
|
||||
@@ -92,11 +96,12 @@ export function buildCompareChart(
|
||||
}
|
||||
}
|
||||
|
||||
const idx202122 = years.indexOf(202122);
|
||||
const showUnpublished202122Note =
|
||||
idx202122 >= 0 &&
|
||||
englandDataset?.data[idx202122] != null &&
|
||||
schoolDatasets.every((ds) => ds.data[idx202122] == null);
|
||||
const englandOnlyYears = years.filter(
|
||||
(year, i) =>
|
||||
englandDataset?.data[i] != null && schoolDatasets.every((ds) => ds.data[i] == null),
|
||||
);
|
||||
|
||||
return { years, schoolDatasets, englandDataset, showUnpublished202122Note };
|
||||
const showUnpublished202122Note = englandOnlyYears.includes(202122);
|
||||
|
||||
return { years, schoolDatasets, englandDataset, showUnpublished202122Note, englandOnlyYears };
|
||||
}
|
||||
|
||||
@@ -192,6 +192,12 @@ export function summariseAdmissions(
|
||||
if (pct != null) {
|
||||
if (pct >= 100) {
|
||||
chip = { tone: 'good', text: 'All first choices offered' };
|
||||
} else if (pct < 50) {
|
||||
// Banded, not one blanket chip: "Over 1 in 4" on a school where more
|
||||
// than half missed out understated the worst cases by half.
|
||||
chip = { tone: 'warn', text: 'More than half of first choices missed out' };
|
||||
} else if (pct < 67) {
|
||||
chip = { tone: 'warn', text: 'About 1 in 3 first choices missed out' };
|
||||
} else if (pct < 75) {
|
||||
chip = { tone: 'warn', text: 'Over 1 in 4 first choices missed out' };
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user