feat(ux): 8 UX improvements — simpler home, advanced filters, phase tabs, 4-line rows
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
All checks were successful
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 48s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 1m13s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 32s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
1. Simpler home page: only search box on landing, no filter dropdowns 2. Advanced filters: hidden behind toggle on results page, auto-open if active 3. Per-school phase rendering: each row renders based on its own data 4. Taller 4-line rows with context line (type, age range, denomination, gender) 5. Result-scoped filters: dropdown values reflect current search results 6. Fix blank filter values: exclude empty strings and "Not applicable" 7. Rankings: Primary/Secondary phase tabs with phase-specific metrics 8. Compare: Primary/Secondary tabs with school counts and phase metrics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,17 @@ from .data_loader import get_data_info as get_db_info
|
||||
from .schemas import METRIC_DEFINITIONS, RANKING_COLUMNS, SCHOOL_COLUMNS
|
||||
from .utils import clean_for_json
|
||||
|
||||
# Values to exclude from filter dropdowns (empty strings, non-applicable labels)
|
||||
EXCLUDED_FILTER_VALUES = {"", "Not applicable", "Does not apply"}
|
||||
|
||||
|
||||
def clean_filter_values(series: pd.Series) -> list[str]:
|
||||
"""Return sorted unique values from a Series, excluding NaN and junk labels."""
|
||||
return sorted(
|
||||
v for v in series.dropna().unique().tolist()
|
||||
if v not in EXCLUDED_FILTER_VALUES
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SECURITY MIDDLEWARE & HELPERS
|
||||
@@ -380,6 +391,15 @@ async def get_schools(
|
||||
schools_df["school_type"].str.lower() == school_type.lower()
|
||||
]
|
||||
|
||||
# Compute result-scoped filter values (before pagination)
|
||||
result_filters = {
|
||||
"local_authorities": clean_filter_values(schools_df["local_authority"]) if "local_authority" in schools_df.columns else [],
|
||||
"school_types": clean_filter_values(schools_df["school_type"]) if "school_type" in schools_df.columns else [],
|
||||
"phases": clean_filter_values(schools_df["phase"]) if "phase" in schools_df.columns else [],
|
||||
"genders": clean_filter_values(schools_df["gender"]) if "gender" in schools_df.columns else [],
|
||||
"admissions_policies": clean_filter_values(schools_df["admissions_policy"]) if "admissions_policy" in schools_df.columns else [],
|
||||
}
|
||||
|
||||
# Pagination
|
||||
total = len(schools_df)
|
||||
start_idx = (page - 1) * page_size
|
||||
@@ -392,6 +412,7 @@ async def get_schools(
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
|
||||
"result_filters": result_filters,
|
||||
"location_info": {
|
||||
"postcode": postcode,
|
||||
"radius": radius * 1.60934, # Convert miles to km for frontend display
|
||||
@@ -507,7 +528,11 @@ async def compare_schools(
|
||||
"urn": urn,
|
||||
"school_name": latest.get("school_name", ""),
|
||||
"local_authority": latest.get("local_authority", ""),
|
||||
"school_type": latest.get("school_type", ""),
|
||||
"address": latest.get("address", ""),
|
||||
"phase": latest.get("phase", ""),
|
||||
"attainment_8_score": float(latest["attainment_8_score"]) if pd.notna(latest.get("attainment_8_score")) else None,
|
||||
"rwm_expected_pct": float(latest["rwm_expected_pct"]) if pd.notna(latest.get("rwm_expected_pct")) else None,
|
||||
},
|
||||
"yearly_data": clean_for_json(school_data),
|
||||
}
|
||||
@@ -529,15 +554,15 @@ async def get_filter_options(request: Request):
|
||||
}
|
||||
|
||||
# Phases: return values from data, ordered sensibly
|
||||
phases = sorted(df["phase"].dropna().unique().tolist()) if "phase" in df.columns else []
|
||||
phases = clean_filter_values(df["phase"]) if "phase" in df.columns else []
|
||||
|
||||
secondary_df = df[df["attainment_8_score"].notna()] if "attainment_8_score" in df.columns else df.iloc[0:0]
|
||||
genders = sorted(secondary_df["gender"].dropna().unique().tolist()) if "gender" in secondary_df.columns else []
|
||||
admissions_policies = sorted(secondary_df["admissions_policy"].dropna().unique().tolist()) if "admissions_policy" in secondary_df.columns else []
|
||||
genders = clean_filter_values(secondary_df["gender"]) if "gender" in secondary_df.columns else []
|
||||
admissions_policies = clean_filter_values(secondary_df["admissions_policy"]) if "admissions_policy" in secondary_df.columns else []
|
||||
|
||||
return {
|
||||
"local_authorities": sorted(df["local_authority"].dropna().unique().tolist()),
|
||||
"school_types": sorted(df["school_type"].dropna().unique().tolist()),
|
||||
"local_authorities": clean_filter_values(df["local_authority"]) if "local_authority" in df.columns else [],
|
||||
"school_types": clean_filter_values(df["school_type"]) if "school_type" in df.columns else [],
|
||||
"years": sorted(df["year"].dropna().unique().tolist()),
|
||||
"phases": phases,
|
||||
"genders": genders,
|
||||
@@ -641,6 +666,9 @@ async def get_rankings(
|
||||
local_authority: Optional[str] = Query(
|
||||
None, description="Filter by local authority", max_length=100
|
||||
),
|
||||
phase: Optional[str] = Query(
|
||||
None, description="Filter by phase: primary or secondary", max_length=20
|
||||
),
|
||||
):
|
||||
"""Get school rankings by a specific metric."""
|
||||
# Sanitize local authority input
|
||||
@@ -670,6 +698,12 @@ async def get_rankings(
|
||||
if local_authority:
|
||||
df = df[df["local_authority"].str.lower() == local_authority.lower()]
|
||||
|
||||
# Filter by phase
|
||||
if phase == "primary" and "rwm_expected_pct" in df.columns:
|
||||
df = df[df["rwm_expected_pct"].notna()]
|
||||
elif phase == "secondary" and "attainment_8_score" in df.columns:
|
||||
df = df[df["attainment_8_score"].notna()]
|
||||
|
||||
# Sort and rank (exclude rows with no data for this metric)
|
||||
df = df.dropna(subset=[metric])
|
||||
total = len(df)
|
||||
|
||||
Reference in New Issue
Block a user