Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

📊 JupyterHealth Demo: CGM Data Analysis (Researcher View)

Welcome to the Continuous Glucose Monitoring (CGM) demo on the JupyterHealth platform.
This notebook walks you through:

  • 📥 Loading CGM data from JupyterHealth Exchange

  • 🧮 Calculating core AGP (Ambulatory Glucose Profile) metrics

  • 📊 Visualizing glucose trends for researchers or clinicians


🔗 Step 1: Connect to JupyterHealth Exchange and Explore

We use the jupyterhealth-client library to securely pull CGM data from a trusted JupyterHealth Data Exchange.

- 🏥 First, list available organizations

-🧍🏾‍♀️📋🧍🏻‍♂️Second, list available studies and patients

👩🏻‍🦰 Step 2: Select study and patient

Select the study and patient you are interested in. This is the same thing the widgets do in the dashboard.

🧠 Step 3: Calculate Glucose Metrics

These help us evaluate patient stability and risk zones.

We now compute key statistics to understand the glucose trends:

  • Mean Glucose =∑Glucose ReadingsTotal Number of Readings = \frac{\sum \text{Glucose Readings}}{\text{Total Number of Readings}}

  • Glucose Variability (CV%) =(Standard Deviation of GlucoseMean Glucose)×100 = \left( \frac{\text{Standard Deviation of Glucose}}{\text{Mean Glucose}} \right) \times 100

  • Glucose Management Indicator (GMI) =3.31+0.02392×Mean Glucose= 3.31 + 0.02392 \times \text{Mean Glucose}

  • Time in Range (%) = (Number of readings in range of 70-180mg/dLTotal number of readings)×100\left( \frac{\text{Number of readings in range of 70-180mg/dL}}{\text{Total number of readings}} \right) \times 100

Breakdown:

  • VLow = Level 2 Hypoglycemia (<54 mg/dL)

  • Low = Level 1 Hypoglycemia (54–69 mg/dL)

  • Target Range (70–180 mg/dL)

  • High = Level 1 Hyperglycemia (181–250 mg/dL)

  • VHigh = Level 2 Hyperglycemia (>250 mg/dL)

🧠 Glycemia Risk Index (GRI) GRI is a composite metric developed to summarize the overall glycemic risk based on CGM data. It combines both hypoglycemia and hyperglycemia exposure into a single score from 0 to 100, where:

  • 0 = Ideal glycemic control (100% Time In Range)

  • 100 = Maximum glycemic risk

The GRI is computed from the percentage of time spent in four glucose ranges:

📐 GRI Calculation

GRI=3.0×VLow+2.4×Low+1.6×VHigh+0.8×High\text{GRI} = 3.0 \times \text{VLow} + 2.4 \times \text{Low} + 1.6 \times \text{VHigh} + 0.8 \times \text{High}

GRI is also visualized using a 2D GRI Grid, where:

  • X-axis = Hypoglycemia Component = VLow + (0.8 × Low)

  • Y-axis = Hyperglycemia Component = VHigh + (0.5 × High)

This allows clinicians to see whether risk is driven more by lows or highs, even if two patients have the same overall GRI.

The first block of code:

1.	✅ Confirms units are in mg/dL.
2.	🔍 Extracts just CGM glucose values and timestamps.
3.	🕐 Sorts the glucose values by time for downstream analysis.

📈 Step 4: Visualize Glucose Patterns

This section generates:

  • GRI Grid

  • Daily Glucose Profiles

  • Ambulatory Glucose Profile (AGP)

  • Goals for Type 1 and Type Diabetes

These are vital for interpreting glycemic control and variability.

🟩 Median Glucose Trend Line

The green line represents a smoothed median glucose trend for each day. It is calculated using a rolling window of 5 readings, which helps reduce short-term fluctuations and highlight overall trends in glucose levels.

  • ✅ Helps visualize daytime and nighttime patterns

  • ✅ Reduces noise from rapid sensor changes

  • ⚠️ May slightly lag or flatten sharp spikes or drops

This smoothing approach mimics how continuous glucose monitoring (CGM) systems often display data to enhance clinical interpretation.

Ambulatory Glucose Profile (AGP) visualizes glucose trends as if they occurred in a single day. Colored Bands (Percentile Ranges):

  • Dark Green Line: Median glucose (50th percentile).

  • Green Shaded Area: 25th–75th percentile range.

  • Orange Shaded Area: 5th–95th percentile range.

  • Target Range (70–180 mg/dL):

  • Marked in green with horizontal lines at 70 mg/dL and 180 mg/dL.

