Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e74d3882ce | ||
|
|
b4b0249a06 | ||
|
|
fef83b3bf2 | ||
|
|
cf458fe05c | ||
|
|
19b41b6999 |
@@ -577,6 +577,7 @@ def compute_benchmarks(df: pd.DataFrame) -> dict:
|
||||
"eal_pct": _median(sub, "eal_pct"),
|
||||
"sen_support_pct": _median(sub, "sen_support_pct"),
|
||||
"disadvantaged_pct": _median(sub, "disadvantaged_pct"),
|
||||
"fsm_pct": _median(sub, "fsm_pct"),
|
||||
"median_pupils": median_pupils,
|
||||
}
|
||||
if with_disadvantaged:
|
||||
|
||||
@@ -17,33 +17,33 @@ def _df():
|
||||
# weighted = (40*100 + 60*300) / 400 = 55.0 ; unweighted mean = 50.0
|
||||
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=100,
|
||||
rwm_expected_disadvantaged_pct=40.0, eal_pct=10.0,
|
||||
sen_support_pct=10.0, disadvantaged_pct=20.0, total_pupils=200),
|
||||
sen_support_pct=10.0, disadvantaged_pct=20.0, fsm_pct=15.0, total_pupils=200),
|
||||
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=300,
|
||||
rwm_expected_disadvantaged_pct=60.0, eal_pct=20.0,
|
||||
sen_support_pct=14.0, disadvantaged_pct=24.0, total_pupils=280),
|
||||
sen_support_pct=14.0, disadvantaged_pct=24.0, fsm_pct=17.0, total_pupils=280),
|
||||
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=np.nan,
|
||||
rwm_expected_disadvantaged_pct=99.0, eal_pct=30.0,
|
||||
sen_support_pct=18.0, disadvantaged_pct=30.0, total_pupils=300),
|
||||
sen_support_pct=18.0, disadvantaged_pct=30.0, fsm_pct=19.0, total_pupils=300),
|
||||
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=50,
|
||||
rwm_expected_disadvantaged_pct=np.nan, eal_pct=np.nan,
|
||||
sen_support_pct=np.nan, disadvantaged_pct=np.nan, total_pupils=np.nan),
|
||||
sen_support_pct=np.nan, disadvantaged_pct=np.nan, fsm_pct=np.nan, total_pupils=np.nan),
|
||||
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=40,
|
||||
rwm_expected_disadvantaged_pct=np.nan, eal_pct=40.0,
|
||||
sen_support_pct=20.0, disadvantaged_pct=40.0, total_pupils=350),
|
||||
sen_support_pct=20.0, disadvantaged_pct=40.0, fsm_pct=21.0, total_pupils=350),
|
||||
dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=60,
|
||||
rwm_expected_disadvantaged_pct=np.nan, eal_pct=50.0,
|
||||
sen_support_pct=22.0, disadvantaged_pct=44.0, total_pupils=400),
|
||||
sen_support_pct=22.0, disadvantaged_pct=44.0, fsm_pct=23.0, total_pupils=400),
|
||||
# Two secondary schools (attainment_8 non-null)
|
||||
dict(year=LATEST, attainment_8_score=45.0, eligible_pupils=180,
|
||||
rwm_expected_disadvantaged_pct=np.nan, eal_pct=15.0,
|
||||
sen_support_pct=12.0, disadvantaged_pct=22.0, total_pupils=1000),
|
||||
sen_support_pct=12.0, disadvantaged_pct=22.0, fsm_pct=12.0, total_pupils=1000),
|
||||
dict(year=LATEST, attainment_8_score=50.0, eligible_pupils=200,
|
||||
rwm_expected_disadvantaged_pct=np.nan, eal_pct=25.0,
|
||||
sen_support_pct=16.0, disadvantaged_pct=26.0, total_pupils=1200),
|
||||
sen_support_pct=16.0, disadvantaged_pct=26.0, fsm_pct=14.0, total_pupils=1200),
|
||||
# An older-year primary row that must NOT influence anything
|
||||
dict(year=202324, attainment_8_score=np.nan, eligible_pupils=500,
|
||||
rwm_expected_disadvantaged_pct=1.0, eal_pct=99.0,
|
||||
sen_support_pct=99.0, disadvantaged_pct=99.0, total_pupils=9999),
|
||||
sen_support_pct=99.0, disadvantaged_pct=99.0, fsm_pct=99.0, total_pupils=9999),
|
||||
]
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
@@ -59,6 +59,8 @@ def test_medians_ignore_nan_and_older_years():
|
||||
assert b["year"] == LATEST
|
||||
# eal medians over [10,20,30,40,50] = 30
|
||||
assert b["primary"]["eal_pct"] == 30.0
|
||||
# fsm medians over [15,17,19,21,23] = 19
|
||||
assert b["primary"]["fsm_pct"] == 19.0
|
||||
# median pupils over [200,280,300,350,400] = 300
|
||||
assert b["primary"]["median_pupils"] == 300
|
||||
|
||||
@@ -66,6 +68,7 @@ def test_medians_ignore_nan_and_older_years():
|
||||
def test_secondary_block_has_no_disadvantaged_rwm():
|
||||
b = compute_benchmarks(_df())
|
||||
assert "disadvantaged_rwm_expected_pct" not in b["secondary"]
|
||||
assert b["secondary"]["fsm_pct"] == 13.0
|
||||
assert b["secondary"]["median_pupils"] == 1100
|
||||
|
||||
|
||||
|
||||
@@ -227,6 +227,19 @@ test('compare chart on mobile shows school chips with tap-to-focus', async ({ pa
|
||||
);
|
||||
expect(bodyOverflowsX).toBe(false);
|
||||
|
||||
// The sticky school bar must pin *below* the sticky site header, not at
|
||||
// top:0 where the header covers it and the selected schools are hidden.
|
||||
// Assert the sticky offset directly (robust — no scroll timing needed).
|
||||
const barTop = await page
|
||||
.locator('[class*="schoolBar"]')
|
||||
.first()
|
||||
.evaluate((el) => parseFloat(getComputedStyle(el).top));
|
||||
const headerHeight = await page
|
||||
.locator('[class*="header"]')
|
||||
.first()
|
||||
.evaluate((el) => el.getBoundingClientRect().height);
|
||||
expect(barTop).toBeGreaterThanOrEqual(headerHeight - 1);
|
||||
|
||||
// The trends chart still renders (inside the Explore trends section)…
|
||||
const chartCanvas = page.locator('canvas:visible').first();
|
||||
await expect(chartCanvas).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
@@ -126,6 +126,13 @@ describe('ofstedDisplay', () => {
|
||||
expect(ofstedDisplay(ofsted({})).kind).toBe('none');
|
||||
});
|
||||
|
||||
it('identifies transitional inspections without overall grades', () => {
|
||||
const transitional = ofstedDisplay(
|
||||
ofsted({ overall_effectiveness: null, inspection_date: '2024-11-05' }),
|
||||
);
|
||||
expect(transitional.kind).toBe('transitional');
|
||||
});
|
||||
|
||||
it('uses the four legacy grade words', () => {
|
||||
expect(OFSTED_LEGACY_GRADES).toEqual({
|
||||
1: 'Outstanding',
|
||||
|
||||
@@ -80,10 +80,12 @@
|
||||
}
|
||||
|
||||
/* Sticky school bar — column identity while scrolling; horizontal scroll on
|
||||
narrow screens */
|
||||
narrow screens. Offset by the sticky site header's height (Navigation is
|
||||
position: sticky, top: 0) so this bar pins just below it instead of
|
||||
sliding underneath and being hidden. Header ≈ 65px desktop / 57px mobile. */
|
||||
.schoolBar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: 65px;
|
||||
z-index: 10;
|
||||
background: var(--bg-primary, #faf7f2);
|
||||
display: flex;
|
||||
@@ -173,6 +175,11 @@
|
||||
pills with short names (matching the mobile mockup) instead of full-width
|
||||
cards whose names wrap to several lines. */
|
||||
@media (max-width: 640px) {
|
||||
/* The mobile Navigation header is shorter (≈57px). */
|
||||
.schoolBar {
|
||||
top: 57px;
|
||||
}
|
||||
|
||||
.schoolChip {
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
|
||||
@@ -142,19 +142,23 @@ export function ComparisonView({
|
||||
});
|
||||
}, [urnKey, isInitialized]);
|
||||
|
||||
// Classify schools by phase using comparison data
|
||||
const classifySchool = (school: School): 'primary' | 'secondary' => {
|
||||
const primarySchools = selectedSchools.filter((school) => {
|
||||
const info = comparisonData?.[school.urn]?.school_info;
|
||||
if (info?.attainment_8_score != null) return 'secondary';
|
||||
if (info?.rwm_expected_pct != null) return 'primary';
|
||||
// Fallback: check yearly data
|
||||
const yearlyData = comparisonData?.[school.urn]?.yearly_data;
|
||||
if (yearlyData?.some((d) => d.attainment_8_score != null)) return 'secondary';
|
||||
return 'primary';
|
||||
};
|
||||
const hasPrimaryData =
|
||||
info?.rwm_expected_pct != null ||
|
||||
comparisonData?.[school.urn]?.yearly_data?.some((d) => d.rwm_expected_pct != null);
|
||||
if (hasPrimaryData) return true;
|
||||
return school.phase?.toLowerCase().includes('primary') || false;
|
||||
});
|
||||
|
||||
const primarySchools = selectedSchools.filter((s) => classifySchool(s) === 'primary');
|
||||
const secondarySchools = selectedSchools.filter((s) => classifySchool(s) === 'secondary');
|
||||
const secondarySchools = selectedSchools.filter((school) => {
|
||||
const info = comparisonData?.[school.urn]?.school_info;
|
||||
const hasSecondaryData =
|
||||
info?.attainment_8_score != null ||
|
||||
comparisonData?.[school.urn]?.yearly_data?.some((d) => d.attainment_8_score != null);
|
||||
if (hasSecondaryData) return true;
|
||||
return school.phase?.toLowerCase().includes('secondary') || false;
|
||||
});
|
||||
|
||||
// Auto-select tab with more schools and sync the metric to match the phase.
|
||||
useEffect(() => {
|
||||
@@ -375,6 +379,7 @@ export function ComparisonView({
|
||||
data={activeComparisonData}
|
||||
nationalAverages={nationalAverages}
|
||||
benchmarks={benchmarks}
|
||||
isSecondary={!isPrimary}
|
||||
/>
|
||||
<CompareOfsted schools={activeSchools} data={activeComparisonData} />
|
||||
<CompareAcademics
|
||||
@@ -382,12 +387,14 @@ export function ComparisonView({
|
||||
data={activeComparisonData}
|
||||
nationalAverages={nationalAverages}
|
||||
benchmarks={benchmarks}
|
||||
isSecondary={!isPrimary}
|
||||
/>
|
||||
<CompareAdmissions schools={activeSchools} data={activeComparisonData} />
|
||||
<CompareCommunity
|
||||
schools={activeSchools}
|
||||
data={activeComparisonData}
|
||||
benchmarks={benchmarks}
|
||||
isSecondary={!isPrimary}
|
||||
/>
|
||||
<TrendsExplorer
|
||||
schools={activeSchools}
|
||||
|
||||
@@ -125,15 +125,17 @@ export function CompareAcademics({
|
||||
data,
|
||||
nationalAverages,
|
||||
benchmarks,
|
||||
isSecondary: propIsSecondary,
|
||||
}: {
|
||||
schools: School[];
|
||||
data: Record<string, ComparisonData>;
|
||||
nationalAverages?: NationalAverages;
|
||||
benchmarks?: Benchmarks;
|
||||
isSecondary?: boolean;
|
||||
}) {
|
||||
const urns = schools.map((school) => school.urn);
|
||||
const schoolNames = schools.map((school) => school.school_name);
|
||||
const isSecondary = schools.some(
|
||||
const isSecondary = propIsSecondary !== undefined ? propIsSecondary : schools.some(
|
||||
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
||||
);
|
||||
|
||||
|
||||
@@ -47,14 +47,16 @@ export function CompareAtAGlance({
|
||||
data,
|
||||
nationalAverages,
|
||||
benchmarks,
|
||||
isSecondary: propIsSecondary,
|
||||
}: {
|
||||
schools: School[];
|
||||
data: Record<string, ComparisonData>;
|
||||
nationalAverages?: NationalAverages;
|
||||
benchmarks?: Benchmarks;
|
||||
isSecondary?: boolean;
|
||||
}) {
|
||||
const urns = schools.map((school) => school.urn);
|
||||
const isSecondary = schools.some(
|
||||
const isSecondary = propIsSecondary !== undefined ? propIsSecondary : schools.some(
|
||||
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
||||
);
|
||||
const headlineKey = isSecondary ? 'attainment_8_score' : 'rwm_expected_pct';
|
||||
@@ -83,6 +85,14 @@ export function CompareAtAGlance({
|
||||
{display.carriedForward && <span className={s.small}>Grade carried forward</span>}
|
||||
</>
|
||||
)}
|
||||
{display.kind === 'transitional' && (
|
||||
<>
|
||||
<span className={s.badge} style={{ backgroundColor: '#e2e8f0', color: '#475569' }}>
|
||||
No overall grade
|
||||
</span>
|
||||
<span className={s.small}>Sub-judgements only</span>
|
||||
</>
|
||||
)}
|
||||
{display.kind === 'none' && <span className={s.small}>No inspection in our dataset</span>}
|
||||
</Cell>
|
||||
);
|
||||
|
||||
@@ -20,24 +20,27 @@ export function CompareCommunity({
|
||||
schools,
|
||||
data,
|
||||
benchmarks,
|
||||
isSecondary: propIsSecondary,
|
||||
}: {
|
||||
schools: School[];
|
||||
data: Record<string, ComparisonData>;
|
||||
benchmarks?: Benchmarks;
|
||||
isSecondary?: boolean;
|
||||
}) {
|
||||
const isSecondary = schools.some(
|
||||
const isSecondary = propIsSecondary !== undefined ? propIsSecondary : schools.some(
|
||||
(school) => data[String(school.urn)]?.school_info?.attainment_8_score != null,
|
||||
);
|
||||
const bench = isSecondary ? benchmarks?.secondary : benchmarks?.primary;
|
||||
|
||||
const fsmChip = (value: number | null) => {
|
||||
if (value == null || bench?.disadvantaged_pct == null) return null;
|
||||
const v = verdict(value, bench.disadvantaged_pct, 3);
|
||||
const anchor = bench?.fsm_pct ?? bench?.disadvantaged_pct ?? null;
|
||||
if (value == null || anchor == null) return null;
|
||||
const v = verdict(value, anchor, 3);
|
||||
return (
|
||||
<Chip tone="neutral">
|
||||
{v === 'above' && 'Above the state-school average'}
|
||||
{v === 'close' && 'About the state-school average'}
|
||||
{v === 'below' && 'Below the state-school average'}
|
||||
{v === 'above' && `Above the state-school average (${Math.round(anchor)}%)`}
|
||||
{v === 'close' && `About the state-school average (${Math.round(anchor)}%)`}
|
||||
{v === 'below' && `Below the state-school average (${Math.round(anchor)}%)`}
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,6 +51,18 @@ function ResultCell({ display }: { display: OfstedDisplay }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (display.kind === 'transitional') {
|
||||
return (
|
||||
<>
|
||||
<span className={s.badge} style={{ backgroundColor: '#e2e8f0', color: '#475569' }}>
|
||||
No overall grade
|
||||
</span>
|
||||
<span className={s.small}>
|
||||
Inspected under transitional framework (sub-judgements only)
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<span className={`${s.badge} ${display.grade <= 2 ? s.badgeGood : display.grade === 3 ? s.badgeWarn : s.badgeBad}`}>
|
||||
|
||||
@@ -102,6 +102,7 @@ export type OfstedDisplay =
|
||||
| { kind: 'none' }
|
||||
| { kind: 'graded'; grade: number; gradeLabel: string; carriedForward: false }
|
||||
| { kind: 'carried_forward'; grade: number; gradeLabel: string; carriedForward: true }
|
||||
| { kind: 'transitional' }
|
||||
| { kind: 'report_card'; summary: ReportCardSummary };
|
||||
|
||||
export function ofstedDisplay(
|
||||
@@ -117,7 +118,12 @@ export function ofstedDisplay(
|
||||
|
||||
const grade = ofsted.overall_effectiveness;
|
||||
const gradeLabel = grade != null ? OFSTED_LEGACY_GRADES[grade] : undefined;
|
||||
if (grade == null || gradeLabel === undefined) return { kind: 'none' };
|
||||
if (grade == null || gradeLabel === undefined) {
|
||||
if (ofsted.inspection_date) {
|
||||
return { kind: 'transitional' };
|
||||
}
|
||||
return { kind: 'none' };
|
||||
}
|
||||
|
||||
if (ofsted.grade_source === 'ungraded_carried_forward') {
|
||||
return { kind: 'carried_forward', grade, gradeLabel, carriedForward: true };
|
||||
|
||||
@@ -357,6 +357,7 @@ export interface BenchmarkBlock {
|
||||
eal_pct: number | null;
|
||||
sen_support_pct: number | null;
|
||||
disadvantaged_pct: number | null;
|
||||
fsm_pct?: number | null;
|
||||
median_pupils: number | null;
|
||||
/** Primary only — weighted by cohort size. */
|
||||
disadvantaged_rwm_expected_pct?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user