Skip to content

Fix range and mmol/L colors in chart and Live Activity - #777

Open
MtlPhil wants to merge 7 commits into
loopandlearn:devfrom
achkars-org:fix/la-custom-range-updates
Open

MtlPhil wants to merge 7 commits into
loopandlearn:devfrom
achkars-org:fix/la-custom-range-updates

Conversation

@MtlPhil

@MtlPhil MtlPhil commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #762. The first commit (Use the selected range mode for Live Activity colors) is #762 rebased onto dev. The commits to review are the ones after it. I will rebase once #762 merges.

What this fixes

The app decided "low / in range / high" in several places, each with its own comparison. With custom range lines and mmol/L they could disagree with each other and with the number on screen: two readings that both display "3.9" could get different colors, and the Live Activity and stats pie could be stale after a range change.

Changes

  • One shared rule. GlucoseBand.of(_:low:high:mmol:) in Shared/ classifies a reading at the precision shown to the user. A reading on a line is in range. The chart, BG text, contact image, stats pie and Live Activity widget all use it (through UnitSettingsStore.glucoseBand(for:thresholds:) in the app), replacing the repeated compare-and-pick-a-color blocks.
  • Boundary rule: a reading on a line is in range. Whether a reading exactly on the low or high line is in range or out of range is discussed in Treatment of "on the line" readings (in range vs out of range) #779, which would benefit from community resolution. This PR implements the inclusive rule, which matches the international consensus on time in range (70–180 mg/dL, 3.9–10.0 mmol/L) and the TIR screen. It differs from the rule the colors use today, where a reading on a line counts as low or high. In mmol/L, readings that display 3.9 or 10.0 are in range.
  • Compared at the displayed value. Readings and lines are rounded to one decimal before comparing (GlucoseConversion.toDisplayedMmol), so readings that display the same value always get the same band. mg/dL is compared exactly as before.
  • Custom line changes refresh the Live Activity through refreshFromCurrentState, the same path as a range-mode change, so the dismissed/enabled checks apply and a burst of picker changes collapses into one update (it lands after the existing 20 s refresh delay). Thresholds are published through writeThresholds() so the effective range is always used.
  • Stats pie refreshes when the range lines or range mode change, and uses the same rule as the colors.
  • TIR screen and GRI keep their inclusive bands and published cutoffs, and now compare at display precision too, so in mmol/L 180 and 181 mg/dL (both shown 10.0) land in the same band.
  • Contact image uses effectiveThresholds(), so TIR and TITR modes match the chart, BG text and Live Activity.

Adjacent changes (separate commit)

I have read the single-concern guideline in the README. I judged the two changes below adjacent to this fix rather than unrelated, because they apply the same display-precision rule to the places that would otherwise still disagree with the new colors. They are in their own commit (Compare alarm thresholds at display precision and unify picker factor) so they can be split into a separate PR if you prefer.

  • Alarms compare at display precision. HighBGCondition, LowBGCondition, the activation limits in AlarmCondition.passesBGLimits, and MissedBolusCondition round the reading and the threshold the same way. Before, a custom 7.8 mmol/L high alarm was stored as 140.4 mg/dL, so a reading of 140 (displayed 7.8) did not trigger it even though the alarm is set at 7.8. The alarm boundary itself is unchanged (a High alarm still fires at its threshold, a Low alarm at or below it): an alarm threshold is a trigger, not a range. In mg/dL nothing changes; in mmol/L an alarm can fire one reading earlier or later at the boundary.
  • BGPicker conversion factor. It converted mmol/L to mg/dL with a hardcoded 18; it now uses GlucoseConversion.mmolToMgDl (18.01559), the same factor as the display. Values already saved with ×18 need no migration, because both sides are rounded before comparing.

bjorkert and others added 2 commits October 12, 2026 10:59
The Live Activity now colors the reading against the same low and high
values as the chart and BG header, so TIR and TITR use 70-180 and
70-140 and Custom uses the user's lines. A reading exactly on a line
counts as low or high, like in the header. Changing the range mode
refreshes the Live Activity.
- Push an immediate Live Activity update when the custom low/high lines
  change. TIR/TITR use fixed ranges, so this only applies in custom mode.
  Thresholds are published through writeThresholds() so the effective
  range is always used.
- Refresh the main stats pie when the range lines or range mode change.
- Compare reading colors at mmol/L display precision, so two readings that
  both display as "3.9" get the same color. Applies to the chart, BG text,
  contact image and the Live Activity widget. mg/dL is unchanged.
- Share the rounding via GlucoseConversion.toDisplayedMmol.
@MtlPhil
MtlPhil marked this pull request as ready for review October 12, 2026 16:37
@MtlPhil
MtlPhil marked this pull request as draft October 12, 2026 17:46
…ndary

- Route custom low/high line changes through refreshFromCurrentState
  instead of a separate hand-built Live Activity push, so the dismissed,
  enabled and in-flight checks apply and picker bursts are debounced.
- Use effectiveThresholds() for the contact image so TIR and TITR modes
  match the chart, BG text and Live Activity.
- Add GlucoseBand.of(_:low:high:mmol:) in Shared and use it for the chart,
  BG text, contact image, stats pie and the Live Activity widget. A
  reading on a line is low or high, compared at display precision.
- Make the stats pie follow the same boundary rule.
- Rename normalizeForColorComparison to normalizeToDisplayPrecision and
  shorten its comment.
- High, Low, activation limits and missed bolus alarms now round the
  reading and the threshold to the display precision of the current unit,
  so an alarm agrees with the number shown in mmol/L.
- BGPicker converts mmol/L to mg/dL with GlucoseConversion.mmolToMgDl
  instead of a hardcoded 18. Existing saved values need no migration
  because both sides are rounded before comparing.
- Add tests for boundary readings with both conversion factors.
Replace the function-reference/closure ternary with plain per-value
conversions so the compiler can type-check it.
A reading exactly on the low or high line is now in range, matching the
international consensus on time in range (70-180 mg/dL, 3.9-10.0 mmol/L).
GlucoseBand, and so the chart, BG text, contact image, stats pie and
Live Activity widget, use the inclusive rule at display precision.

The TIR screen and GRI keep their inclusive bands and published cutoffs
but now compare at display precision, so readings that display the same
mmol/L value share a band. Alarm thresholds are unchanged.
@MtlPhil
MtlPhil marked this pull request as ready for review October 12, 2026 18:49
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.

2 participants