MAGE (Mean Amplitude of Glycemic Excursions)

MAGE measures large glucose fluctuations:

MODD (Mean of Daily Differences)

MODD measures day-to-day glucose fluctuations at the same time of day:

🖥️ Step 5: Interactive & Multimodal Visualization with CGM Sandbox

Steps 3 and 4 produced static summaries, which are the right tool for quantifying control and the AGP. However, CGM data benefits from a second look at a different scale. A single day shows the shape of one meal response; the full record shows what repeats across weeks.

Glucose is also hard to read on its own. A rise at 2 PM and an identical rise at 2 AM mean very different things, and telling them apart means reading the trace next to lifestyle data: when the participant slept, ate, or moved.

CGM Sandbox is the custom tool we built for this. It gives you an interactive view of the glucose trace that other data can be layered onto, through two extension points:

  • an overlay draws on the same axes as the glucose trace, as shaded ranges, reference lines or markers

  • an extension gets its own panel below the trace, sharing the time axis, for data with a different unit or shape such as a night-by-night summary

An interactive view of the CGM data

CGMViewer draws the glucose frame as an interactive figure, with a day picker and prev/next controls above it. Each argument in the call below carries a comment explaining what it does.

Once the trace is up, an overlay can be layered onto it:

  • TimeInRangeOverlay colours the trace by whether glucose is in, below, or above range

  • MeanGlucoseOverlay draws the mean of the visible window as a horizontal line, and hovering it reads the value back

Both overlays are constructed at the end of the cell but not yet applied, so the cell first shows the raw signal. Uncomment the loop to apply them.

A second modality: sleep

Here is the link that makes the view multimodal. Sleep for this same participant is in the JupyterHealth Exchange too, reachable with the same client, the same patient_id, and the same study_id this notebook has used throughout. There is nothing new to connect.

Layering sleep onto the trace

With the raw trace in view, sleep is what makes the overnight readings legible. SleepWindowOverlay shades each sleep window behind the trace, and SleepCompositionExtension adds a panel showing how each night was composed.

Read together, they answer a question glucose cannot on its own: did that rise happen while the participant was awake or asleep?

Making MAGE visible

In Step 3 MAGE came out as a single number, the mean of the jumps between consecutive readings that exceed one standard deviation. That number is easy to report and hard to interpret, because nothing in it shows which jumps it counted.

MageJumpOverlay takes the same threshold and draws a segment across every jump that qualified, so the values behind the mean are visible on the trace. It measures the frame the viewer is drawing, which has the first and last partial days trimmed, so the marked set is a little smaller than the one behind the number from Step 3.

WakeupGlucoseOverlay marks the first reading after each wake time, which is the glucose the participant woke up with. It takes the nights frame from the cell above, so the night windows and the wake-up readings come from one source.

Exercise: overlay physical activity

Every layer so far came from the package. This one is yours.

This study also records physical activity as omh:step-count:3.0, one record per day covering the whole local day. Load it with the client the way we loaded sleep, then write an overlay that draws it against the glucose trace.

A daily step total is a per-day number, so it has no natural home on a mg/dL axis. That is part of the exercise: pick a mapping that reads clearly behind the trace without competing with it.

An overlay is just an object with two methods:

  • __init__(...) takes whatever it needs and stores it

  • draw() runs once per render, with self.viewer already set by add_overlay()

Two things about the viewer are worth knowing before you start:

  • the axes are not always a single panel. viewer.iter_axes_by_time() yields (ax, start, end) for each visible time axis, which is what lets an overlay work in both the one-day and full views

  • viewer.scale(daily_value, full_value) returns the value appropriate to the current mode, which is how the built-in overlays keep line widths and fonts readable at both scales

One hint: omh:step-count:3.0 is an Open mHealth code, so the client’s default code system applies and no explicit system|code pair is needed. There is no Code enum member for it, so pass the string directly.

🛑 Stop here. The next cell is a scaffold, not a solution. Load the step count and fill in the two methods yourself, then compare with the cell after it.

🩺 Step 6: Interpretation & Takeaways

Use the plots above to assess:

  • 🔼 High variability (wide percentile bands)

  • 🔽 Stability (tight percentile bands)

  • 🔍 Time spent in hypo-/hyperglycemic zones

Discuss intervention strategies based on visual and numeric patterns.