Skip to content

feat(reports): Statistics Summary (AGP) + modern report charts - #8542

Draft
tim2000s wants to merge 20 commits into
nightscout:masterfrom
tim2000s:pr/reports-agp
Draft

feat(reports): Statistics Summary (AGP) + modern report charts#8542
tim2000s wants to merge 20 commits into
nightscout:masterfrom
tim2000s:pr/reports-agp

Conversation

@tim2000s

@tim2000s tim2000s commented Jun 16, 2026

Copy link
Copy Markdown

feat(reports): Statistics Summary (AGP) + modern report charts

⚠️ Draft — depends on #8539 (D3 v7 upgrade). This branch is stacked on pr/d3-v7, so the diff below currently also contains the D3 commits. Once #8539 merges I'll rebase onto master and the diff will shrink to the report files. Marking draft until then.

What this adds

  • New "Statistics Summary" report — a standard ambulatory glucose profile (AGP): glucose statistics (average, GMI in % and mmol/mol, CV, SD, % CGM active), a vertical consensus Time-in-Range bar, a 24-hour percentile chart (5/25/50/75/95, 15-minute bins, target-range shading), and a daily glucose-profile grid. Registered as the first report tab. Includes a Copy / Download control that exports a clinician-friendly plain-text summary.
  • Retire the abandoned Flot library from reports: percentile bands and the hourly-stats candlestick → D3 v7; the per-day pie → a stacked TIR bar. The existing D3 charts (day-to-day, week-to-week, calibrations) and loopalyzer get a consistent modern palette.
  • Distribution report: selectable range metrics — Time in Range (70–180), Tight (70–140), Normal (63–140), and Custom — with a TIR/TITR/TINR + GMI/CV summary and a vertical band bar.
  • Reports UX: auto-render the default 7-day report on open; render each tab lazily from the already-loaded data on first view.
  • Reports page chrome modernized (system fonts, pill tab nav, card-framed reports, cleaner tables). Kept light and print-friendly.

⚠️ Statistical corrections (intentional behaviour changes vs upstream)

These change displayed numbers/visuals on purpose, to fix pre-existing anomalies. Flagged explicitly for review:

  1. Hourly stats — box-and-whisker whiskers. Upstream draws the box from Q1–Q3 but the candlestick "wicks" from mean ± standard deviation — two incommensurable statistics. flotcandle.js only drew a wick when it extended beyond the body (if (highY < y + height) / if (lowY > y)), so for glucose data, where mean±SD usually sits inside the interquartile range, most hours rendered as a box with no whiskers at all and the anomaly stayed hidden. This PR makes it a correct box-and-whisker with whiskers at the 10th/90th percentile (by definition p10 ≤ Q1 and p90 ≥ Q3, so whiskers are always at or beyond the box edges). This is a deliberate correction, not a faithful port.

  2. Distribution — duplicate readings were double-counted. Upstream's dedup guard (seen.includes(item.displayTime) against an array that only ever receives string-keyed properties) never actually removes anything, so readings sharing a timestamp were counted multiple times in the band percentages and per-band statistics. This PR dedupes by displayTime in O(n) (matching the new AGP report and the main-view widget). For data containing duplicate-timestamp uploads, the reported percentages will shift slightly toward the correct values.

  3. Distribution — PGS in-range fraction (minor). In the consensus modes, PGS now uses the 70–180 in-range fraction rather than the report-target band; Custom mode is unchanged.

⚠️ Maintainer decision

The auto-render-on-open issues a default 7-day query whenever the reports page is opened (previously a manual "Show" click). It's a nice glanceable default but adds DB load on large/shared instances. Happy to gate it behind a setting if you'd prefer it opt-in.

Clinical correctness (verified)

GMI = 3.31 + 0.02392 × mean(mg/dL); CV = SD/mean × 100; SD via simple-statistics standard_deviation. TIR/TITR/TINR band boundaries follow the consensus definitions and are non-overlapping. AGP percentile binning is O(n). No $.plot (Flot) call remains in any report plugin.

Testing

  • Full reports integration suite passes.
  • All report plugins lint clean; production bundle builds.

🤖 Generated with Claude Code

Screenshots

Statistics Summary (AGP) — glucose stats, vertical Time-in-Range bar, 24h percentile profile, daily grid, and the Copy/Download export:

Statistics Summary / AGP report

Distribution — summary metrics on top, TIR/TITR/TINR/Custom mode selector, vertical band bar beside the per-band statistics, variability metrics below:

Distribution report

tim2000s and others added 16 commits February 15, 2018 08:55
Bumps d3 from ^5.16.0 to ^7.9.0 and migrates all client D3 usage to the v7 API:
the d3.event global was removed in v6, so every selection/brush/drag event
handler now takes (event, datum); d3.mouse(node) -> d3.pointer(event, node)
with touch-event unwrapping in the focus/context brush. Affects chart.js,
renderer.js and the day-to-day report. No behavioural change to the charts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New 'Statistics Summary' report plugin (ambulatory glucose profile): glucose
  stats (avg, GMI %/mmol-mol, CV, SD, % CGM active), vertical consensus TIR bar,
  24h percentile chart (5/25/50/75/95, 15-min bins, target shading) and a daily
  glucose-profile grid. Registered as the first report tab.
- Retire the abandoned Flot library from reports: percentile -> D3 v7 percentile
  chart; hourly stats candlestick -> D3 box-and-whisker; daily pie -> stacked TIR
  bar. Style refresh of the existing D3 charts (day-to-day, week-to-week,
  calibrations) and loopalyzer to a consistent modern palette.
- Distribution report: selectable range metrics (TIR / Tight 70-140 /
  Normal 63-140 / Custom), TIR/TITR/TINR + GMI/CV summary, vertical band bar.
- Reports UX: auto-render the default 7-day report on open; render each tab
  lazily from the loaded data on first view.
- Modernize the reports page chrome (report.css): system fonts, pill tab nav,
  card-framed reports, cleaner tables. Kept light + print-friendly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tim2000s and others added 4 commits June 16, 2026 12:42
Adds a Copy summary + Download control to the AGP report header that produces a
clinician-friendly plain-text summary (date range, % CGM active, readings,
average glucose, GMI %/mmol-mol, CV, SD, and the full Time-in-Range breakdown
with %/time-per-day). Copy uses the async clipboard API with an execCommand
fallback; Download writes a .txt via a Blob. Hidden when printing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t mean±SD)

The hourly-stats chart drew the box from Q1-Q3 but kept the legacy Flot
candlestick's mean±SD as 'whiskers', which for glucose data frequently fall
inside the interquartile box -> whisker caps rendered inside the box. Use the
10th/90th percentile for the whiskers instead; these are always at or beyond
the box edges (p10<=Q1, p90>=Q3), making it a correct box-and-whisker. Tooltip
now shows 90%/10% alongside Q3/median/Q1 and min/max.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d3 v7's package entry is ESM-only, so the test fixture's require('d3') threw
"Unexpected token 'export'" under Node's CommonJS require on Node 20/22/24
(it only worked locally on Node 25, which supports require(esm)). The webpack
bundle the harness already loads exposes a working window.d3, so reuse that
instead of requiring d3 from node_modules. Fixes CI for the D3 v7 upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
admintools.test.js has its own benv setup (separate from the headless fixture)
with its own require('d3'), which hits d3 v7's ESM-only entry and fails under
Node < 25. Reuse the window.d3 the bundle (loaded just above) already exposes,
matching the headless fixture fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant