fix(detail): gap-honest year axis on the detail-page trend charts (Batch D)
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m8s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 12s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m38s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m8s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 12s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m38s
Applies the compare screen's chart-truthfulness rules (spec §8.1) to the school detail page's time-series charts. PerformanceChart (Results Over Time, both phases): - Fill every academic year between the first and last data point via the shared fillAcademicYears helper, so cancelled/unpublished years (2019/20, 2020/21, and — for KS2 — 2021/22) render as real gaps instead of compressed time. Each series and the England overlay map onto this filled axis with null for missing years; spanGaps:false so school lines never bridge a gap. - Replace the primary-only COVID note with a distinct, honest gap caption: KS2 names the cancelled tests plus the unpublished 2021/22 school-level year; KS4 names the unpublished 2019/20–2020/21 GCSE grading years. - Mobile x-axis switches to autoSkip so the longer (gap-honest) axis stays readable; the broken line still marks a missing year even when its tick label is skipped. AdmissionsTrendChart: - Same gap-honest axis + spanGaps:false so a missing admissions year is a real gap, not compressed time. Point 12 (definite canvas heights): desktop is already a definite 280px; fix the secondary detail's mobile .chartContainer, which fixed the outer box at 220px and double-constrained PerformanceChart's own 220px canvas + chip strip (clipping the chips onto the plot) — now height:auto to match the primary view. SatsChart is out of scope (single-year per-subject CSS bars — no year axis, no canvas height to constrain). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -10,6 +10,7 @@ import { Line } from 'react-chartjs-2';
|
||||
import { ChartOptions } from 'chart.js';
|
||||
import '@/lib/chartSetup';
|
||||
import { formatAcademicYear } from '@/lib/utils';
|
||||
import { fillAcademicYears } from '@/lib/compareChartData';
|
||||
import type { SchoolAdmissions } from '@/lib/types';
|
||||
import styles from './AdmissionsTrendChart.module.css';
|
||||
|
||||
@@ -17,13 +18,22 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis
|
||||
const pts = history.filter((h) => h.first_preference_offer_pct != null);
|
||||
if (pts.length < 2) return null;
|
||||
|
||||
const labels = pts.map((p) => formatAcademicYear(p.year));
|
||||
const values = pts.map((p) => p.first_preference_offer_pct as number);
|
||||
const lastIdx = pts.length - 1;
|
||||
// Gap-honest axis: every academic year between the first and last data point
|
||||
// appears, so a missing admissions year renders as a real gap (spanGaps:false)
|
||||
// rather than compressing time between distant years.
|
||||
const axisYears = fillAcademicYears(pts.map((p) => p.year));
|
||||
const byYear = new Map(pts.map((p) => [p.year, p.first_preference_offer_pct as number]));
|
||||
const labels = axisYears.map(formatAcademicYear);
|
||||
const values: (number | null)[] = axisYears.map((y) => byYear.get(y) ?? null);
|
||||
const present = values
|
||||
.map((v, i) => (v != null ? i : -1))
|
||||
.filter((i) => i >= 0);
|
||||
const lastIdx = present[present.length - 1];
|
||||
|
||||
// Auto-scale with headroom so variation is visible, clamped to 0–100.
|
||||
const lo = Math.min(...values);
|
||||
const hi = Math.max(...values);
|
||||
const numeric = values.filter((v): v is number => v != null);
|
||||
const lo = Math.min(...numeric);
|
||||
const hi = Math.max(...numeric);
|
||||
const padded = Math.max(5, Math.round((hi - lo) * 0.25));
|
||||
const yMin = Math.max(0, Math.floor((lo - padded) / 5) * 5);
|
||||
const yMax = Math.min(100, Math.ceil((hi + padded) / 5) * 5);
|
||||
@@ -69,15 +79,16 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis
|
||||
label: 'First-choice offer rate',
|
||||
data: values,
|
||||
clip: false as const,
|
||||
spanGaps: false,
|
||||
borderColor: '#e07256',
|
||||
backgroundColor: 'rgba(224,114,86,0.10)',
|
||||
borderWidth: 2.5,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: pts.map((_, i) => (i === lastIdx ? 5 : 3)),
|
||||
pointRadius: values.map((_, i) => (i === lastIdx ? 5 : 3)),
|
||||
pointBackgroundColor: '#e07256',
|
||||
pointBorderColor: '#fff',
|
||||
pointBorderWidth: pts.map((_, i) => (i === lastIdx ? 2 : 0)),
|
||||
pointBorderWidth: values.map((_, i) => (i === lastIdx ? 2 : 0)),
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user