feat: add secondary school support with KS4 data and metric tooltips
Some checks failed
Build and Push Docker Images / Build Frontend (Next.js) (push) Has been cancelled
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Has been cancelled
Build and Push Docker Images / Trigger Portainer Update (push) Has been cancelled
Build and Push Docker Images / Build Backend (FastAPI) (push) Has been cancelled
Some checks failed
Build and Push Docker Images / Build Frontend (Next.js) (push) Has been cancelled
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Has been cancelled
Build and Push Docker Images / Trigger Portainer Update (push) Has been cancelled
Build and Push Docker Images / Build Backend (FastAPI) (push) Has been cancelled
- Backend: replace INNER JOIN ks2 with UNION ALL (ks2 + ks4) so primary and secondary schools both appear in the main DataFrame - Backend: add /api/national-averages endpoint computing means from live data, replacing the hardcoded NATIONAL_AVG constant on the frontend - Backend: add phase filter param to /api/schools; return phases from /api/filters; fix hardcoded "phase": "Primary" in school detail endpoint - Backend: add KS4 metric definitions (Attainment 8, Progress 8, EBacc, English & Maths pass rates) to METRIC_DEFINITIONS and RANKING_COLUMNS - Frontend: SchoolDetailView is now phase-aware — secondary schools show a GCSE Results section (Att8, P8, E&M, EBacc) instead of SATs; phonics tab hidden for secondary; admissions says Year 7 instead of Year 3; history table shows KS4 columns; chart datasets switch for secondary - Frontend: new MetricTooltip component (CSS-only ⓘ icon) backed by METRIC_EXPLANATIONS — added to RWM, GPS, SEN, EAL, IDACI, progress scores and all KS4 metrics throughout SchoolDetailView and SchoolCard - Frontend: METRIC_EXPLANATIONS extended with KS4 terms (Attainment 8, Progress 8, EBacc) and previously missing terms (SEN, EHCP, EAL, IDACI) - Frontend: SchoolCard expands "RWM" to "Reading, Writing & Maths" and shows Attainment 8 / English & Maths Grade 4+ for secondary schools - Frontend: FilterBar adds Phase dropdown (Primary / Secondary / All-through) - Frontend: HomeView hero copy updated; compact list shows phase-aware metric - Global metadata updated to remove "primary only" framing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
SchoolCompare.co.uk API
|
SchoolCompare.co.uk API
|
||||||
Serves primary school (KS2) performance data for comparing schools.
|
Serves primary and secondary school performance data for comparing schools.
|
||||||
Uses real data from UK Government Compare School Performance downloads.
|
Uses real data from UK Government Compare School Performance downloads.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="SchoolCompare API",
|
title="SchoolCompare API",
|
||||||
description="API for comparing primary school (KS2) performance data - schoolcompare.co.uk",
|
description="API for comparing primary and secondary school performance data - schoolcompare.co.uk",
|
||||||
version="2.0.0",
|
version="2.0.0",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
# Disable docs in production for security
|
# Disable docs in production for security
|
||||||
@@ -213,21 +213,23 @@ async def get_schools(
|
|||||||
None, description="Filter by local authority", max_length=100
|
None, description="Filter by local authority", max_length=100
|
||||||
),
|
),
|
||||||
school_type: Optional[str] = Query(None, description="Filter by school type", max_length=100),
|
school_type: Optional[str] = Query(None, description="Filter by school type", max_length=100),
|
||||||
|
phase: Optional[str] = Query(None, description="Filter by phase: primary, secondary, all-through", max_length=50),
|
||||||
postcode: Optional[str] = Query(None, description="Search near postcode", max_length=10),
|
postcode: Optional[str] = Query(None, description="Search near postcode", max_length=10),
|
||||||
radius: float = Query(5.0, ge=0.1, le=50, description="Search radius in miles"),
|
radius: float = Query(5.0, ge=0.1, le=50, description="Search radius in miles"),
|
||||||
page: int = Query(1, ge=1, le=1000, description="Page number"),
|
page: int = Query(1, ge=1, le=1000, description="Page number"),
|
||||||
page_size: int = Query(None, ge=1, le=100, description="Results per page"),
|
page_size: int = Query(None, ge=1, le=100, description="Results per page"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get list of unique primary schools with pagination.
|
Get list of schools with pagination.
|
||||||
|
|
||||||
Returns paginated results with total count for efficient loading.
|
Returns paginated results with total count for efficient loading.
|
||||||
Supports location-based search using postcode.
|
Supports location-based search using postcode and phase filtering.
|
||||||
"""
|
"""
|
||||||
# Sanitize inputs
|
# Sanitize inputs
|
||||||
search = sanitize_search_input(search)
|
search = sanitize_search_input(search)
|
||||||
local_authority = sanitize_search_input(local_authority)
|
local_authority = sanitize_search_input(local_authority)
|
||||||
school_type = sanitize_search_input(school_type)
|
school_type = sanitize_search_input(school_type)
|
||||||
|
phase = sanitize_search_input(phase)
|
||||||
postcode = validate_postcode(postcode)
|
postcode = validate_postcode(postcode)
|
||||||
|
|
||||||
df = load_school_data()
|
df = load_school_data()
|
||||||
@@ -253,9 +255,25 @@ async def get_schools(
|
|||||||
)
|
)
|
||||||
df_latest = df_latest.merge(prev_rwm, on="urn", how="left")
|
df_latest = df_latest.merge(prev_rwm, on="urn", how="left")
|
||||||
|
|
||||||
|
# Phase filter
|
||||||
|
if phase:
|
||||||
|
phase_lower = phase.lower()
|
||||||
|
if phase_lower in ("primary", "secondary", "all-through", "all_through"):
|
||||||
|
# Map param values to GIAS phase strings (partial match)
|
||||||
|
phase_map = {
|
||||||
|
"primary": "primary",
|
||||||
|
"secondary": "secondary",
|
||||||
|
"all-through": "all-through",
|
||||||
|
"all_through": "all-through",
|
||||||
|
}
|
||||||
|
phase_substr = phase_map[phase_lower]
|
||||||
|
schools_df_phase_mask = df_latest["phase"].str.lower().str.contains(phase_substr, na=False)
|
||||||
|
df_latest = df_latest[schools_df_phase_mask]
|
||||||
|
|
||||||
# Include key result metrics for display on cards
|
# Include key result metrics for display on cards
|
||||||
location_cols = ["latitude", "longitude"]
|
location_cols = ["latitude", "longitude"]
|
||||||
result_cols = [
|
result_cols = [
|
||||||
|
"phase",
|
||||||
"year",
|
"year",
|
||||||
"rwm_expected_pct",
|
"rwm_expected_pct",
|
||||||
"rwm_high_pct",
|
"rwm_high_pct",
|
||||||
@@ -264,6 +282,8 @@ async def get_schools(
|
|||||||
"writing_expected_pct",
|
"writing_expected_pct",
|
||||||
"maths_expected_pct",
|
"maths_expected_pct",
|
||||||
"total_pupils",
|
"total_pupils",
|
||||||
|
"attainment_8_score",
|
||||||
|
"english_maths_standard_pass_pct",
|
||||||
]
|
]
|
||||||
available_cols = [
|
available_cols = [
|
||||||
c
|
c
|
||||||
@@ -364,7 +384,7 @@ async def get_schools(
|
|||||||
@app.get("/api/schools/{urn}")
|
@app.get("/api/schools/{urn}")
|
||||||
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
||||||
async def get_school_details(request: Request, urn: int):
|
async def get_school_details(request: Request, urn: int):
|
||||||
"""Get detailed KS2 data for a specific primary school across all years."""
|
"""Get detailed performance data for a specific school across all years."""
|
||||||
# Validate URN range (UK school URNs are 6 digits)
|
# Validate URN range (UK school URNs are 6 digits)
|
||||||
if not (100000 <= urn <= 999999):
|
if not (100000 <= urn <= 999999):
|
||||||
raise HTTPException(status_code=400, detail="Invalid URN format")
|
raise HTTPException(status_code=400, detail="Invalid URN format")
|
||||||
@@ -406,7 +426,7 @@ async def get_school_details(request: Request, urn: int):
|
|||||||
"age_range": latest.get("age_range", ""),
|
"age_range": latest.get("age_range", ""),
|
||||||
"latitude": latest.get("latitude"),
|
"latitude": latest.get("latitude"),
|
||||||
"longitude": latest.get("longitude"),
|
"longitude": latest.get("longitude"),
|
||||||
"phase": "Primary",
|
"phase": latest.get("phase"),
|
||||||
# GIAS fields
|
# GIAS fields
|
||||||
"website": latest.get("website"),
|
"website": latest.get("website"),
|
||||||
"headteacher_name": latest.get("headteacher_name"),
|
"headteacher_name": latest.get("headteacher_name"),
|
||||||
@@ -433,7 +453,7 @@ async def compare_schools(
|
|||||||
request: Request,
|
request: Request,
|
||||||
urns: str = Query(..., description="Comma-separated URNs", max_length=100)
|
urns: str = Query(..., description="Comma-separated URNs", max_length=100)
|
||||||
):
|
):
|
||||||
"""Compare multiple primary schools side by side."""
|
"""Compare multiple schools side by side."""
|
||||||
df = load_school_data()
|
df = load_school_data()
|
||||||
|
|
||||||
if df.empty:
|
if df.empty:
|
||||||
@@ -487,10 +507,66 @@ async def get_filter_options(request: Request):
|
|||||||
"years": [],
|
"years": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Phases: return values from data, ordered sensibly
|
||||||
|
phases = sorted(df["phase"].dropna().unique().tolist()) if "phase" in df.columns else []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"local_authorities": sorted(df["local_authority"].dropna().unique().tolist()),
|
"local_authorities": sorted(df["local_authority"].dropna().unique().tolist()),
|
||||||
"school_types": sorted(df["school_type"].dropna().unique().tolist()),
|
"school_types": sorted(df["school_type"].dropna().unique().tolist()),
|
||||||
"years": sorted(df["year"].dropna().unique().tolist()),
|
"years": sorted(df["year"].dropna().unique().tolist()),
|
||||||
|
"phases": phases,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/national-averages")
|
||||||
|
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
||||||
|
async def get_national_averages(request: Request):
|
||||||
|
"""
|
||||||
|
Compute national average for each metric from the latest data year.
|
||||||
|
Returns separate averages for primary (KS2) and secondary (KS4) schools.
|
||||||
|
Values are derived from the loaded DataFrame so they automatically
|
||||||
|
stay current when new data is loaded.
|
||||||
|
"""
|
||||||
|
df = load_school_data()
|
||||||
|
if df.empty:
|
||||||
|
return {"primary": {}, "secondary": {}}
|
||||||
|
|
||||||
|
latest_year = int(df["year"].max())
|
||||||
|
df_latest = df[df["year"] == latest_year]
|
||||||
|
|
||||||
|
ks2_metrics = [
|
||||||
|
"rwm_expected_pct", "rwm_high_pct",
|
||||||
|
"reading_expected_pct", "writing_expected_pct", "maths_expected_pct",
|
||||||
|
"reading_avg_score", "maths_avg_score", "gps_avg_score",
|
||||||
|
"reading_progress", "writing_progress", "maths_progress",
|
||||||
|
"overall_absence_pct", "persistent_absence_pct",
|
||||||
|
"disadvantaged_gap", "disadvantaged_pct", "sen_support_pct",
|
||||||
|
]
|
||||||
|
ks4_metrics = [
|
||||||
|
"attainment_8_score", "progress_8_score",
|
||||||
|
"english_maths_standard_pass_pct", "english_maths_strong_pass_pct",
|
||||||
|
"ebacc_entry_pct", "ebacc_standard_pass_pct", "ebacc_strong_pass_pct",
|
||||||
|
"ebacc_avg_score", "gcse_grade_91_pct",
|
||||||
|
]
|
||||||
|
|
||||||
|
def _means(sub_df, metric_list):
|
||||||
|
out = {}
|
||||||
|
for col in metric_list:
|
||||||
|
if col in sub_df.columns:
|
||||||
|
val = sub_df[col].dropna()
|
||||||
|
if len(val) > 0:
|
||||||
|
out[col] = round(float(val.mean()), 2)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# Primary: schools where KS2 data is non-null
|
||||||
|
primary_df = df_latest[df_latest["rwm_expected_pct"].notna()]
|
||||||
|
# Secondary: schools where KS4 data is non-null
|
||||||
|
secondary_df = df_latest[df_latest["attainment_8_score"].notna()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"year": latest_year,
|
||||||
|
"primary": _means(primary_df, ks2_metrics),
|
||||||
|
"secondary": _means(secondary_df, ks4_metrics),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -498,7 +574,7 @@ async def get_filter_options(request: Request):
|
|||||||
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
||||||
async def get_available_metrics(request: Request):
|
async def get_available_metrics(request: Request):
|
||||||
"""
|
"""
|
||||||
Get list of available KS2 performance metrics for primary schools.
|
Get list of available performance metrics for schools.
|
||||||
|
|
||||||
This is the single source of truth for metric definitions.
|
This is the single source of truth for metric definitions.
|
||||||
Frontend should consume this to avoid duplication.
|
Frontend should consume this to avoid duplication.
|
||||||
@@ -517,7 +593,7 @@ async def get_available_metrics(request: Request):
|
|||||||
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
@limiter.limit(f"{settings.rate_limit_per_minute}/minute")
|
||||||
async def get_rankings(
|
async def get_rankings(
|
||||||
request: Request,
|
request: Request,
|
||||||
metric: str = Query("rwm_expected_pct", description="KS2 metric to rank by", max_length=50),
|
metric: str = Query("rwm_expected_pct", description="Metric to rank by", max_length=50),
|
||||||
year: Optional[int] = Query(
|
year: Optional[int] = Query(
|
||||||
None, description="Specific year (defaults to most recent)", ge=2000, le=2100
|
None, description="Specific year (defaults to most recent)", ge=2000, le=2100
|
||||||
),
|
),
|
||||||
@@ -526,7 +602,7 @@ async def get_rankings(
|
|||||||
None, description="Filter by local authority", max_length=100
|
None, description="Filter by local authority", max_length=100
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
"""Get primary school rankings by a specific KS2 metric."""
|
"""Get school rankings by a specific metric."""
|
||||||
# Sanitize local authority input
|
# Sanitize local authority input
|
||||||
local_authority = sanitize_search_input(local_authority)
|
local_authority = sanitize_search_input(local_authority)
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
_MAIN_QUERY = text("""
|
_MAIN_QUERY = text("""
|
||||||
|
-- Branch 1: Primary schools (KS2 data; KS4 columns NULL)
|
||||||
SELECT
|
SELECT
|
||||||
s.urn,
|
s.urn,
|
||||||
s.school_name,
|
s.school_name,
|
||||||
@@ -137,11 +138,11 @@ _MAIN_QUERY = text("""
|
|||||||
l.postcode,
|
l.postcode,
|
||||||
l.latitude,
|
l.latitude,
|
||||||
l.longitude,
|
l.longitude,
|
||||||
-- KS2 performance
|
|
||||||
k.year,
|
k.year,
|
||||||
k.source_urn,
|
k.source_urn,
|
||||||
k.total_pupils,
|
k.total_pupils,
|
||||||
k.eligible_pupils,
|
k.eligible_pupils,
|
||||||
|
-- KS2 columns
|
||||||
k.rwm_expected_pct,
|
k.rwm_expected_pct,
|
||||||
k.rwm_high_pct,
|
k.rwm_high_pct,
|
||||||
k.reading_expected_pct,
|
k.reading_expected_pct,
|
||||||
@@ -175,11 +176,116 @@ _MAIN_QUERY = text("""
|
|||||||
k.eal_pct,
|
k.eal_pct,
|
||||||
k.sen_support_pct,
|
k.sen_support_pct,
|
||||||
k.sen_ehcp_pct,
|
k.sen_ehcp_pct,
|
||||||
k.stability_pct
|
k.stability_pct,
|
||||||
|
-- KS4 columns (NULL for primary)
|
||||||
|
NULL::numeric AS attainment_8_score,
|
||||||
|
NULL::numeric AS progress_8_score,
|
||||||
|
NULL::numeric AS progress_8_lower_ci,
|
||||||
|
NULL::numeric AS progress_8_upper_ci,
|
||||||
|
NULL::numeric AS progress_8_english,
|
||||||
|
NULL::numeric AS progress_8_maths,
|
||||||
|
NULL::numeric AS progress_8_ebacc,
|
||||||
|
NULL::numeric AS progress_8_open,
|
||||||
|
NULL::numeric AS english_maths_strong_pass_pct,
|
||||||
|
NULL::numeric AS english_maths_standard_pass_pct,
|
||||||
|
NULL::numeric AS ebacc_entry_pct,
|
||||||
|
NULL::numeric AS ebacc_strong_pass_pct,
|
||||||
|
NULL::numeric AS ebacc_standard_pass_pct,
|
||||||
|
NULL::numeric AS ebacc_avg_score,
|
||||||
|
NULL::numeric AS gcse_grade_91_pct,
|
||||||
|
NULL::numeric AS prior_attainment_avg
|
||||||
FROM marts.dim_school s
|
FROM marts.dim_school s
|
||||||
JOIN marts.dim_location l ON s.urn = l.urn
|
JOIN marts.dim_location l ON s.urn = l.urn
|
||||||
JOIN marts.fact_ks2_performance k ON s.urn = k.urn
|
JOIN marts.fact_ks2_performance k ON s.urn = k.urn
|
||||||
ORDER BY s.school_name, k.year
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- Branch 2: Secondary schools (KS4 data; KS2 columns NULL)
|
||||||
|
SELECT
|
||||||
|
s.urn,
|
||||||
|
s.school_name,
|
||||||
|
s.phase,
|
||||||
|
s.school_type,
|
||||||
|
s.academy_trust_name AS trust_name,
|
||||||
|
s.academy_trust_uid AS trust_uid,
|
||||||
|
s.religious_character AS religious_denomination,
|
||||||
|
s.gender,
|
||||||
|
s.age_range,
|
||||||
|
s.capacity,
|
||||||
|
s.headteacher_name,
|
||||||
|
s.website,
|
||||||
|
s.ofsted_grade,
|
||||||
|
s.ofsted_date,
|
||||||
|
s.ofsted_framework,
|
||||||
|
l.local_authority_name AS local_authority,
|
||||||
|
l.local_authority_code,
|
||||||
|
l.address_line1 AS address1,
|
||||||
|
l.address_line2 AS address2,
|
||||||
|
l.town,
|
||||||
|
l.postcode,
|
||||||
|
l.latitude,
|
||||||
|
l.longitude,
|
||||||
|
k4.year,
|
||||||
|
k4.source_urn,
|
||||||
|
k4.total_pupils,
|
||||||
|
k4.eligible_pupils,
|
||||||
|
-- KS2 columns (NULL for secondary)
|
||||||
|
NULL::numeric AS rwm_expected_pct,
|
||||||
|
NULL::numeric AS rwm_high_pct,
|
||||||
|
NULL::numeric AS reading_expected_pct,
|
||||||
|
NULL::numeric AS reading_high_pct,
|
||||||
|
NULL::numeric AS reading_avg_score,
|
||||||
|
NULL::numeric AS reading_progress,
|
||||||
|
NULL::numeric AS writing_expected_pct,
|
||||||
|
NULL::numeric AS writing_high_pct,
|
||||||
|
NULL::numeric AS writing_progress,
|
||||||
|
NULL::numeric AS maths_expected_pct,
|
||||||
|
NULL::numeric AS maths_high_pct,
|
||||||
|
NULL::numeric AS maths_avg_score,
|
||||||
|
NULL::numeric AS maths_progress,
|
||||||
|
NULL::numeric AS gps_expected_pct,
|
||||||
|
NULL::numeric AS gps_high_pct,
|
||||||
|
NULL::numeric AS gps_avg_score,
|
||||||
|
NULL::numeric AS science_expected_pct,
|
||||||
|
NULL::numeric AS reading_absence_pct,
|
||||||
|
NULL::numeric AS writing_absence_pct,
|
||||||
|
NULL::numeric AS maths_absence_pct,
|
||||||
|
NULL::numeric AS gps_absence_pct,
|
||||||
|
NULL::numeric AS science_absence_pct,
|
||||||
|
NULL::numeric AS rwm_expected_boys_pct,
|
||||||
|
NULL::numeric AS rwm_high_boys_pct,
|
||||||
|
NULL::numeric AS rwm_expected_girls_pct,
|
||||||
|
NULL::numeric AS rwm_high_girls_pct,
|
||||||
|
NULL::numeric AS rwm_expected_disadvantaged_pct,
|
||||||
|
NULL::numeric AS rwm_expected_non_disadvantaged_pct,
|
||||||
|
NULL::numeric AS disadvantaged_gap,
|
||||||
|
NULL::numeric AS disadvantaged_pct,
|
||||||
|
NULL::numeric AS eal_pct,
|
||||||
|
k4.sen_support_pct,
|
||||||
|
k4.sen_ehcp_pct,
|
||||||
|
NULL::numeric AS stability_pct,
|
||||||
|
-- KS4 columns
|
||||||
|
k4.attainment_8_score,
|
||||||
|
k4.progress_8_score,
|
||||||
|
k4.progress_8_lower_ci,
|
||||||
|
k4.progress_8_upper_ci,
|
||||||
|
k4.progress_8_english,
|
||||||
|
k4.progress_8_maths,
|
||||||
|
k4.progress_8_ebacc,
|
||||||
|
k4.progress_8_open,
|
||||||
|
k4.english_maths_strong_pass_pct,
|
||||||
|
k4.english_maths_standard_pass_pct,
|
||||||
|
k4.ebacc_entry_pct,
|
||||||
|
k4.ebacc_strong_pass_pct,
|
||||||
|
k4.ebacc_standard_pass_pct,
|
||||||
|
k4.ebacc_avg_score,
|
||||||
|
k4.gcse_grade_91_pct,
|
||||||
|
k4.prior_attainment_avg
|
||||||
|
FROM marts.dim_school s
|
||||||
|
JOIN marts.dim_location l ON s.urn = l.urn
|
||||||
|
JOIN marts.fact_ks4_performance k4 ON s.urn = k4.urn
|
||||||
|
|
||||||
|
ORDER BY school_name, year
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -401,6 +401,70 @@ METRIC_DEFINITIONS = {
|
|||||||
"type": "score",
|
"type": "score",
|
||||||
"category": "trends",
|
"category": "trends",
|
||||||
},
|
},
|
||||||
|
# ── GCSE Performance (KS4) ────────────────────────────────────────────
|
||||||
|
"attainment_8_score": {
|
||||||
|
"name": "Attainment 8",
|
||||||
|
"short_name": "Att 8",
|
||||||
|
"description": "Average grade across a pupil's best 8 GCSEs including English and Maths",
|
||||||
|
"type": "score",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"progress_8_score": {
|
||||||
|
"name": "Progress 8",
|
||||||
|
"short_name": "P8",
|
||||||
|
"description": "Progress from KS2 baseline to GCSE relative to similar pupils nationally (0 = national average)",
|
||||||
|
"type": "score",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"english_maths_standard_pass_pct": {
|
||||||
|
"name": "English & Maths Grade 4+",
|
||||||
|
"short_name": "E&M 4+",
|
||||||
|
"description": "% of pupils achieving grade 4 (standard pass) or above in both English and Maths",
|
||||||
|
"type": "percentage",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"english_maths_strong_pass_pct": {
|
||||||
|
"name": "English & Maths Grade 5+",
|
||||||
|
"short_name": "E&M 5+",
|
||||||
|
"description": "% of pupils achieving grade 5 (strong pass) or above in both English and Maths",
|
||||||
|
"type": "percentage",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"ebacc_entry_pct": {
|
||||||
|
"name": "EBacc Entry %",
|
||||||
|
"short_name": "EBacc Entry",
|
||||||
|
"description": "% of pupils entered for the English Baccalaureate (English, Maths, Sciences, Languages, Humanities)",
|
||||||
|
"type": "percentage",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"ebacc_standard_pass_pct": {
|
||||||
|
"name": "EBacc Grade 4+",
|
||||||
|
"short_name": "EBacc 4+",
|
||||||
|
"description": "% of pupils achieving grade 4+ across all EBacc subjects",
|
||||||
|
"type": "percentage",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"ebacc_strong_pass_pct": {
|
||||||
|
"name": "EBacc Grade 5+",
|
||||||
|
"short_name": "EBacc 5+",
|
||||||
|
"description": "% of pupils achieving grade 5+ across all EBacc subjects",
|
||||||
|
"type": "percentage",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"ebacc_avg_score": {
|
||||||
|
"name": "EBacc Average Score",
|
||||||
|
"short_name": "EBacc Avg",
|
||||||
|
"description": "Average points score across EBacc subjects",
|
||||||
|
"type": "score",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
|
"gcse_grade_91_pct": {
|
||||||
|
"name": "GCSE Grade 9–1 %",
|
||||||
|
"short_name": "GCSE 9–1",
|
||||||
|
"description": "% of GCSE entries achieving a grade 9 to 1",
|
||||||
|
"type": "percentage",
|
||||||
|
"category": "gcse",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ranking columns to include in rankings response
|
# Ranking columns to include in rankings response
|
||||||
@@ -456,6 +520,16 @@ RANKING_COLUMNS = [
|
|||||||
"rwm_expected_3yr_pct",
|
"rwm_expected_3yr_pct",
|
||||||
"reading_avg_3yr",
|
"reading_avg_3yr",
|
||||||
"maths_avg_3yr",
|
"maths_avg_3yr",
|
||||||
|
# GCSE (KS4)
|
||||||
|
"attainment_8_score",
|
||||||
|
"progress_8_score",
|
||||||
|
"english_maths_standard_pass_pct",
|
||||||
|
"english_maths_strong_pass_pct",
|
||||||
|
"ebacc_entry_pct",
|
||||||
|
"ebacc_standard_pass_pct",
|
||||||
|
"ebacc_strong_pass_pct",
|
||||||
|
"ebacc_avg_score",
|
||||||
|
"gcse_grade_91_pct",
|
||||||
]
|
]
|
||||||
|
|
||||||
# School listing columns
|
# School listing columns
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ const playfairDisplay = Playfair_Display({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
default: 'SchoolCompare | Compare Primary School Performance',
|
default: 'SchoolCompare | Compare School Performance',
|
||||||
template: '%s | SchoolCompare',
|
template: '%s | SchoolCompare',
|
||||||
},
|
},
|
||||||
description: 'Compare primary school KS2 performance across England',
|
description: 'Compare primary and secondary school SATs and GCSE performance across England',
|
||||||
keywords: 'school comparison, KS2 results, primary school performance, England schools, SATs 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',
|
||||||
icons: {
|
icons: {
|
||||||
@@ -37,15 +37,15 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website',
|
type: 'website',
|
||||||
title: 'SchoolCompare | Compare Primary School Performance',
|
title: 'SchoolCompare | Compare School Performance',
|
||||||
description: 'Compare primary school KS2 performance across England',
|
description: 'Compare primary and secondary school SATs and GCSE performance across England',
|
||||||
url: 'https://schoolcompare.co.uk',
|
url: 'https://schoolcompare.co.uk',
|
||||||
siteName: 'SchoolCompare',
|
siteName: 'SchoolCompare',
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary',
|
card: 'summary',
|
||||||
title: 'SchoolCompare | Compare Primary School Performance',
|
title: 'SchoolCompare | Compare School Performance',
|
||||||
description: 'Compare primary school KS2 performance across England',
|
description: 'Compare primary and secondary school SATs and GCSE performance across England',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface HomePageProps {
|
|||||||
search?: string;
|
search?: string;
|
||||||
local_authority?: string;
|
local_authority?: string;
|
||||||
school_type?: string;
|
school_type?: string;
|
||||||
|
phase?: string;
|
||||||
page?: string;
|
page?: string;
|
||||||
postcode?: string;
|
postcode?: string;
|
||||||
radius?: string;
|
radius?: string;
|
||||||
@@ -19,7 +20,7 @@ interface HomePageProps {
|
|||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
description: 'Search and compare primary school KS2 performance across England',
|
description: 'Search and compare school performance across England',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force dynamic rendering (no static generation at build time)
|
// Force dynamic rendering (no static generation at build time)
|
||||||
@@ -38,6 +39,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
params.search ||
|
params.search ||
|
||||||
params.local_authority ||
|
params.local_authority ||
|
||||||
params.school_type ||
|
params.school_type ||
|
||||||
|
params.phase ||
|
||||||
params.postcode
|
params.postcode
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
search: params.search,
|
search: params.search,
|
||||||
local_authority: params.local_authority,
|
local_authority: params.local_authority,
|
||||||
school_type: params.school_type,
|
school_type: params.school_type,
|
||||||
|
phase: params.phase,
|
||||||
postcode: params.postcode,
|
postcode: params.postcode,
|
||||||
radius,
|
radius,
|
||||||
page,
|
page,
|
||||||
@@ -65,7 +68,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
return (
|
return (
|
||||||
<HomeView
|
<HomeView
|
||||||
initialSchools={schoolsData}
|
initialSchools={schoolsData}
|
||||||
filters={filtersData || { local_authorities: [], school_types: [], years: [] }}
|
filters={filtersData || { local_authorities: [], school_types: [], years: [], phases: [] }}
|
||||||
totalSchools={dataInfo?.total_schools ?? null}
|
totalSchools={dataInfo?.total_schools ?? null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -76,7 +79,7 @@ export default async function HomePage({ searchParams }: HomePageProps) {
|
|||||||
return (
|
return (
|
||||||
<HomeView
|
<HomeView
|
||||||
initialSchools={{ schools: [], page: 1, page_size: 50, total: 0, total_pages: 0 }}
|
initialSchools={{ schools: [], page: 1, page_size: 50, total: 0, total_pages: 0 }}
|
||||||
filters={{ local_authorities: [], school_types: [], years: [] }}
|
filters={{ local_authorities: [], school_types: [], years: [], phases: [] }}
|
||||||
totalSchools={null}
|
totalSchools={null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ interface RankingsPageProps {
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'School Rankings',
|
title: 'School Rankings',
|
||||||
description: 'Top-ranked primary schools by KS2 performance across England',
|
description: 'Top-ranked schools by SATs and GCSE performance across England',
|
||||||
keywords: 'school rankings, top schools, best schools, KS2 rankings, school league tables',
|
keywords: 'school rankings, top schools, best schools, KS2 rankings, KS4 rankings, school league tables',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Force dynamic rendering
|
// Force dynamic rendering
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
|||||||
|
|
||||||
const currentLA = searchParams.get('local_authority') || '';
|
const currentLA = searchParams.get('local_authority') || '';
|
||||||
const currentType = searchParams.get('school_type') || '';
|
const currentType = searchParams.get('school_type') || '';
|
||||||
|
const currentPhase = searchParams.get('phase') || '';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -86,7 +87,7 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActiveFilters = currentSearch || currentLA || currentType || currentPostcode;
|
const hasActiveFilters = currentSearch || currentLA || currentType || currentPhase || currentPostcode;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.filterBar} ${isPending ? styles.isLoading : ''} ${isHero ? styles.heroMode : ''}`}>
|
<div className={`${styles.filterBar} ${isPending ? styles.isLoading : ''} ${isHero ? styles.heroMode : ''}`}>
|
||||||
@@ -152,6 +153,22 @@ export function FilterBar({ filters, isHero }: FilterBarProps) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{filters.phases && filters.phases.length > 0 && (
|
||||||
|
<select
|
||||||
|
value={currentPhase}
|
||||||
|
onChange={(e) => handleFilterChange('phase', e.target.value)}
|
||||||
|
className={styles.filterSelect}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<option value="">All Phases</option>
|
||||||
|
{filters.phases.map((p) => (
|
||||||
|
<option key={p} value={p.toLowerCase()}>
|
||||||
|
{p}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<button onClick={handleClearFilters} className={`btn btn-tertiary ${styles.clearButton}`} type="button" disabled={isPending}>
|
<button onClick={handleClearFilters} className={`btn btn-tertiary ${styles.clearButton}`} type="button" disabled={isPending}>
|
||||||
Clear Filters
|
Clear Filters
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function Footer() {
|
|||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<h3 className={styles.title}>SchoolCompare</h3>
|
<h3 className={styles.title}>SchoolCompare</h3>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
Compare primary schools across England.
|
Compare primary and secondary schools across England.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
|||||||
{/* Combined Hero + Search and Filters */}
|
{/* Combined Hero + Search and Filters */}
|
||||||
{!isSearchActive && (
|
{!isSearchActive && (
|
||||||
<div className={styles.heroSection}>
|
<div className={styles.heroSection}>
|
||||||
<h1 className={styles.heroTitle}>Compare Primary School Performance</h1>
|
<h1 className={styles.heroTitle}>Compare School Performance</h1>
|
||||||
<p className={styles.heroDescription}>Search and compare KS2 results for thousands of schools across England</p>
|
<p className={styles.heroDescription}>Search and compare SATs and GCSE results for thousands of schools across England</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export function HomeView({ initialSchools, filters, totalSchools }: HomeViewProp
|
|||||||
{/* Discovery section shown on landing page before any search */}
|
{/* Discovery section shown on landing page before any search */}
|
||||||
{!isSearchActive && initialSchools.schools.length === 0 && (
|
{!isSearchActive && initialSchools.schools.length === 0 && (
|
||||||
<div className={styles.discoverySection}>
|
<div className={styles.discoverySection}>
|
||||||
{totalSchools && <p className={styles.discoveryCount}><strong>{totalSchools.toLocaleString()}+</strong> primary schools across England</p>}
|
{totalSchools && <p className={styles.discoveryCount}><strong>{totalSchools.toLocaleString()}+</strong> primary and secondary schools across England</p>}
|
||||||
<p className={styles.discoveryHints}>Try searching for a school name, or enter a postcode to find schools near you.</p>
|
<p className={styles.discoveryHints}>Try searching for a school name, or enter a postcode to find schools near you.</p>
|
||||||
<div className={styles.quickSearches}>
|
<div className={styles.quickSearches}>
|
||||||
<span className={styles.quickSearchLabel}>Quick searches:</span>
|
<span className={styles.quickSearchLabel}>Quick searches:</span>
|
||||||
@@ -258,7 +258,14 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare }: CompactSchoo
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.compactItemStats}>
|
<div className={styles.compactItemStats}>
|
||||||
<span className={styles.compactStat}>
|
<span className={styles.compactStat}>
|
||||||
<strong>{school.rwm_expected_pct !== null ? `${school.rwm_expected_pct}%` : '-'}</strong> RWM
|
<strong>
|
||||||
|
{school.attainment_8_score != null
|
||||||
|
? school.attainment_8_score.toFixed(1)
|
||||||
|
: school.rwm_expected_pct !== null
|
||||||
|
? `${school.rwm_expected_pct}%`
|
||||||
|
: '-'}
|
||||||
|
</strong>{' '}
|
||||||
|
{school.attainment_8_score != null ? 'Att 8' : 'RWM'}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.compactStat}>
|
<span className={styles.compactStat}>
|
||||||
<strong>{school.total_pupils || '-'}</strong> pupils
|
<strong>{school.total_pupils || '-'}</strong> pupils
|
||||||
|
|||||||
83
nextjs-app/components/MetricTooltip.module.css
Normal file
83
nextjs-app/components/MetricTooltip.module.css
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-muted, #8a7a72);
|
||||||
|
cursor: help;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper:hover .icon {
|
||||||
|
color: var(--accent-coral, #e07256);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 9999;
|
||||||
|
width: 220px;
|
||||||
|
background: var(--bg-primary, #faf7f2);
|
||||||
|
border: 1px solid var(--border-color, #e8ddd4);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 16px rgba(44, 36, 32, 0.15);
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep tooltip visible when hovering over it */
|
||||||
|
.wrapper:hover .tooltip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small arrow pointing down */
|
||||||
|
.tooltip::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-top-color: var(--border-color, #e8ddd4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipLabel {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-primary, #2c2420);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipPlain {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary, #5a4a44);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipDetail {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted, #8a7a72);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flip tooltip below when near top of screen */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.tooltip {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
nextjs-app/components/MetricTooltip.tsx
Normal file
31
nextjs-app/components/MetricTooltip.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { METRIC_EXPLANATIONS } from '@/lib/metrics';
|
||||||
|
import styles from './MetricTooltip.module.css';
|
||||||
|
|
||||||
|
interface MetricTooltipProps {
|
||||||
|
metricKey?: string;
|
||||||
|
label?: string;
|
||||||
|
plain?: string;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricTooltip({ metricKey, label, plain, detail }: MetricTooltipProps) {
|
||||||
|
const explanation = metricKey ? METRIC_EXPLANATIONS[metricKey] : undefined;
|
||||||
|
const tooltipLabel = label ?? explanation?.label;
|
||||||
|
const tooltipPlain = plain ?? explanation?.plain;
|
||||||
|
const tooltipDetail = detail ?? explanation?.detail;
|
||||||
|
|
||||||
|
if (!tooltipPlain) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.wrapper}>
|
||||||
|
<span className={styles.icon} aria-label={tooltipLabel ?? 'More information'} role="img">ⓘ</span>
|
||||||
|
<span className={styles.tooltip} role="tooltip">
|
||||||
|
{tooltipLabel && <span className={styles.tooltipLabel}>{tooltipLabel}</span>}
|
||||||
|
<span className={styles.tooltipPlain}>{tooltipPlain}</span>
|
||||||
|
{tooltipDetail && <span className={styles.tooltipDetail}>{tooltipDetail}</span>}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,24 +34,50 @@ ChartJS.register(
|
|||||||
interface PerformanceChartProps {
|
interface PerformanceChartProps {
|
||||||
data: SchoolResult[];
|
data: SchoolResult[];
|
||||||
schoolName: string;
|
schoolName: string;
|
||||||
|
isSecondary?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PerformanceChart({ data, schoolName }: PerformanceChartProps) {
|
export function PerformanceChart({ data, schoolName, isSecondary = false }: PerformanceChartProps) {
|
||||||
// Sort data by year
|
// Sort data by year
|
||||||
const sortedData = [...data].sort((a, b) => a.year - b.year);
|
const sortedData = [...data].sort((a, b) => a.year - b.year);
|
||||||
const years = sortedData.map(d => d.year.toString());
|
const years = sortedData.map(d => d.year.toString());
|
||||||
|
|
||||||
// Prepare datasets
|
// Prepare datasets — phase-aware
|
||||||
const datasets = [
|
const datasets = isSecondary ? [
|
||||||
{
|
{
|
||||||
label: 'RWM Expected %',
|
label: 'Attainment 8',
|
||||||
|
data: sortedData.map(d => d.attainment_8_score),
|
||||||
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
tension: 0.3,
|
||||||
|
yAxisID: 'y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'English & Maths Grade 4+',
|
||||||
|
data: sortedData.map(d => d.english_maths_standard_pass_pct),
|
||||||
|
borderColor: 'rgb(16, 185, 129)',
|
||||||
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||||
|
tension: 0.3,
|
||||||
|
yAxisID: 'y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Progress 8',
|
||||||
|
data: sortedData.map(d => d.progress_8_score),
|
||||||
|
borderColor: 'rgb(245, 158, 11)',
|
||||||
|
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
||||||
|
tension: 0.3,
|
||||||
|
yAxisID: 'y1',
|
||||||
|
},
|
||||||
|
] : [
|
||||||
|
{
|
||||||
|
label: 'Reading, Writing & Maths Expected %',
|
||||||
data: sortedData.map(d => d.rwm_expected_pct),
|
data: sortedData.map(d => d.rwm_expected_pct),
|
||||||
borderColor: 'rgb(59, 130, 246)',
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'RWM Higher %',
|
label: 'Reading, Writing & Maths Higher %',
|
||||||
data: sortedData.map(d => d.rwm_high_pct),
|
data: sortedData.map(d => d.rwm_high_pct),
|
||||||
borderColor: 'rgb(16, 185, 129)',
|
borderColor: 'rgb(16, 185, 129)',
|
||||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||||
@@ -153,14 +179,14 @@ export function PerformanceChart({ data, schoolName }: PerformanceChartProps) {
|
|||||||
position: 'left' as const,
|
position: 'left' as const,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Percentage (%)',
|
text: isSecondary ? 'Score / Percentage (%)' : 'Percentage (%)',
|
||||||
font: {
|
font: {
|
||||||
size: 12,
|
size: 12,
|
||||||
weight: 'bold',
|
weight: 'bold',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: isSecondary ? undefined : 100,
|
||||||
grid: {
|
grid: {
|
||||||
color: 'rgba(0, 0, 0, 0.05)',
|
color: 'rgba(0, 0, 0, 0.05)',
|
||||||
},
|
},
|
||||||
@@ -171,7 +197,7 @@ export function PerformanceChart({ data, schoolName }: PerformanceChartProps) {
|
|||||||
position: 'right' as const,
|
position: 'right' as const,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Progress Score',
|
text: isSecondary ? 'Progress 8 Score' : 'Progress Score',
|
||||||
font: {
|
font: {
|
||||||
size: 12,
|
size: 12,
|
||||||
weight: 'bold',
|
weight: 'bold',
|
||||||
|
|||||||
@@ -48,12 +48,35 @@ export function SchoolCard({ school, onAddToCompare, onRemoveFromCompare, showDi
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(school.rwm_expected_pct !== null || school.reading_progress !== null) && (
|
{(school.rwm_expected_pct != null || school.attainment_8_score != null || school.reading_progress !== null) && (
|
||||||
<div className={styles.metrics}>
|
<div className={styles.metrics}>
|
||||||
|
{/* KS4 card metrics for secondary schools */}
|
||||||
|
{school.attainment_8_score != null && (
|
||||||
|
<div className={styles.metric}>
|
||||||
|
<span className={styles.metricLabel}>
|
||||||
|
Attainment 8
|
||||||
|
<span className={styles.metricHint}>avg grade across best 8 GCSEs</span>
|
||||||
|
</span>
|
||||||
|
<div className={styles.metricValue}>
|
||||||
|
<strong>{school.attainment_8_score.toFixed(1)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{school.english_maths_standard_pass_pct != null && (
|
||||||
|
<div className={styles.metric}>
|
||||||
|
<span className={styles.metricLabel}>
|
||||||
|
English & Maths Grade 4+
|
||||||
|
<span className={styles.metricHint}>% standard pass in both</span>
|
||||||
|
</span>
|
||||||
|
<div className={styles.metricValue}>
|
||||||
|
<strong>{formatPercentage(school.english_maths_standard_pass_pct)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{school.rwm_expected_pct !== null && (
|
{school.rwm_expected_pct !== null && (
|
||||||
<div className={styles.metric}>
|
<div className={styles.metric}>
|
||||||
<span className={styles.metricLabel}>
|
<span className={styles.metricLabel}>
|
||||||
RWM Expected
|
Reading, Writing & Maths
|
||||||
<span className={styles.metricHint}>% meeting expected standard</span>
|
<span className={styles.metricHint}>% meeting expected standard</span>
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.metricValue}>
|
<div className={styles.metricValue}>
|
||||||
|
|||||||
@@ -5,15 +5,17 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useComparison } from '@/hooks/useComparison';
|
import { useComparison } from '@/hooks/useComparison';
|
||||||
import { PerformanceChart } from './PerformanceChart';
|
import { PerformanceChart } from './PerformanceChart';
|
||||||
import { SchoolMap } from './SchoolMap';
|
import { SchoolMap } from './SchoolMap';
|
||||||
|
import { MetricTooltip } from './MetricTooltip';
|
||||||
import type {
|
import type {
|
||||||
School, SchoolResult, AbsenceData,
|
School, SchoolResult, AbsenceData,
|
||||||
OfstedInspection, OfstedParentView, SchoolCensus,
|
OfstedInspection, OfstedParentView, SchoolCensus,
|
||||||
SchoolAdmissions, SenDetail, Phonics,
|
SchoolAdmissions, SenDetail, Phonics,
|
||||||
SchoolDeprivation, SchoolFinance,
|
SchoolDeprivation, SchoolFinance, NationalAverages,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils';
|
import { formatPercentage, formatProgress, formatAcademicYear } from '@/lib/utils';
|
||||||
import styles from './SchoolDetailView.module.css';
|
import styles from './SchoolDetailView.module.css';
|
||||||
@@ -37,19 +39,6 @@ const RC_CATEGORIES = [
|
|||||||
{ key: 'rc_sixth_form' as const, label: 'Sixth Form' },
|
{ key: 'rc_sixth_form' as const, label: 'Sixth Form' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 2023 national averages for context
|
|
||||||
const NATIONAL_AVG = {
|
|
||||||
rwm_expected: 60,
|
|
||||||
rwm_high: 8,
|
|
||||||
reading_expected: 73,
|
|
||||||
writing_expected: 71,
|
|
||||||
maths_expected: 73,
|
|
||||||
phonics_yr1: 79,
|
|
||||||
overall_absence: 6.7,
|
|
||||||
persistent_absence: 22,
|
|
||||||
class_size: 27,
|
|
||||||
per_pupil_spend: 6000,
|
|
||||||
};
|
|
||||||
|
|
||||||
function progressClass(val: number | null | undefined): string {
|
function progressClass(val: number | null | undefined): string {
|
||||||
if (val == null) return '';
|
if (val == null) return '';
|
||||||
@@ -82,6 +71,23 @@ export function SchoolDetailView({
|
|||||||
|
|
||||||
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
const latestResults = yearlyData.length > 0 ? yearlyData[yearlyData.length - 1] : null;
|
||||||
|
|
||||||
|
// Phase detection
|
||||||
|
const phase = schoolInfo.phase ?? '';
|
||||||
|
const isSecondary = phase.toLowerCase().includes('secondary') || phase.toLowerCase() === 'all-through';
|
||||||
|
const isPrimary = !isSecondary;
|
||||||
|
|
||||||
|
// National averages (fetched dynamically so they stay current)
|
||||||
|
const [nationalAvg, setNationalAvg] = useState<NationalAverages | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/national-averages')
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(data => { if (data) setNationalAvg(data); })
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const primaryAvg = nationalAvg?.primary ?? {};
|
||||||
|
const secondaryAvg = nationalAvg?.secondary ?? {};
|
||||||
|
|
||||||
const handleComparisonToggle = () => {
|
const handleComparisonToggle = () => {
|
||||||
if (isInComparison) {
|
if (isInComparison) {
|
||||||
removeSchool(schoolInfo.urn);
|
removeSchool(schoolInfo.urn);
|
||||||
@@ -108,13 +114,18 @@ export function SchoolDetailView({
|
|||||||
const hasFinance = finance != null && finance.per_pupil_spend != null;
|
const hasFinance = finance != null && finance.per_pupil_spend != null;
|
||||||
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
|
const hasLocation = schoolInfo.latitude != null && schoolInfo.longitude != null;
|
||||||
|
|
||||||
|
// Determine whether this school has KS2 or KS4 results to show
|
||||||
|
const hasKS2Results = latestResults != null && latestResults.rwm_expected_pct != null;
|
||||||
|
const hasKS4Results = latestResults != null && latestResults.attainment_8_score != null;
|
||||||
|
const hasAnyResults = hasKS2Results || hasKS4Results;
|
||||||
|
|
||||||
// Build section nav items dynamically — only sections with data
|
// Build section nav items dynamically — only sections with data
|
||||||
const navItems: { id: string; label: string }[] = [];
|
const navItems: { id: string; label: string }[] = [];
|
||||||
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
if (ofsted) navItems.push({ id: 'ofsted', label: 'Ofsted' });
|
||||||
if (parentView && parentView.total_responses != null && parentView.total_responses > 0)
|
if (parentView && parentView.total_responses != null && parentView.total_responses > 0)
|
||||||
navItems.push({ id: 'parents', label: 'Parents' });
|
navItems.push({ id: 'parents', label: 'Parents' });
|
||||||
if (latestResults) navItems.push({ id: 'sats', label: 'SATs' });
|
if (hasAnyResults) navItems.push({ id: 'results', label: isSecondary ? 'GCSEs' : 'SATs' });
|
||||||
if (hasPhonics) navItems.push({ id: 'phonics', label: 'Phonics' });
|
if (hasPhonics && isPrimary) navItems.push({ id: 'phonics', label: 'Phonics' });
|
||||||
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
if (hasSchoolLife) navItems.push({ id: 'school-life', label: 'School Life' });
|
||||||
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
if (admissions) navItems.push({ id: 'admissions', label: 'Admissions' });
|
||||||
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
if (hasInclusionData) navItems.push({ id: 'inclusion', label: 'Pupils' });
|
||||||
@@ -328,135 +339,259 @@ export function SchoolDetailView({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* SATs Results (merged with Subject Breakdown) */}
|
{/* Results Section (SATs for primary, GCSEs for secondary) */}
|
||||||
{latestResults && (
|
{hasAnyResults && latestResults && (
|
||||||
<section id="sats" className={styles.card}>
|
<section id="results" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>SATs Results ({formatAcademicYear(latestResults.year)})</h2>
|
<h2 className={styles.sectionTitle}>
|
||||||
|
{isSecondary ? 'GCSE Results' : 'SATs Results'} ({formatAcademicYear(latestResults.year)})
|
||||||
|
</h2>
|
||||||
<p className={styles.sectionSubtitle}>
|
<p className={styles.sectionSubtitle}>
|
||||||
End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.
|
{isSecondary
|
||||||
|
? 'GCSE results for Year 11 pupils. National averages shown for comparison.'
|
||||||
|
: 'End-of-primary-school tests taken by Year 6 pupils. National averages shown for comparison.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Headline numbers: RWM combined */}
|
{/* ── Primary / KS2 content ── */}
|
||||||
<div className={styles.metricsGrid}>
|
{hasKS2Results && (
|
||||||
{latestResults.rwm_expected_pct !== null && (
|
<>
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricsGrid}>
|
||||||
<div className={styles.metricLabel}>Reading, Writing & Maths combined</div>
|
{latestResults.rwm_expected_pct !== null && (
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.rwm_expected_pct)}</div>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricHint}>National avg: {NATIONAL_AVG.rwm_expected}%</div>
|
<div className={styles.metricLabel}>
|
||||||
|
Reading, Writing & Maths combined
|
||||||
|
<MetricTooltip metricKey="rwm_expected_pct" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.metricValue}>{formatPercentage(latestResults.rwm_expected_pct)}</div>
|
||||||
|
{primaryAvg.rwm_expected_pct != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: {primaryAvg.rwm_expected_pct.toFixed(0)}%</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.rwm_high_pct !== null && (
|
||||||
|
<div className={styles.metricCard}>
|
||||||
|
<div className={styles.metricLabel}>
|
||||||
|
Exceeding expected level (Reading, Writing & Maths)
|
||||||
|
<MetricTooltip metricKey="rwm_high_pct" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.metricValue}>{formatPercentage(latestResults.rwm_high_pct)}</div>
|
||||||
|
{primaryAvg.rwm_high_pct != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: {primaryAvg.rwm_high_pct.toFixed(0)}%</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{latestResults.rwm_high_pct !== null && (
|
|
||||||
<div className={styles.metricCard}>
|
|
||||||
<div className={styles.metricLabel}>Exceeding expected level (RWM)</div>
|
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.rwm_high_pct)}</div>
|
|
||||||
<div className={styles.metricHint}>National avg: {NATIONAL_AVG.rwm_high}%</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Per-subject detail table */}
|
<div className={styles.metricGroupsGrid} style={{ marginTop: '1rem' }}>
|
||||||
<div className={styles.metricGroupsGrid} style={{ marginTop: '1rem' }}>
|
<div className={styles.metricGroup}>
|
||||||
<div className={styles.metricGroup}>
|
<h3 className={styles.metricGroupTitle}>Reading</h3>
|
||||||
<h3 className={styles.metricGroupTitle}>Reading</h3>
|
<div className={styles.metricTable}>
|
||||||
<div className={styles.metricTable}>
|
{latestResults.reading_expected_pct !== null && (
|
||||||
{latestResults.reading_expected_pct !== null && (
|
<div className={styles.metricRow}>
|
||||||
<div className={styles.metricRow}>
|
<span className={styles.metricName}>Expected level</span>
|
||||||
<span className={styles.metricName}>Expected level</span>
|
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_expected_pct)}</span>
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_expected_pct)}</span>
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.reading_high_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>Exceeding</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_high_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.reading_progress !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
Progress score
|
||||||
|
<MetricTooltip metricKey="reading_progress" />
|
||||||
|
</span>
|
||||||
|
<span className={`${styles.metricValue} ${progressClass(latestResults.reading_progress)}`}>
|
||||||
|
{formatProgress(latestResults.reading_progress)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.reading_avg_score !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
Average score
|
||||||
|
<MetricTooltip metricKey="reading_avg_score" />
|
||||||
|
</span>
|
||||||
|
<span className={styles.metricValue}>{latestResults.reading_avg_score.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.metricGroup}>
|
||||||
|
<h3 className={styles.metricGroupTitle}>Writing</h3>
|
||||||
|
<div className={styles.metricTable}>
|
||||||
|
{latestResults.writing_expected_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>Expected level</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_expected_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.writing_high_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>Exceeding</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_high_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.writing_progress !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
Progress score
|
||||||
|
<MetricTooltip metricKey="writing_progress" />
|
||||||
|
</span>
|
||||||
|
<span className={`${styles.metricValue} ${progressClass(latestResults.writing_progress)}`}>
|
||||||
|
{formatProgress(latestResults.writing_progress)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.metricGroup}>
|
||||||
|
<h3 className={styles.metricGroupTitle}>Maths</h3>
|
||||||
|
<div className={styles.metricTable}>
|
||||||
|
{latestResults.maths_expected_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>Expected level</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_expected_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.maths_high_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>Exceeding</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_high_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.maths_progress !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
Progress score
|
||||||
|
<MetricTooltip metricKey="maths_progress" />
|
||||||
|
</span>
|
||||||
|
<span className={`${styles.metricValue} ${progressClass(latestResults.maths_progress)}`}>
|
||||||
|
{formatProgress(latestResults.maths_progress)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.maths_avg_score !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
Average score
|
||||||
|
<MetricTooltip metricKey="maths_avg_score" />
|
||||||
|
</span>
|
||||||
|
<span className={styles.metricValue}>{latestResults.maths_avg_score.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && (
|
||||||
|
<p className={styles.progressNote}>
|
||||||
|
Progress scores measure how much pupils improved compared to similar schools nationally. Above 0 = better than average, below 0 = below average.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Secondary / KS4 content ── */}
|
||||||
|
{hasKS4Results && (
|
||||||
|
<>
|
||||||
|
<div className={styles.metricsGrid}>
|
||||||
|
{latestResults.attainment_8_score !== null && (
|
||||||
|
<div className={styles.metricCard}>
|
||||||
|
<div className={styles.metricLabel}>
|
||||||
|
Attainment 8
|
||||||
|
<MetricTooltip metricKey="attainment_8_score" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.metricValue}>{latestResults.attainment_8_score.toFixed(1)}</div>
|
||||||
|
{secondaryAvg.attainment_8_score != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: {secondaryAvg.attainment_8_score.toFixed(1)}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults.reading_high_pct !== null && (
|
{latestResults.progress_8_score !== null && (
|
||||||
<div className={styles.metricRow}>
|
<div className={styles.metricCard}>
|
||||||
<span className={styles.metricName}>Exceeding</span>
|
<div className={styles.metricLabel}>
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.reading_high_pct)}</span>
|
Progress 8
|
||||||
|
<MetricTooltip metricKey="progress_8_score" />
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.metricValue} ${progressClass(latestResults.progress_8_score)}`}>
|
||||||
|
{formatProgress(latestResults.progress_8_score)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.metricHint}>0 = national average</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults.reading_progress !== null && (
|
{latestResults.english_maths_standard_pass_pct !== null && (
|
||||||
<div className={styles.metricRow}>
|
<div className={styles.metricCard}>
|
||||||
<span className={styles.metricName}>Progress score</span>
|
<div className={styles.metricLabel}>
|
||||||
<span className={`${styles.metricValue} ${progressClass(latestResults.reading_progress)}`}>
|
English & Maths Grade 4+
|
||||||
{formatProgress(latestResults.reading_progress)}
|
<MetricTooltip metricKey="english_maths_standard_pass_pct" />
|
||||||
</span>
|
</div>
|
||||||
|
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_standard_pass_pct)}</div>
|
||||||
|
{secondaryAvg.english_maths_standard_pass_pct != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_standard_pass_pct.toFixed(0)}%</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults.reading_avg_score !== null && (
|
{latestResults.english_maths_strong_pass_pct !== null && (
|
||||||
<div className={styles.metricRow}>
|
<div className={styles.metricCard}>
|
||||||
<span className={styles.metricName}>Average score</span>
|
<div className={styles.metricLabel}>
|
||||||
<span className={styles.metricValue}>{latestResults.reading_avg_score.toFixed(1)}</span>
|
English & Maths Grade 5+
|
||||||
|
<MetricTooltip metricKey="english_maths_strong_pass_pct" />
|
||||||
|
</div>
|
||||||
|
<div className={styles.metricValue}>{formatPercentage(latestResults.english_maths_strong_pass_pct)}</div>
|
||||||
|
{secondaryAvg.english_maths_strong_pass_pct != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: {secondaryAvg.english_maths_strong_pass_pct.toFixed(0)}%</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.metricGroup}>
|
{/* EBacc */}
|
||||||
<h3 className={styles.metricGroupTitle}>Writing</h3>
|
{(latestResults.ebacc_entry_pct !== null || latestResults.ebacc_standard_pass_pct !== null) && (
|
||||||
<div className={styles.metricTable}>
|
<>
|
||||||
{latestResults.writing_expected_pct !== null && (
|
<h3 className={styles.subSectionTitle} style={{ marginTop: '1rem' }}>
|
||||||
<div className={styles.metricRow}>
|
English Baccalaureate (EBacc)
|
||||||
<span className={styles.metricName}>Expected level</span>
|
<MetricTooltip metricKey="ebacc_entry_pct" />
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_expected_pct)}</span>
|
</h3>
|
||||||
|
<div className={styles.metricTable}>
|
||||||
|
{latestResults.ebacc_entry_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>Pupils entered for EBacc</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_entry_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.ebacc_standard_pass_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
EBacc Grade 4+
|
||||||
|
<MetricTooltip metricKey="ebacc_standard_pass_pct" />
|
||||||
|
</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_standard_pass_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{latestResults.ebacc_strong_pass_pct !== null && (
|
||||||
|
<div className={styles.metricRow}>
|
||||||
|
<span className={styles.metricName}>
|
||||||
|
EBacc Grade 5+
|
||||||
|
<MetricTooltip metricKey="ebacc_strong_pass_pct" />
|
||||||
|
</span>
|
||||||
|
<span className={styles.metricValue}>{formatPercentage(latestResults.ebacc_strong_pass_pct)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
{latestResults.writing_high_pct !== null && (
|
)}
|
||||||
<div className={styles.metricRow}>
|
</>
|
||||||
<span className={styles.metricName}>Exceeding</span>
|
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.writing_high_pct)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{latestResults.writing_progress !== null && (
|
|
||||||
<div className={styles.metricRow}>
|
|
||||||
<span className={styles.metricName}>Progress score</span>
|
|
||||||
<span className={`${styles.metricValue} ${progressClass(latestResults.writing_progress)}`}>
|
|
||||||
{formatProgress(latestResults.writing_progress)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.metricGroup}>
|
|
||||||
<h3 className={styles.metricGroupTitle}>Maths</h3>
|
|
||||||
<div className={styles.metricTable}>
|
|
||||||
{latestResults.maths_expected_pct !== null && (
|
|
||||||
<div className={styles.metricRow}>
|
|
||||||
<span className={styles.metricName}>Expected level</span>
|
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_expected_pct)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{latestResults.maths_high_pct !== null && (
|
|
||||||
<div className={styles.metricRow}>
|
|
||||||
<span className={styles.metricName}>Exceeding</span>
|
|
||||||
<span className={styles.metricValue}>{formatPercentage(latestResults.maths_high_pct)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{latestResults.maths_progress !== null && (
|
|
||||||
<div className={styles.metricRow}>
|
|
||||||
<span className={styles.metricName}>Progress score</span>
|
|
||||||
<span className={`${styles.metricValue} ${progressClass(latestResults.maths_progress)}`}>
|
|
||||||
{formatProgress(latestResults.maths_progress)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{latestResults.maths_avg_score !== null && (
|
|
||||||
<div className={styles.metricRow}>
|
|
||||||
<span className={styles.metricName}>Average score</span>
|
|
||||||
<span className={styles.metricValue}>{latestResults.maths_avg_score.toFixed(1)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(latestResults.reading_progress !== null || latestResults.writing_progress !== null || latestResults.maths_progress !== null) && (
|
|
||||||
<p className={styles.progressNote}>
|
|
||||||
Progress scores measure how much pupils improved compared to similar schools nationally. Above 0 = better than average, below 0 = below average.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Year 1 Phonics */}
|
{/* Year 1 Phonics — primary only */}
|
||||||
{hasPhonics && phonics && (
|
{hasPhonics && isPrimary && phonics && (
|
||||||
<section id="phonics" className={styles.card}>
|
<section id="phonics" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(phonics.year)})</h2>
|
<h2 className={styles.sectionTitle}>Year 1 Phonics ({formatAcademicYear(phonics.year)})</h2>
|
||||||
<p className={styles.sectionSubtitle}>
|
<p className={styles.sectionSubtitle}>
|
||||||
@@ -466,7 +601,7 @@ export function SchoolDetailView({
|
|||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Passed the phonics check</div>
|
<div className={styles.metricLabel}>Passed the phonics check</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(phonics.year1_phonics_pct)}</div>
|
<div className={styles.metricValue}>{formatPercentage(phonics.year1_phonics_pct)}</div>
|
||||||
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.phonics_yr1}%</div>
|
<div className={styles.metricHint}>Phonics is a key early reading skill tested at end of Year 1</div>
|
||||||
</div>
|
</div>
|
||||||
{phonics.year2_phonics_pct != null && (
|
{phonics.year2_phonics_pct != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
@@ -487,21 +622,31 @@ export function SchoolDetailView({
|
|||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Average class size</div>
|
<div className={styles.metricLabel}>Average class size</div>
|
||||||
<div className={styles.metricValue}>{census.class_size_avg.toFixed(1)}</div>
|
<div className={styles.metricValue}>{census.class_size_avg.toFixed(1)}</div>
|
||||||
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.class_size} pupils</div>
|
<div className={styles.metricHint}>Average number of pupils per class</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{absenceData?.overall_absence_rate != null && (
|
{absenceData?.overall_absence_rate != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Days missed (overall absence)</div>
|
<div className={styles.metricLabel}>
|
||||||
|
Days missed (overall absence)
|
||||||
|
<MetricTooltip metricKey="overall_absence_pct" />
|
||||||
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
|
<div className={styles.metricValue}>{formatPercentage(absenceData.overall_absence_rate)}</div>
|
||||||
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.overall_absence}%</div>
|
{primaryAvg.overall_absence_pct != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: ~{primaryAvg.overall_absence_pct.toFixed(1)}%</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{absenceData?.persistent_absence_rate != null && (
|
{absenceData?.persistent_absence_rate != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Regularly missing school</div>
|
<div className={styles.metricLabel}>
|
||||||
|
Regularly missing school
|
||||||
|
<MetricTooltip metricKey="persistent_absence_pct" />
|
||||||
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
|
<div className={styles.metricValue}>{formatPercentage(absenceData.persistent_absence_rate)}</div>
|
||||||
<div className={styles.metricHint}>National avg: ~{NATIONAL_AVG.persistent_absence}%. Missing 10%+ of sessions.</div>
|
{primaryAvg.persistent_absence_pct != null && (
|
||||||
|
<div className={styles.metricHint}>National avg: ~{primaryAvg.persistent_absence_pct.toFixed(0)}%</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -515,7 +660,7 @@ export function SchoolDetailView({
|
|||||||
<div className={styles.metricsGrid}>
|
<div className={styles.metricsGrid}>
|
||||||
{admissions.published_admission_number != null && (
|
{admissions.published_admission_number != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Year 3 places per year</div>
|
<div className={styles.metricLabel}>{isSecondary ? 'Year 7' : 'Year 3'} places per year</div>
|
||||||
<div className={styles.metricValue}>{admissions.published_admission_number}</div>
|
<div className={styles.metricValue}>{admissions.published_admission_number}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -556,13 +701,19 @@ export function SchoolDetailView({
|
|||||||
)}
|
)}
|
||||||
{latestResults?.eal_pct != null && (
|
{latestResults?.eal_pct != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>English as an additional language</div>
|
<div className={styles.metricLabel}>
|
||||||
|
English as an additional language
|
||||||
|
<MetricTooltip metricKey="eal_pct" />
|
||||||
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.eal_pct)}</div>
|
<div className={styles.metricValue}>{formatPercentage(latestResults.eal_pct)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{latestResults?.sen_support_pct != null && (
|
{latestResults?.sen_support_pct != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Pupils with additional needs (SEN support)</div>
|
<div className={styles.metricLabel}>
|
||||||
|
Pupils receiving SEN support
|
||||||
|
<MetricTooltip metricKey="sen_support_pct" />
|
||||||
|
</div>
|
||||||
<div className={styles.metricValue}>{formatPercentage(latestResults.sen_support_pct)}</div>
|
<div className={styles.metricValue}>{formatPercentage(latestResults.sen_support_pct)}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -610,7 +761,10 @@ export function SchoolDetailView({
|
|||||||
{/* Local Area Context */}
|
{/* Local Area Context */}
|
||||||
{hasDeprivation && deprivation && (
|
{hasDeprivation && deprivation && (
|
||||||
<section id="local-area" className={styles.card}>
|
<section id="local-area" className={styles.card}>
|
||||||
<h2 className={styles.sectionTitle}>Local Area Context</h2>
|
<h2 className={styles.sectionTitle}>
|
||||||
|
Local Area Context
|
||||||
|
<MetricTooltip metricKey="idaci_decile" />
|
||||||
|
</h2>
|
||||||
<div className={styles.deprivationDots}>
|
<div className={styles.deprivationDots}>
|
||||||
{Array.from({ length: 10 }, (_, i) => (
|
{Array.from({ length: 10 }, (_, i) => (
|
||||||
<div
|
<div
|
||||||
@@ -639,7 +793,7 @@ export function SchoolDetailView({
|
|||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
<div className={styles.metricLabel}>Total spend per pupil per year</div>
|
<div className={styles.metricLabel}>Total spend per pupil per year</div>
|
||||||
<div className={styles.metricValue}>£{Math.round(finance.per_pupil_spend!).toLocaleString()}</div>
|
<div className={styles.metricValue}>£{Math.round(finance.per_pupil_spend!).toLocaleString()}</div>
|
||||||
<div className={styles.metricHint}>National avg: ~£{NATIONAL_AVG.per_pupil_spend.toLocaleString()}</div>
|
<div className={styles.metricHint}>How much the school has to spend on each pupil annually</div>
|
||||||
</div>
|
</div>
|
||||||
{finance.teacher_cost_pct != null && (
|
{finance.teacher_cost_pct != null && (
|
||||||
<div className={styles.metricCard}>
|
<div className={styles.metricCard}>
|
||||||
@@ -665,6 +819,7 @@ export function SchoolDetailView({
|
|||||||
<PerformanceChart
|
<PerformanceChart
|
||||||
data={yearlyData}
|
data={yearlyData}
|
||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
|
isSecondary={isSecondary}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{yearlyData.length > 1 && (
|
{yearlyData.length > 1 && (
|
||||||
@@ -675,22 +830,44 @@ export function SchoolDetailView({
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Year</th>
|
<th>Year</th>
|
||||||
<th>Reading, Writing & Maths (expected %)</th>
|
{isSecondary ? (
|
||||||
<th>Exceeding expected (%)</th>
|
<>
|
||||||
<th>Reading Progress</th>
|
<th>Attainment 8</th>
|
||||||
<th>Writing Progress</th>
|
<th>Progress 8</th>
|
||||||
<th>Maths Progress</th>
|
<th>English & Maths Grade 4+</th>
|
||||||
|
<th>English & Maths Grade 5+</th>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<th>Reading, Writing & Maths (expected %)</th>
|
||||||
|
<th>Exceeding expected (%)</th>
|
||||||
|
<th>Reading Progress</th>
|
||||||
|
<th>Writing Progress</th>
|
||||||
|
<th>Maths Progress</th>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{yearlyData.map((result) => (
|
{yearlyData.map((result) => (
|
||||||
<tr key={result.year}>
|
<tr key={result.year}>
|
||||||
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</td>
|
<td className={styles.yearCell}>{formatAcademicYear(result.year)}</td>
|
||||||
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
{isSecondary ? (
|
||||||
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
<>
|
||||||
<td>{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}</td>
|
<td>{result.attainment_8_score !== null ? result.attainment_8_score.toFixed(1) : '-'}</td>
|
||||||
<td>{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}</td>
|
<td>{result.progress_8_score !== null ? formatProgress(result.progress_8_score) : '-'}</td>
|
||||||
<td>{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}</td>
|
<td>{result.english_maths_standard_pass_pct !== null ? formatPercentage(result.english_maths_standard_pass_pct) : '-'}</td>
|
||||||
|
<td>{result.english_maths_strong_pass_pct !== null ? formatPercentage(result.english_maths_strong_pass_pct) : '-'}</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td>{result.rwm_expected_pct !== null ? formatPercentage(result.rwm_expected_pct) : '-'}</td>
|
||||||
|
<td>{result.rwm_high_pct !== null ? formatPercentage(result.rwm_high_pct) : '-'}</td>
|
||||||
|
<td>{result.reading_progress !== null ? formatProgress(result.reading_progress) : '-'}</td>
|
||||||
|
<td>{result.writing_progress !== null ? formatProgress(result.writing_progress) : '-'}</td>
|
||||||
|
<td>{result.maths_progress !== null ? formatProgress(result.maths_progress) : '-'}</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -98,6 +98,66 @@ export const METRIC_EXPLANATIONS: Record<string, MetricExplanation> = {
|
|||||||
plain: 'Difference in attainment between disadvantaged pupils and their peers',
|
plain: 'Difference in attainment between disadvantaged pupils and their peers',
|
||||||
detail: 'A smaller gap means the school is doing more to support disadvantaged pupils.',
|
detail: 'A smaller gap means the school is doing more to support disadvantaged pupils.',
|
||||||
},
|
},
|
||||||
|
sen_support_pct: {
|
||||||
|
label: 'SEN Support',
|
||||||
|
plain: '% of pupils receiving SEN (Special Educational Needs) support without a formal plan',
|
||||||
|
detail: 'These pupils need extra help but do not yet have an Education, Health and Care Plan (EHCP).',
|
||||||
|
},
|
||||||
|
sen_ehcp_pct: {
|
||||||
|
label: 'Education, Health and Care Plan (EHCP)',
|
||||||
|
plain: '% of pupils with a formal EHCP — a legal plan for pupils with significant additional needs',
|
||||||
|
},
|
||||||
|
eal_pct: {
|
||||||
|
label: 'English as an Additional Language',
|
||||||
|
plain: '% of pupils whose first language is not English',
|
||||||
|
},
|
||||||
|
idaci_decile: {
|
||||||
|
label: 'Deprivation (IDACI)',
|
||||||
|
plain: 'How deprived the area around the school is. Decile 1 = most deprived 10% of areas in England.',
|
||||||
|
detail: 'IDACI stands for Income Deprivation Affecting Children Index. It measures the proportion of children living in low-income households in an area.',
|
||||||
|
},
|
||||||
|
// ── KS4 / GCSE metrics ────────────────────────────────────────────────
|
||||||
|
attainment_8_score: {
|
||||||
|
label: 'Attainment 8',
|
||||||
|
plain: 'Average grade across a pupil\'s best 8 GCSEs, including English and Maths',
|
||||||
|
detail: 'Each GCSE grade is converted to a points score (grade 9 = 9 points, grade 1 = 1 point). The national average is around 46.',
|
||||||
|
},
|
||||||
|
progress_8_score: {
|
||||||
|
label: 'Progress 8',
|
||||||
|
plain: 'How much pupils improved from their primary school results to GCSE, compared to similar pupils nationally',
|
||||||
|
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: {
|
||||||
|
label: 'English & Maths — Grade 4+ (Standard Pass)',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
english_maths_strong_pass_pct: {
|
||||||
|
label: 'English & Maths — Grade 5+ (Strong Pass)',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
ebacc_entry_pct: {
|
||||||
|
label: 'EBacc Entry',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
ebacc_standard_pass_pct: {
|
||||||
|
label: 'EBacc — Grade 4+ (Standard Pass)',
|
||||||
|
plain: '% of pupils achieving grade 4 or above across all EBacc subjects',
|
||||||
|
},
|
||||||
|
ebacc_strong_pass_pct: {
|
||||||
|
label: 'EBacc — Grade 5+ (Strong Pass)',
|
||||||
|
plain: '% of pupils achieving grade 5 or above across all EBacc subjects',
|
||||||
|
},
|
||||||
|
ebacc_avg_score: {
|
||||||
|
label: 'EBacc Average Score',
|
||||||
|
plain: 'Average points score across all EBacc subject entries',
|
||||||
|
},
|
||||||
|
gcse_grade_91_pct: {
|
||||||
|
label: 'GCSE Grade 9–1 %',
|
||||||
|
plain: '% of GCSE entries where a pupil achieved a grade between 9 (highest) and 1',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,6 +48,13 @@ export interface School {
|
|||||||
// Location search fields
|
// Location search fields
|
||||||
distance?: number | null;
|
distance?: number | null;
|
||||||
|
|
||||||
|
// Phase (Primary, Secondary, All-through, etc.)
|
||||||
|
phase?: string | null;
|
||||||
|
|
||||||
|
// KS4 card metrics
|
||||||
|
attainment_8_score?: number | null;
|
||||||
|
english_maths_standard_pass_pct?: number | null;
|
||||||
|
|
||||||
// GIAS enrichment fields
|
// GIAS enrichment fields
|
||||||
website?: string | null;
|
website?: string | null;
|
||||||
headteacher_name?: string | null;
|
headteacher_name?: string | null;
|
||||||
@@ -225,6 +232,24 @@ export interface SchoolResult {
|
|||||||
rwm_expected_3yr_pct: number | null;
|
rwm_expected_3yr_pct: number | null;
|
||||||
reading_avg_3yr: number | null;
|
reading_avg_3yr: number | null;
|
||||||
maths_avg_3yr: number | null;
|
maths_avg_3yr: number | null;
|
||||||
|
|
||||||
|
// KS4 / GCSE metrics
|
||||||
|
attainment_8_score: number | null;
|
||||||
|
progress_8_score: number | null;
|
||||||
|
progress_8_lower_ci: number | null;
|
||||||
|
progress_8_upper_ci: number | null;
|
||||||
|
progress_8_english: number | null;
|
||||||
|
progress_8_maths: number | null;
|
||||||
|
progress_8_ebacc: number | null;
|
||||||
|
progress_8_open: number | null;
|
||||||
|
english_maths_strong_pass_pct: number | null;
|
||||||
|
english_maths_standard_pass_pct: number | null;
|
||||||
|
ebacc_entry_pct: number | null;
|
||||||
|
ebacc_strong_pass_pct: number | null;
|
||||||
|
ebacc_standard_pass_pct: number | null;
|
||||||
|
ebacc_avg_score: number | null;
|
||||||
|
gcse_grade_91_pct: number | null;
|
||||||
|
prior_attainment_avg: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -304,6 +329,13 @@ export interface Filters {
|
|||||||
local_authorities: string[];
|
local_authorities: string[];
|
||||||
school_types: string[];
|
school_types: string[];
|
||||||
years: number[];
|
years: number[];
|
||||||
|
phases: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NationalAverages {
|
||||||
|
year: number;
|
||||||
|
primary: Record<string, number>;
|
||||||
|
secondary: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend returns filters directly, not wrapped
|
// Backend returns filters directly, not wrapped
|
||||||
@@ -338,6 +370,7 @@ export interface SchoolSearchParams {
|
|||||||
search?: string;
|
search?: string;
|
||||||
local_authority?: string;
|
local_authority?: string;
|
||||||
school_type?: string;
|
school_type?: string;
|
||||||
|
phase?: string;
|
||||||
postcode?: string;
|
postcode?: string;
|
||||||
radius?: number;
|
radius?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user