diff --git a/backend/app.py b/backend/app.py index 08895be..1d86a6f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -676,9 +676,6 @@ async def get_national_averages(request: Request): 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", @@ -703,15 +700,30 @@ async def get_national_averages(request: Request): out[col] = round(float(val.mean()), 2) return out + latest_year = int(df["year"].max()) + df_latest = df[df["year"] == latest_year] + # 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()] + # Per-year averages for every year in the dataset (used by chart reference lines) + by_year = [] + for yr in sorted(df["year"].dropna().unique()): + yr = int(yr) + df_yr = df[df["year"] == yr] + by_year.append({ + "year": yr, + "primary": _means(df_yr[df_yr["rwm_expected_pct"].notna()], ks2_metrics), + "secondary": _means(df_yr[df_yr["attainment_8_score"].notna()], ks4_metrics), + }) + return { "year": latest_year, "primary": _means(primary_df, ks2_metrics), "secondary": _means(secondary_df, ks4_metrics), + "by_year": by_year, } diff --git a/nextjs-app/components/PerformanceChart.tsx b/nextjs-app/components/PerformanceChart.tsx index 300a3f1..4ca0a0f 100644 --- a/nextjs-app/components/PerformanceChart.tsx +++ b/nextjs-app/components/PerformanceChart.tsx @@ -24,14 +24,22 @@ import styles from './PerformanceChart.module.css'; ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); +interface NationalByYear { + year: number; + primary: Record; + secondary: Record; +} + interface PerformanceChartProps { data: SchoolResult[]; schoolName: string; isSecondary?: boolean; - /** National average RWM expected % — rendered as a dashed reference line */ + /** National average RWM expected % for the latest year — fallback if no by_year data */ nationalRwmAvg?: number | null; - /** National average Attainment 8 — rendered as a dashed reference line */ + /** National average Attainment 8 for the latest year — fallback if no by_year data */ nationalAtt8Avg?: number | null; + /** Per-year national averages — used to draw a changing reference line */ + nationalByYear?: NationalByYear[]; } // Academic years when SATs/GCSEs were cancelled due to COVID @@ -42,10 +50,30 @@ export function PerformanceChart({ isSecondary = false, nationalRwmAvg, nationalAtt8Avg, + nationalByYear, }: PerformanceChartProps) { const sortedData = [...data].sort((a, b) => a.year - b.year); const years = sortedData.map(d => formatAcademicYear(d.year)); + // Build per-year national average series aligned to the school's data years. + // Falls back to a flat line using the scalar prop if by_year isn't available. + const natRefRwm: (number | null)[] = sortedData.map(d => { + if (nationalByYear) { + const match = nationalByYear.find(n => n.year === d.year); + return match?.primary?.rwm_expected_pct ?? null; + } + return nationalRwmAvg ?? null; + }); + const natRefAtt8: (number | null)[] = sortedData.map(d => { + if (nationalByYear) { + const match = nationalByYear.find(n => n.year === d.year); + return match?.secondary?.attainment_8_score ?? null; + } + return nationalAtt8Avg ?? null; + }); + const hasNatRwm = natRefRwm.some(v => v != null); + const hasNatAtt8 = natRefAtt8.some(v => v != null); + // ── Trend summary (primary only) ────────────────────────────────────── const trendSummary = (() => { if (isSecondary) return null; @@ -114,10 +142,10 @@ export function PerformanceChart({ hidden: true, yAxisID: 'y1', }, - ...(nationalAtt8Avg != null ? [{ + ...(hasNatAtt8 ? [{ ...refLineStyle, label: 'National average', - data: sortedData.map(() => nationalAtt8Avg!), + data: natRefAtt8, yAxisID: 'y', } as ChartDataset<'line'>] : []), ] : [ @@ -142,10 +170,10 @@ export function PerformanceChart({ pointRadius: 3, yAxisID: 'y', }, - ...(nationalRwmAvg != null ? [{ + ...(hasNatRwm ? [{ ...refLineStyle, label: 'National average', - data: sortedData.map(() => nationalRwmAvg!), + data: natRefRwm, yAxisID: 'y', } as ChartDataset<'line'>] : []), { diff --git a/nextjs-app/components/SchoolDetailView.tsx b/nextjs-app/components/SchoolDetailView.tsx index 99782dd..ce1f150 100644 --- a/nextjs-app/components/SchoolDetailView.tsx +++ b/nextjs-app/components/SchoolDetailView.tsx @@ -1030,6 +1030,7 @@ export function SchoolDetailView({ isSecondary={isSecondary} nationalRwmAvg={isPrimary ? (primaryAvg.rwm_expected_pct ?? null) : null} nationalAtt8Avg={isSecondary ? (secondaryAvg.attainment_8_score ?? null) : null} + nationalByYear={nationalAvg?.by_year} /> {yearlyData.length > 1 && ( diff --git a/nextjs-app/components/SecondarySchoolDetailView.tsx b/nextjs-app/components/SecondarySchoolDetailView.tsx index 7219f82..00e124a 100644 --- a/nextjs-app/components/SecondarySchoolDetailView.tsx +++ b/nextjs-app/components/SecondarySchoolDetailView.tsx @@ -618,6 +618,7 @@ export function SecondarySchoolDetailView({ schoolName={schoolInfo.school_name} isSecondary={true} nationalAtt8Avg={heroAtt8Nat} + nationalByYear={nationalAvg?.by_year} /> diff --git a/nextjs-app/lib/types.ts b/nextjs-app/lib/types.ts index 7ead4bf..eddb1b1 100644 --- a/nextjs-app/lib/types.ts +++ b/nextjs-app/lib/types.ts @@ -351,12 +351,17 @@ export interface Filters { admissions_policies: string[]; } -export interface NationalAverages { +export interface NationalAveragesYear { year: number; primary: Record; secondary: Record; } +export interface NationalAverages extends NationalAveragesYear { + /** Per-year averages for every year in the dataset, used for chart reference lines. */ + by_year: NationalAveragesYear[]; +} + export interface LAaveragesResponse { year: number; secondary: {