feat: national average reference line now tracks per year on history chart
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 24s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m52s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 24s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 51s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 1m52s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s
Previously the dashed reference line was a flat horizontal at the latest year's national average across all historical data, implying the national figure was constant. Now the backend returns per-year averages in `by_year` and the chart maps each data year to its own national average, so the reference line correctly reflects how the national picture changed over time (including COVID recovery dip/recovery). - backend: /api/national-averages now includes `by_year` list alongside existing `year`/`primary`/`secondary` latest-year snapshot - types: NationalAverages extended with `by_year: NationalAveragesYear[]` - PerformanceChart: accepts `nationalByYear` prop; builds per-year series aligned to school data years, falling back to scalar prop if absent - SchoolDetailView + SecondarySchoolDetailView: pass `nationalAvg.by_year` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -676,9 +676,6 @@ async def get_national_averages(request: Request):
|
|||||||
if df.empty:
|
if df.empty:
|
||||||
return {"primary": {}, "secondary": {}}
|
return {"primary": {}, "secondary": {}}
|
||||||
|
|
||||||
latest_year = int(df["year"].max())
|
|
||||||
df_latest = df[df["year"] == latest_year]
|
|
||||||
|
|
||||||
ks2_metrics = [
|
ks2_metrics = [
|
||||||
"rwm_expected_pct", "rwm_high_pct",
|
"rwm_expected_pct", "rwm_high_pct",
|
||||||
"reading_expected_pct", "writing_expected_pct", "maths_expected_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)
|
out[col] = round(float(val.mean()), 2)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
latest_year = int(df["year"].max())
|
||||||
|
df_latest = df[df["year"] == latest_year]
|
||||||
|
|
||||||
# Primary: schools where KS2 data is non-null
|
# Primary: schools where KS2 data is non-null
|
||||||
primary_df = df_latest[df_latest["rwm_expected_pct"].notna()]
|
primary_df = df_latest[df_latest["rwm_expected_pct"].notna()]
|
||||||
# Secondary: schools where KS4 data is non-null
|
# Secondary: schools where KS4 data is non-null
|
||||||
secondary_df = df_latest[df_latest["attainment_8_score"].notna()]
|
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 {
|
return {
|
||||||
"year": latest_year,
|
"year": latest_year,
|
||||||
"primary": _means(primary_df, ks2_metrics),
|
"primary": _means(primary_df, ks2_metrics),
|
||||||
"secondary": _means(secondary_df, ks4_metrics),
|
"secondary": _means(secondary_df, ks4_metrics),
|
||||||
|
"by_year": by_year,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,22 @@ import styles from './PerformanceChart.module.css';
|
|||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||||
|
|
||||||
|
interface NationalByYear {
|
||||||
|
year: number;
|
||||||
|
primary: Record<string, number>;
|
||||||
|
secondary: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
interface PerformanceChartProps {
|
interface PerformanceChartProps {
|
||||||
data: SchoolResult[];
|
data: SchoolResult[];
|
||||||
schoolName: string;
|
schoolName: string;
|
||||||
isSecondary?: boolean;
|
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;
|
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;
|
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
|
// Academic years when SATs/GCSEs were cancelled due to COVID
|
||||||
@@ -42,10 +50,30 @@ export function PerformanceChart({
|
|||||||
isSecondary = false,
|
isSecondary = false,
|
||||||
nationalRwmAvg,
|
nationalRwmAvg,
|
||||||
nationalAtt8Avg,
|
nationalAtt8Avg,
|
||||||
|
nationalByYear,
|
||||||
}: PerformanceChartProps) {
|
}: PerformanceChartProps) {
|
||||||
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 => formatAcademicYear(d.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) ──────────────────────────────────────
|
// ── Trend summary (primary only) ──────────────────────────────────────
|
||||||
const trendSummary = (() => {
|
const trendSummary = (() => {
|
||||||
if (isSecondary) return null;
|
if (isSecondary) return null;
|
||||||
@@ -114,10 +142,10 @@ export function PerformanceChart({
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
yAxisID: 'y1',
|
yAxisID: 'y1',
|
||||||
},
|
},
|
||||||
...(nationalAtt8Avg != null ? [{
|
...(hasNatAtt8 ? [{
|
||||||
...refLineStyle,
|
...refLineStyle,
|
||||||
label: 'National average',
|
label: 'National average',
|
||||||
data: sortedData.map(() => nationalAtt8Avg!),
|
data: natRefAtt8,
|
||||||
yAxisID: 'y',
|
yAxisID: 'y',
|
||||||
} as ChartDataset<'line'>] : []),
|
} as ChartDataset<'line'>] : []),
|
||||||
] : [
|
] : [
|
||||||
@@ -142,10 +170,10 @@ export function PerformanceChart({
|
|||||||
pointRadius: 3,
|
pointRadius: 3,
|
||||||
yAxisID: 'y',
|
yAxisID: 'y',
|
||||||
},
|
},
|
||||||
...(nationalRwmAvg != null ? [{
|
...(hasNatRwm ? [{
|
||||||
...refLineStyle,
|
...refLineStyle,
|
||||||
label: 'National average',
|
label: 'National average',
|
||||||
data: sortedData.map(() => nationalRwmAvg!),
|
data: natRefRwm,
|
||||||
yAxisID: 'y',
|
yAxisID: 'y',
|
||||||
} as ChartDataset<'line'>] : []),
|
} as ChartDataset<'line'>] : []),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1030,6 +1030,7 @@ export function SchoolDetailView({
|
|||||||
isSecondary={isSecondary}
|
isSecondary={isSecondary}
|
||||||
nationalRwmAvg={isPrimary ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
nationalRwmAvg={isPrimary ? (primaryAvg.rwm_expected_pct ?? null) : null}
|
||||||
nationalAtt8Avg={isSecondary ? (secondaryAvg.attainment_8_score ?? null) : null}
|
nationalAtt8Avg={isSecondary ? (secondaryAvg.attainment_8_score ?? null) : null}
|
||||||
|
nationalByYear={nationalAvg?.by_year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{yearlyData.length > 1 && (
|
{yearlyData.length > 1 && (
|
||||||
|
|||||||
@@ -618,6 +618,7 @@ export function SecondarySchoolDetailView({
|
|||||||
schoolName={schoolInfo.school_name}
|
schoolName={schoolInfo.school_name}
|
||||||
isSecondary={true}
|
isSecondary={true}
|
||||||
nationalAtt8Avg={heroAtt8Nat}
|
nationalAtt8Avg={heroAtt8Nat}
|
||||||
|
nationalByYear={nationalAvg?.by_year}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -351,12 +351,17 @@ export interface Filters {
|
|||||||
admissions_policies: string[];
|
admissions_policies: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NationalAverages {
|
export interface NationalAveragesYear {
|
||||||
year: number;
|
year: number;
|
||||||
primary: Record<string, number>;
|
primary: Record<string, number>;
|
||||||
secondary: Record<string, number>;
|
secondary: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface LAaveragesResponse {
|
||||||
year: number;
|
year: number;
|
||||||
secondary: {
|
secondary: {
|
||||||
|
|||||||
Reference in New Issue
Block a user