GlucoseIQ
Glucose analytics in TypeScript, with separate checks for usable readings and sufficient data.
Glucose metrics without a chart dependency
Continuous glucose monitors produce a reading every five minutes. Turning that stream into something a person can act on requires metrics from the clinical literature. Most teams building diabetes software end up reimplementing them.
GlucoseIQ builds on Diabetic Utils with five packages, a size budget, and total coverage on the core. The headless core computes and returns data; optional packages layer on top. Nothing in the core renders to a screen or reaches for the network.
- Time in Range, AGP percentile bands, and variability metrics rewritten per project
- Unit handling that silently mixes mg/dL and mmol/L in the same calculation
- Analytics welded to a chart library, so the numbers cannot be used anywhere else
- Vendor exports from Dexcom, Libre, and Nightscout each parsed by hand
- No way to test the pipeline without real patient data
In this case study
Usable readings can still be incomplete
A week of patchy readings can produce a Time in Range number without enough coverage for a clinical summary. GlucoseIQ gives validity and data sufficiency separate fields, so an application can show that distinction.
- Latest reading
- 101mg/dL
- 14-day mean
- 132mg/dL
- Variability CV
- 26.7%
- Below 70
- 0.8%
- 70–180
- 85.6%
- Above 180
- 13.7%
Enough data for a 14-day report
All expected readings are present in this synthetic sample.
Inspect the computed report
Output from analyzeGlucose() using a fixed seed and a 14-day UTC window. No patient data. Non-finite numbers appear as null in JSON. The optional percentile profile is disabled for this example.
{
"meanGlucose": 131.5,
"gmi": 6.5,
"cv": 26.7,
"sd": 35.2,
"timeInRange": {
"veryLow": {
"percentage": 0.4,
"duration": 75,
"readingCount": 15,
"averageValue": 49
},
"low": {
"percentage": 0.4,
"duration": 75,
"readingCount": 15,
"averageValue": 61
},
"inRange": {
"percentage": 85.6,
"duration": 17260,
"readingCount": 3452,
"averageValue": 121
},
"high": {
"percentage": 13.7,
"duration": 2755,
"readingCount": 551,
"averageValue": 200
},
"veryHigh": {
"percentage": 0,
"duration": 0,
"readingCount": 0,
"averageValue": null
},
"meetsTargets": {
"tirMeetsGoal": true,
"tbrLevel1Safe": true,
"tbrLevel2Safe": true,
"tarLevel1Acceptable": true,
"tarLevel2Acceptable": true,
"targetBasis": "consensus-ranges",
"overallAssessment": "excellent",
"recommendations": [
"All metrics meet consensus targets."
]
},
"summary": {
"totalReadings": 4033,
"totalDuration": 20165,
"dataQuality": "excellent"
}
},
"tightRange": {
"inRange": 69.7,
"belowRange": 0.7,
"aboveRange": 29.6,
"meetsTarget": true,
"target": 50,
"readingCount": 4033
},
"risk": {
"lbgi": 0.31024877045506144,
"hbgi": 2.460865210078005,
"adrr": 19.8,
"grade": {
"grade": 4.44,
"gradeEuglycemia": 26.94,
"gradeHyperglycemia": 71.1,
"gradeHypoglycemia": 1.96
},
"gri": {
"score": 13.1,
"zone": "A",
"hypoComponent": 2.2,
"hyperComponent": 11
},
"jIndex": 27.77,
"modd": 11.4,
"conga": 32.9
},
"dataSufficiency": {
"totalReadings": 4033,
"daysOfData": 14,
"activePercent": 100,
"meetsCGMStandard": true
},
"agpProfile": null,
"episodes": {
"hypoEvents": [
{
"type": "hypo",
"level": 2,
"startTime": "2026-05-04T02:35:00.000Z",
"endTime": "2026-05-04T03:20:00.000Z",
"durationMinutes": 45,
"extremeValue": 46,
"meanValue": 54,
"readingCount": 10
},
{
"type": "hypo",
"level": 2,
"startTime": "2026-05-09T02:35:00.000Z",
"endTime": "2026-05-09T03:20:00.000Z",
"durationMinutes": 45,
"extremeValue": 50,
"meanValue": 58,
"readingCount": 10
},
{
"type": "hypo",
"level": 2,
"startTime": "2026-05-14T02:35:00.000Z",
"endTime": "2026-05-14T03:20:00.000Z",
"durationMinutes": 45,
"extremeValue": 46,
"meanValue": 55,
"readingCount": 10
}
],
"hyperEvents": [
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-01T07:30:00.000Z",
"endTime": "2026-05-01T09:15:00.000Z",
"durationMinutes": 105,
"extremeValue": 225,
"meanValue": 211,
"readingCount": 22
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-01T13:35:00.000Z",
"endTime": "2026-05-01T14:50:00.000Z",
"durationMinutes": 75,
"extremeValue": 201,
"meanValue": 194,
"readingCount": 16
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-02T07:30:00.000Z",
"endTime": "2026-05-02T08:50:00.000Z",
"durationMinutes": 80,
"extremeValue": 201,
"meanValue": 193,
"readingCount": 17
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-02T13:25:00.000Z",
"endTime": "2026-05-02T15:15:00.000Z",
"durationMinutes": 110,
"extremeValue": 232,
"meanValue": 214,
"readingCount": 23
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-03T08:30:00.000Z",
"endTime": "2026-05-03T09:20:00.000Z",
"durationMinutes": 50,
"extremeValue": 185,
"meanValue": 183,
"readingCount": 11
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-03T14:05:00.000Z",
"endTime": "2026-05-03T15:40:00.000Z",
"durationMinutes": 95,
"extremeValue": 210,
"meanValue": 202,
"readingCount": 20
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-03T19:55:00.000Z",
"endTime": "2026-05-03T20:40:00.000Z",
"durationMinutes": 45,
"extremeValue": 196,
"meanValue": 192,
"readingCount": 10
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-04T13:45:00.000Z",
"endTime": "2026-05-04T15:00:00.000Z",
"durationMinutes": 75,
"extremeValue": 199,
"meanValue": 194,
"readingCount": 16
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-04T20:00:00.000Z",
"endTime": "2026-05-04T20:35:00.000Z",
"durationMinutes": 35,
"extremeValue": 186,
"meanValue": 184,
"readingCount": 8
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-05T07:25:00.000Z",
"endTime": "2026-05-05T08:50:00.000Z",
"durationMinutes": 85,
"extremeValue": 221,
"meanValue": 210,
"readingCount": 18
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-05T13:40:00.000Z",
"endTime": "2026-05-05T14:45:00.000Z",
"durationMinutes": 65,
"extremeValue": 198,
"meanValue": 193,
"readingCount": 14
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-05T20:30:00.000Z",
"endTime": "2026-05-05T21:10:00.000Z",
"durationMinutes": 40,
"extremeValue": 184,
"meanValue": 182,
"readingCount": 9
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-06T07:35:00.000Z",
"endTime": "2026-05-06T09:30:00.000Z",
"durationMinutes": 115,
"extremeValue": 211,
"meanValue": 203,
"readingCount": 24
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-06T14:05:00.000Z",
"endTime": "2026-05-06T15:55:00.000Z",
"durationMinutes": 110,
"extremeValue": 222,
"meanValue": 209,
"readingCount": 23
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-06T20:35:00.000Z",
"endTime": "2026-05-06T20:55:00.000Z",
"durationMinutes": 20,
"extremeValue": 184,
"meanValue": 183,
"readingCount": 5
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-07T07:40:00.000Z",
"endTime": "2026-05-07T09:30:00.000Z",
"durationMinutes": 110,
"extremeValue": 216,
"meanValue": 205,
"readingCount": 23
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-07T14:15:00.000Z",
"endTime": "2026-05-07T15:40:00.000Z",
"durationMinutes": 85,
"extremeValue": 203,
"meanValue": 195,
"readingCount": 18
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-07T20:05:00.000Z",
"endTime": "2026-05-07T20:50:00.000Z",
"durationMinutes": 45,
"extremeValue": 189,
"meanValue": 186,
"readingCount": 10
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-08T19:50:00.000Z",
"endTime": "2026-05-08T21:15:00.000Z",
"durationMinutes": 85,
"extremeValue": 207,
"meanValue": 200,
"readingCount": 18
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-09T08:05:00.000Z",
"endTime": "2026-05-09T09:35:00.000Z",
"durationMinutes": 90,
"extremeValue": 214,
"meanValue": 203,
"readingCount": 19
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-09T14:10:00.000Z",
"endTime": "2026-05-09T15:20:00.000Z",
"durationMinutes": 70,
"extremeValue": 205,
"meanValue": 197,
"readingCount": 15
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-10T07:50:00.000Z",
"endTime": "2026-05-10T09:55:00.000Z",
"durationMinutes": 125,
"extremeValue": 232,
"meanValue": 216,
"readingCount": 26
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-10T13:55:00.000Z",
"endTime": "2026-05-10T15:15:00.000Z",
"durationMinutes": 80,
"extremeValue": 204,
"meanValue": 196,
"readingCount": 17
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-10T19:55:00.000Z",
"endTime": "2026-05-10T20:55:00.000Z",
"durationMinutes": 60,
"extremeValue": 197,
"meanValue": 193,
"readingCount": 13
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-11T07:50:00.000Z",
"endTime": "2026-05-11T08:35:00.000Z",
"durationMinutes": 45,
"extremeValue": 187,
"meanValue": 185,
"readingCount": 10
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-11T13:40:00.000Z",
"endTime": "2026-05-11T14:10:00.000Z",
"durationMinutes": 30,
"extremeValue": 185,
"meanValue": 183,
"readingCount": 7
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-12T07:40:00.000Z",
"endTime": "2026-05-12T08:55:00.000Z",
"durationMinutes": 75,
"extremeValue": 197,
"meanValue": 192,
"readingCount": 16
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-12T13:55:00.000Z",
"endTime": "2026-05-12T15:15:00.000Z",
"durationMinutes": 80,
"extremeValue": 197,
"meanValue": 191,
"readingCount": 17
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-12T20:25:00.000Z",
"endTime": "2026-05-12T20:55:00.000Z",
"durationMinutes": 30,
"extremeValue": 183,
"meanValue": 182,
"readingCount": 7
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-13T07:50:00.000Z",
"endTime": "2026-05-13T09:05:00.000Z",
"durationMinutes": 75,
"extremeValue": 206,
"meanValue": 198,
"readingCount": 16
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-13T13:45:00.000Z",
"endTime": "2026-05-13T15:30:00.000Z",
"durationMinutes": 105,
"extremeValue": 220,
"meanValue": 207,
"readingCount": 22
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-14T07:40:00.000Z",
"endTime": "2026-05-14T09:40:00.000Z",
"durationMinutes": 120,
"extremeValue": 221,
"meanValue": 209,
"readingCount": 25
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-14T13:45:00.000Z",
"endTime": "2026-05-14T15:05:00.000Z",
"durationMinutes": 80,
"extremeValue": 202,
"meanValue": 196,
"readingCount": 17
},
{
"type": "hyper",
"level": 1,
"startTime": "2026-05-14T19:45:00.000Z",
"endTime": "2026-05-14T21:15:00.000Z",
"durationMinutes": 90,
"extremeValue": 205,
"meanValue": 198,
"readingCount": 19
}
],
"summary": {
"hypoCount": 3,
"hyperCount": 34,
"hypoLevel2Count": 3,
"hyperLevel2Count": 0,
"totalHypoMinutes": 135,
"totalHyperMinutes": 2585
}
},
"valid": true
}Report blocks are nullable, so a caller must acknowledge that a
percentage may not exist. Callers check for usable readings, then
sufficient coverage. Data adequacy gets its own field instead of being
folded into valid.
I am not a clinician. The library needs to make its limits visible, especially when the readings do not support a broader summary.
The host owns the presentation
Renderers return SVG strings for AGP charts, Time in Range bars, and trend tiles. The host decides how to embed, sanitize, or deliver that string, so the same call works in a React app, a server-rendered report, or a PDF pipeline.
Vendor formats normalize into one reading shape, and results can leave in formats other health systems already read:
- Connectors for Dexcom, Libre, and Nightscout, with declared capabilities per source
- FHIR and Open mHealth output for systems that speak those standards
- Mixed-unit-aware APIs normalize declared mg/dL and mmol/L before calculating
- CSV parsing with an explicit contract rather than best-effort guessing
A dedicated package ships fixed-seed synthetic CGM data and scenario fixtures, so tests, demos, and documentation run on realistic glucose traces without touching anyone's real readings.
The docs site is built with Fumadocs, and its code samples are type-checked and executed in CI. A snippet that stops compiling fails the build, so the examples cannot drift away from the API they describe.
Release checks and remaining API work
The core has no runtime or peer dependencies. Release checks enforce both constraints.
- Vitest thresholds at 100% for lines, functions, branches, and statements
- A gzip budget on the reachable core ESM, failing the build when it is exceeded
- Packed-package consumer tests, so what publishes is what gets tested
- Release gated on build, lint, type checking, coverage, size, consumer tests, and the docs build
GlucoseIQ is for informational, educational, and software development use. It is not medical advice and is not a medical device. This scope is stated in the README, the docs, and this case study because the library computes clinical-looking numbers.
Project-defined API results are labeled:
glucoseIQScoreis non-diagnostic, a project-defined score derived from GRI rather than a published measurebuildAGPProfilereturns AGP-style bands, a percentile series rather than a standardized complete AGP reportvalidis notsufficient, which is why data adequacy gets its own field instead of being folded into a booleanEmbedding policy stays with the host, because a library returning an SVG string cannot know where it is about to be placed
Packages publish under the 1.0.0-next tag. The analytics are
settled and covered; what remains before 1.0 is the public API surface. The
prerelease tag reflects that remaining work.
API example, package reference, and input contracts
Validity and sufficiency
function summarize(readings: GlucoseReading[]) {
const report = analyzeGlucose(readings, { timeZone: 'America/Detroit' })
// `valid` only means at least one usable reading survived filtering.
if (!report.valid || report.timeInRange === null) {
return { status: 'no-data' } as const
}
// Whether that data is adequate is a separate question,
// and it gets a separate field rather than folding into `valid`.
const { meetsCGMStandard, daysOfData, activePercent } =
report.dataSufficiency
if (!meetsCGMStandard) {
return { status: 'insufficient', daysOfData, activePercent } as const
}
return {
status: 'ok',
timeInRange: report.timeInRange.inRange.percentage,
gmi: report.gmi,
} as const
}Metric modules
Seventeen metric modules, each named for the published measure it implements rather than an invented abstraction:
- AGP percentile profiles, Time in Range, and time-above and time-below episodes
- Variability: MAGE, CONGA, MODD, ADRR, J-Index, M-value, GVI and PGS
- Risk and control indices: GRI, GRADE, IGC, BGI, AUC
- Meal response, curve shape, and active sensor percentage
Five packages
@glucoseiq/corefor analytics, connectors, interop, and SVG renderers@glucoseiq/reactfor hooks and headless components, React 18 and 19@glucoseiq/tokensfor the five-zone glucose palette, thresholds, and trend glyphs@glucoseiq/testingfor fixed-seed synthetic data and scenario fixtures@glucoseiq/clifor analyzing mapped CSV without writing any application code
Input contracts
The CSV parser has an explicit contract for unfamiliar exports:
- A custom delimiter must be exactly one UTF-16 code unit; double quote, NUL, carriage return, and line feed are rejected
- Blank or BOM-only input returns an empty result, and so does a valid header-only file
- Missing required headers throw; individually invalid rows are skipped
- Renderer dimensions must be finite positive numbers, and text values are escaped
Project notes
17 metrics
AGP, TIR, GRI, MAGE, CONGA, MODD and more, from the clinical literature
Zero deps
Headless core with no runtime dependencies, held to 100% coverage
1.0.0-next
Published to npm as a release candidate, approaching 1.0