From 7accc2c40cee3d12605bf8e39d3141e52d06a27f Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Thu, 5 Mar 2026 21:57:05 -0500 Subject: [PATCH 1/4] Fix gradient definition for colors on predicted BG in LIve Activity --- .../Live Activity/ChartView.swift | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/Loop Widget Extension/Live Activity/ChartView.swift b/Loop Widget Extension/Live Activity/ChartView.swift index d5a0aca0a6..f90900b00d 100644 --- a/Loop Widget Extension/Live Activity/ChartView.swift +++ b/Loop Widget Extension/Live Activity/ChartView.swift @@ -18,7 +18,11 @@ struct ChartView: View { private let preset: Preset? private let yAxisMarks: [Double] private let colorGradient: LinearGradient - + + private static let colorInRange = Color.green + private static let colorBelowRange = Color.red + private static let colorAboveRange = Color.orange + init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) { self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit) self.predicatedData = ChartValues.convert( @@ -29,7 +33,7 @@ struct ChartView: View { lowerLimit: lowerLimit, upperLimit: upperLimit ) - self.colorGradient = ChartView.getGradient(useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit, highestValue: yAxisMarks.max() ?? 1) + self.colorGradient = ChartView.getGradient(useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit, lowestValue: predicatedGlucose.min() ?? 1, highestValue: predicatedGlucose.max() ?? 1) self.preset = preset self.glucoseRanges = glucoseRanges self.yAxisMarks = yAxisMarks @@ -41,22 +45,40 @@ struct ChartView: View { self.preset = preset self.glucoseRanges = glucoseRanges self.yAxisMarks = yAxisMarks - self.colorGradient = ChartView.getGradient(useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit, highestValue: yAxisMarks.max() ?? 1) + self.colorGradient = LinearGradient(colors: [], startPoint: .bottom, endPoint: .top) } - private static func getGradient(useLimits: Bool, lowerLimit: Double, upperLimit: Double, highestValue: Double) -> LinearGradient { + private static func getGradient(useLimits: Bool, lowerLimit: Double, upperLimit: Double, lowestValue: Double, highestValue: Double) -> LinearGradient { + var stops: [Gradient.Stop] = [Gradient.Stop(color: Color("glucose"), location: 0)] if useLimits { - let lowerStop = lowerLimit / highestValue - let upperStop = upperLimit / highestValue - stops = [ - Gradient.Stop(color: .red, location: 0), - Gradient.Stop(color: .red, location: lowerStop - 0.01), - Gradient.Stop(color: .green, location: lowerStop), - Gradient.Stop(color: .green, location: upperStop), - Gradient.Stop(color: .orange, location: upperStop + 0.01), - Gradient.Stop(color: .orange, location: 600), // Just use the mg/dl limit for the most upper value - ] + // For applying a color gradient to line data, the range of the plotted + // data maps to the space 0 to 1 for setting gradient stops, so normalize: + // Normalize the transition points to 0-1 space of the plotted range: + let lowerStop = (lowerLimit - lowestValue) / (highestValue - lowestValue) + let upperStop = (upperLimit - lowestValue) / (highestValue - lowestValue) + // Build up a set of stops, only using those in the 0-1 range: + stops = [] + var stopColor: Color + // Get the color for glucose at the minimum of the line: + if lowestValue < lowerLimit { + stopColor = colorBelowRange + } else if lowestValue < upperLimit { + stopColor = colorInRange + } else { + stopColor = colorAboveRange + } + stops.append(Gradient.Stop(color: stopColor, location: 0)) + // Add the transition stops if they are in the visible range: + if lowerStop > 0, lowerStop < 1 { + stops.append(Gradient.Stop(color: colorBelowRange, location: lowerStop)) + stops.append(Gradient.Stop(color: colorInRange, location: lowerStop + 0.01)) + } + if upperStop > 0, upperStop < 1 { + stops.append(Gradient.Stop(color: colorInRange, location: upperStop)) + stops.append(Gradient.Stop(color: colorAboveRange, location: upperStop + 0.01)) + } + } return LinearGradient( gradient: Gradient(stops: stops), @@ -107,9 +129,9 @@ struct ChartView: View { } } .chartForegroundStyleScale([ - "Good": .green, - "High": .orange, - "Low": .red, + "Good": colorInRange, + "High": colorAboveRange, + "Low": colorBelowRange, "Default": Color("glucose") ]) .chartPlotStyle { plotContent in From e23fe41fad0e87b518c9437f755d31f99aa932c5 Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Thu, 5 Mar 2026 22:05:44 -0500 Subject: [PATCH 2/4] Adjust zero-height overrides and target ranges to be visible on plot --- .../Live Activity/ChartView.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Loop Widget Extension/Live Activity/ChartView.swift b/Loop Widget Extension/Live Activity/ChartView.swift index f90900b00d..fcdac4ab8b 100644 --- a/Loop Widget Extension/Live Activity/ChartView.swift +++ b/Loop Widget Extension/Live Activity/ChartView.swift @@ -23,6 +23,16 @@ struct ChartView: View { private static let colorBelowRange = Color.red private static let colorAboveRange = Color.orange + // Infer chartable increment from yAxisMarks: mmol/L values are always below 40, mg/dL above 54. + private var chartableIncrement: Double { (yAxisMarks.max() ?? 100) < 40 ? 1.0/25.0 : 1.0 } + + // When min == max the rectangle has zero height and is invisible. Mirror the main app's + // doubleRangeWithMinimumIncrement logic by expanding by one chartable increment each side. + private func adjustedRange(min minValue: Double, max maxValue: Double) -> (min: Double, max: Double) { + guard (maxValue - minValue) < .ulpOfOne else { return (minValue, maxValue) } + return (minValue - chartableIncrement, maxValue + chartableIncrement) + } + init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) { self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit) self.predicatedData = ChartValues.convert( @@ -91,22 +101,24 @@ struct ChartView: View { ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){ Chart { if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) { + let (presetMin, presetMax) = adjustedRange(min: preset.minValue, max: preset.maxValue) RectangleMark( xStart: .value("Start", preset.startDate), xEnd: .value("End", preset.endDate), - yStart: .value("Preset override", preset.minValue), - yEnd: .value("Preset override", preset.maxValue) + yStart: .value("Preset override", presetMin), + yEnd: .value("Preset override", presetMax) ) .foregroundStyle(.primary) .opacity(0.6) } ForEach(glucoseRanges) { item in + let (rangeMin, rangeMax) = adjustedRange(min: item.minValue, max: item.maxValue) RectangleMark( xStart: .value("Start", item.startDate), xEnd: .value("End", item.endDate), - yStart: .value("Glucose range", item.minValue), - yEnd: .value("Glucose range", item.maxValue) + yStart: .value("Glucose range", rangeMin), + yEnd: .value("Glucose range", rangeMax) ) .foregroundStyle(.primary) .opacity(0.3) From 79075a39c6eae60c96bdb4e8784ef49bdd0e3f22 Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Thu, 5 Mar 2026 22:11:49 -0500 Subject: [PATCH 3/4] Fix color definitions --- Loop Widget Extension/Live Activity/ChartView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Loop Widget Extension/Live Activity/ChartView.swift b/Loop Widget Extension/Live Activity/ChartView.swift index fcdac4ab8b..2433bad2f3 100644 --- a/Loop Widget Extension/Live Activity/ChartView.swift +++ b/Loop Widget Extension/Live Activity/ChartView.swift @@ -141,9 +141,9 @@ struct ChartView: View { } } .chartForegroundStyleScale([ - "Good": colorInRange, - "High": colorAboveRange, - "Low": colorBelowRange, + "Good": Self.colorInRange, + "High": Self.colorAboveRange, + "Low": Self.colorBelowRange, "Default": Color("glucose") ]) .chartPlotStyle { plotContent in From c3f4b343e7f7af0bda1fc18051f76271035cb563 Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Thu, 5 Mar 2026 22:16:59 -0500 Subject: [PATCH 4/4] Make override bars a little wider --- Loop Widget Extension/Live Activity/ChartView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop Widget Extension/Live Activity/ChartView.swift b/Loop Widget Extension/Live Activity/ChartView.swift index 2433bad2f3..b3b5376a8a 100644 --- a/Loop Widget Extension/Live Activity/ChartView.swift +++ b/Loop Widget Extension/Live Activity/ChartView.swift @@ -30,7 +30,7 @@ struct ChartView: View { // doubleRangeWithMinimumIncrement logic by expanding by one chartable increment each side. private func adjustedRange(min minValue: Double, max maxValue: Double) -> (min: Double, max: Double) { guard (maxValue - minValue) < .ulpOfOne else { return (minValue, maxValue) } - return (minValue - chartableIncrement, maxValue + chartableIncrement) + return (minValue - 3 * chartableIncrement, maxValue + 3 * chartableIncrement) } init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) {