On the Compare page, adding a school with no results data made every school's trend line vanish from the "Explore trends" chart — the schools only reappeared once that school was removed.
Root cause
A school with no performance rows still comes back from /api/compare with a single phantom yearly_data row (produced by the dim_school LEFT JOIN marts.fact_performance in data_loader.py) where year and every metric are null.
In buildCompareChart, Math.trunc(null) is 0, so the year axis was seeded at year 0. fillAcademicYears then walked 0, 101, 202, … and hit its 50-iteration safety cap long before reaching the real academic years (e.g. 202223). Result: the axis was 50 garbage years and every school's series mapped entirely to null — a blank chart.
The sections above the chart were unaffected (they read latest values null-safely), which is why only the trend lines disappeared.
Fix
Ignore yearly rows without a real numeric year when building both the axis (rawYears) and the per-school year map. A null/NaN year is not a data point.
Test
Added a regression test in compareChartData.test.ts: a no-results school (single year: null row) alongside three real schools must not pollute the axis — the real years drive the chart and all three real lines still render. Full suite (113 tests) and typecheck pass.
Note (not fixed here)
The deeper source is the backend emitting a phantom year: null row for no-results schools. Every current consumer guards against it except the chart, so this fix is localised to the chart. Worth a follow-up to drop that phantom row in the compare endpoint if we want defence in depth.
## The bug
On the Compare page, adding a school with **no results data** made every school's trend line vanish from the "Explore trends" chart — the schools only reappeared once that school was removed.
## Root cause
A school with no performance rows still comes back from `/api/compare` with a single **phantom `yearly_data` row** (produced by the `dim_school LEFT JOIN marts.fact_performance` in `data_loader.py`) where `year` and every metric are `null`.
In `buildCompareChart`, `Math.trunc(null)` is `0`, so the year axis was seeded at year 0. `fillAcademicYears` then walked `0, 101, 202, …` and hit its 50-iteration safety cap **long before reaching the real academic years** (e.g. `202223`). Result: the axis was 50 garbage years and every school's series mapped entirely to `null` — a blank chart.
The sections above the chart were unaffected (they read latest values null-safely), which is why only the trend lines disappeared.
## Fix
Ignore yearly rows without a real numeric year when building both the axis (`rawYears`) and the per-school year map. A null/`NaN` year is not a data point.
## Test
Added a regression test in `compareChartData.test.ts`: a no-results school (single `year: null` row) alongside three real schools must not pollute the axis — the real years drive the chart and all three real lines still render. Full suite (113 tests) and typecheck pass.
## Note (not fixed here)
The deeper source is the backend emitting a phantom `year: null` row for no-results schools. Every current consumer guards against it except the chart, so this fix is localised to the chart. Worth a follow-up to drop that phantom row in the compare endpoint if we want defence in depth.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adding a school with no performance data to a comparison made every
school's trend line disappear until that school was removed.
Root cause: /api/compare returns such a school with a single phantom
yearly_data row (the dim_school LEFT JOIN) whose year is null. In
buildCompareChart, Math.trunc(null) is 0, so the axis was seeded at
year 0; fillAcademicYears then walked 0, 101, 202, … and hit its
50-step cap long before reaching the real years, leaving every
school's series mapped entirely to null.
Fix: ignore yearly rows without a real numeric year when building the
axis and the per-school year map. Regression test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This PR fixes a bug where a school with no performance data returns a single phantom yearly_data row (year=null from the LEFT JOIN), which previously got coerced to year 0 via Math.trunc(null) and blew out the chart's axis range, blanking every school's line. The fix adds a hasYear() guard filtering out non-finite/non-number years before axis computation and per-school data mapping, with a matching regression test.
✅ No issues found.
## 🤖 AI Code Review (Claude Code)
This PR fixes a bug where a school with no performance data returns a single phantom yearly_data row (year=null from the LEFT JOIN), which previously got coerced to year 0 via Math.trunc(null) and blew out the chart's axis range, blanking every school's line. The fix adds a hasYear() guard filtering out non-finite/non-number years before axis computation and per-school data mapping, with a matching regression test.
✅ No issues found.
tudor
merged commit 598ba9af4e into main2026-07-22 14:23:25 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The bug
On the Compare page, adding a school with no results data made every school's trend line vanish from the "Explore trends" chart — the schools only reappeared once that school was removed.
Root cause
A school with no performance rows still comes back from
/api/comparewith a single phantomyearly_datarow (produced by thedim_school LEFT JOIN marts.fact_performanceindata_loader.py) whereyearand every metric arenull.In
buildCompareChart,Math.trunc(null)is0, so the year axis was seeded at year 0.fillAcademicYearsthen walked0, 101, 202, …and hit its 50-iteration safety cap long before reaching the real academic years (e.g.202223). Result: the axis was 50 garbage years and every school's series mapped entirely tonull— a blank chart.The sections above the chart were unaffected (they read latest values null-safely), which is why only the trend lines disappeared.
Fix
Ignore yearly rows without a real numeric year when building both the axis (
rawYears) and the per-school year map. A null/NaNyear is not a data point.Test
Added a regression test in
compareChartData.test.ts: a no-results school (singleyear: nullrow) alongside three real schools must not pollute the axis — the real years drive the chart and all three real lines still render. Full suite (113 tests) and typecheck pass.Note (not fixed here)
The deeper source is the backend emitting a phantom
year: nullrow for no-results schools. Every current consumer guards against it except the chart, so this fix is localised to the chart. Worth a follow-up to drop that phantom row in the compare endpoint if we want defence in depth.🤖 Generated with Claude Code
🤖 AI Code Review (Claude Code)
This PR fixes a bug where a school with no performance data returns a single phantom yearly_data row (year=null from the LEFT JOIN), which previously got coerced to year 0 via Math.trunc(null) and blew out the chart's axis range, blanking every school's line. The fix adds a hasYear() guard filtering out non-finite/non-number years before axis computation and per-school data mapping, with a matching regression test.
✅ No issues found.