../work

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
My role
Creator & Maintainer
Timeline
2025 - Present
Built with
TypeScriptNode.js 24React 19TurborepoSVGFHIRChangesets
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.

GlucoseIQ / sample report
Synthetic data
Latest reading
101mg/dL
14-day mean
132mg/dL
Variability CV
26.7%
Last 24 hoursMay 14–15 · UTC
Synthetic glucose traceThe final 24 hours of a 14-day synthetic sample, in mg/dL. 289 readings are shown. The line contains no missing intervals. The latest reading is 101 mg/dL.2501807000:0006:0012:0018:0000:00
00:00 UTC101 mg/dL
The final 24 hours of a 14-day synthetic sample, in mg/dL. 289 readings are shown. The line contains no missing intervals. The latest reading is 101 mg/dL.
Reading distributionmg/dL
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.

100%coverage
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:

  • glucoseIQScore is non-diagnostic, a project-defined score derived from GRI rather than a published measure

  • buildAGPProfile returns AGP-style bands, a percentile series rather than a standardized complete AGP report

  • valid is not sufficient, which is why data adequacy gets its own field instead of being folded into a boolean

  • Embedding 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

typescript
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/core for analytics, connectors, interop, and SVG renderers
  • @glucoseiq/react for hooks and headless components, React 18 and 19
  • @glucoseiq/tokens for the five-zone glucose palette, thresholds, and trend glyphs
  • @glucoseiq/testing for fixed-seed synthetic data and scenario fixtures
  • @glucoseiq/cli for 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