/** * Pure series-building for the comparison trend chart, extracted from * ComparisonChart so it is unit-testable without a canvas. * * Chart truthfulness rules (spec §8.1): every academic year between the * first and last data point appears on the axis — cancelled test years * (2019/20, 2020/21) and the unpublished 2021/22 school-level year render * as real gaps, never as compressed time; school lines never bridge gaps. */ import type { ComparisonData } from './types'; /** 201819 → 201920 (academic-year arithmetic on YYYYYY codes). */ function nextAcademicYear(year: number): number { const start = Math.floor(year / 100); const end = year % 100; return (start + 1) * 100 + (end + 1); } /** Every academic year from min(years) to max(years), inclusive. */ export function fillAcademicYears(years: number[]): number[] { if (years.length === 0) return []; const ints = [...new Set(years.map((y) => Math.trunc(y)))].sort((a, b) => a - b); const out: number[] = []; let y = ints[0]; const last = ints[ints.length - 1]; while (y <= last && out.length < 50) { out.push(y); y = nextAcademicYear(y); } return out; } export interface CompareChartSeries { label: string; data: Array; /** Index into CHART_COLORS / point styles. */ schoolIndex: number; spanGaps: false; } export interface EnglandSeries { label: 'England average'; data: Array; borderDash: [number, number]; spanGaps: false; } export interface CompareChart { years: number[]; schoolDatasets: CompareChartSeries[]; englandDataset: EnglandSeries | null; /** True when England published a 2021/22 figure but no school has one — * the UI shows: "DfE didn't publish school-level figures for 2021/22". */ showUnpublished202122Note: boolean; /** Years where the England overlay has a value but no school does — the * chart shows a dashed-line-only stretch that needs explaining (KS2 and * KS4 have different honest explanations, so the component owns the copy). */ englandOnlyYears: number[]; } export function buildCompareChart( comparisonData: Record, schools: Array<{ urn: number; school_name: string }>, metric: string, nationalByYear?: Record, ): CompareChart { const rawYears = schools.flatMap( (s) => comparisonData[String(s.urn)]?.yearly_data.map((d) => Math.trunc(d.year)) ?? [], ); const years = fillAcademicYears(rawYears); const schoolDatasets: CompareChartSeries[] = schools.map((school, schoolIndex) => { const rows = comparisonData[String(school.urn)]?.yearly_data ?? []; const byYear = new Map>(); for (const row of rows) byYear.set(Math.trunc(row.year), row as unknown as Record); return { label: school.school_name, data: years.map((year) => { const v = byYear.get(year)?.[metric]; return typeof v === 'number' && !Number.isNaN(v) ? v : null; }), schoolIndex, spanGaps: false, }; }); let englandDataset: EnglandSeries | null = null; if (nationalByYear) { const data = years.map((year) => { const v = nationalByYear[year]; return typeof v === 'number' && !Number.isNaN(v) ? v : null; }); if (data.some((v) => v != null)) { englandDataset = { label: 'England average', data, borderDash: [5, 4], spanGaps: false }; } } const englandOnlyYears = years.filter( (year, i) => englandDataset?.data[i] != null && schoolDatasets.every((ds) => ds.data[i] == null), ); const showUnpublished202122Note = englandOnlyYears.includes(202122); return { years, schoolDatasets, englandDataset, showUnpublished202122Note, englandOnlyYears }; }