Просмотр исходного кода

Merge pull request #1337 from MikePlante1/stats-colors

Set default Color Scheme to Dynamic and align Stats Charts and Static Scheme to Dynamic Scheme
Deniz Cengiz 4 дней назад
Родитель
Сommit
37f09a1171
19 измененных файлов с 267 добавлено и 166 удалено
  1. 9 4
      LiveActivity/LiveActivity+Helper.swift
  2. 20 4
      Trio/Sources/Helpers/DynamicGlucoseColor.swift
  3. 8 0
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  4. 38 0
      Trio/Sources/Models/BGTargets.swift
  5. 3 3
      Trio/Sources/Models/GlucoseColorScheme.swift
  6. 1 1
      Trio/Sources/Models/TrioSettings.swift
  7. 4 43
      Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  8. 2 40
      Trio/Sources/Modules/Home/HomeStateModel.swift
  9. 15 15
      Trio/Sources/Modules/Stat/View/ChartsView.swift
  10. 6 6
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyDistributionChart.swift
  11. 3 3
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift
  12. 14 14
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  13. 6 6
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  14. 14 14
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  15. 3 3
      Trio/Sources/Modules/Treatments/View/ForecastChart.swift
  16. 3 1
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsDataFlow.swift
  17. 7 1
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsProvider.swift
  18. 12 1
      Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  19. 99 7
      Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

+ 9 - 4
LiveActivity/LiveActivity+Helper.swift

@@ -70,6 +70,11 @@ extension NumberFormatter {
 }
 
 extension Color {
+    // Static Glucose Color Scheme band colors — needs to be kept in sync with DynamicGlucoseColor.swift
+    static let staticLow = Color(hue: 0.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let staticInRange = Color(hue: 120.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let staticHigh = Color(hue: 270.0 / 360.0, saturation: 0.6, brightness: 0.9)
+
     // Helper function to decide how to pick the glucose color
     static func getDynamicGlucoseColor(
         glucoseValue: Decimal,
@@ -87,14 +92,14 @@ extension Color {
                 targetGlucose: targetGlucose
             )
         }
-        // Otheriwse, use static (orange = high, red = low, green = range)
+        // Otherwise, use static colors
         else {
             if glucoseValue >= highGlucoseColorValue {
-                return Color.orange
+                return Color.staticHigh
             } else if glucoseValue <= lowGlucoseColorValue {
-                return Color.red
+                return Color.staticLow
             } else {
-                return Color.green
+                return Color.staticInRange
             }
         }
     }

+ 20 - 4
Trio/Sources/Helpers/DynamicGlucoseColor.swift

@@ -18,14 +18,14 @@ public func getDynamicGlucoseColor(
             targetGlucose: targetGlucose
         )
     }
-    // Otheriwse, use static (orange = high, red = low, green = range)
+    // Otherwise, use the static colors
     else {
         if glucoseValue >= highGlucoseColorValue {
-            return Color.orange
+            return Color.staticHigh
         } else if glucoseValue <= lowGlucoseColorValue {
-            return Color.red
+            return Color.staticLow
         } else {
-            return Color.green
+            return Color.staticInRange
         }
     }
 }
@@ -64,3 +64,19 @@ public func calculateHueBasedGlucoseColor(
     let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
     return color
 }
+
+// Discrete band colors sampled from the dynamic gradient above
+public extension Color {
+    static let dynamicRed = Color(hue: 0.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let dynamicOrange = Color(hue: 30.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let dynamicGreen = Color(hue: 120.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let dynamicTeal = Color(hue: 165.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let dynamicBlue = Color(hue: 200.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let dynamicIndigo = Color(hue: 235.0 / 360.0, saturation: 0.6, brightness: 0.9)
+    static let dynamicPurple = Color(hue: 270.0 / 360.0, saturation: 0.6, brightness: 0.9)
+
+    // Colors used when the Static Glucose Color Scheme is selected
+    static let staticLow = Color.dynamicRed
+    static let staticInRange = Color.dynamicGreen
+    static let staticHigh = Color.dynamicPurple
+}

+ 8 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -111043,6 +111043,9 @@
         }
       }
     },
+    "Dynamic (Default):" : {
+
+    },
     "Dynamic Insulin Sensitivity" : {
       "localizations" : {
         "bg" : {
@@ -112169,6 +112172,7 @@
       }
     },
     "Dynamic:" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -222383,6 +222387,9 @@
         }
       }
     },
+    "Purple = Above Range" : {
+
+    },
     "Quantity Carbs" : {
       "localizations" : {
         "bg" : {
@@ -314392,6 +314399,7 @@
       }
     },
     "Yellow = Above Range" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

+ 38 - 0
Trio/Sources/Models/BGTargets.swift

@@ -24,3 +24,41 @@ struct BGTargetEntry: JSON {
     let start: String
     let offset: Int
 }
+
+extension BGTargets {
+    func currentTarget(at date: Date = Date()) -> Decimal? {
+        let calendar = Calendar.current
+
+        for (index, entry) in targets.enumerated() {
+            guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
+                debug(.default, "Invalid BG target entry start time: \(entry.start)")
+                continue
+            }
+            let components = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+            guard let start = calendar.date(
+                bySettingHour: components.hour ?? 0,
+                minute: components.minute ?? 0,
+                second: components.second ?? 0,
+                of: date
+            ) else { continue }
+
+            let end: Date
+            if index < targets.count - 1, let nextTime = TherapySettingsUtil.parseTime(targets[index + 1].start) {
+                let nextComponents = calendar.dateComponents([.hour, .minute, .second], from: nextTime)
+                end = calendar.date(
+                    bySettingHour: nextComponents.hour ?? 0,
+                    minute: nextComponents.minute ?? 0,
+                    second: nextComponents.second ?? 0,
+                    of: date
+                ) ?? start
+            } else {
+                end = calendar.date(byAdding: .day, value: 1, to: start) ?? start
+            }
+
+            if date >= start, date < end {
+                return entry.low
+            }
+        }
+        return nil
+    }
+}

+ 3 - 3
Trio/Sources/Models/GlucoseColorScheme.swift

@@ -4,15 +4,15 @@ import UIKit
 
 public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
     public var id: String { rawValue }
-    case staticColor
     case dynamicColor
+    case staticColor
 
     var displayName: String {
         switch self {
-        case .staticColor:
-            return String(localized: "Static")
         case .dynamicColor:
             return String(localized: "Dynamic")
+        case .staticColor:
+            return String(localized: "Static")
         }
     }
 }

+ 1 - 1
Trio/Sources/Models/TrioSettings.swift

@@ -41,7 +41,7 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var low: Decimal = 70
-    var glucoseColorScheme: GlucoseColorScheme = .staticColor
+    var glucoseColorScheme: GlucoseColorScheme = .dynamicColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var hideInsulinBadge: Bool = false

+ 4 - 43
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -100,50 +100,11 @@ extension Adjustments {
 
         /// Retrieves the current glucose target based on the time of day.
         func getCurrentGlucoseTarget() async {
-            let now = Date()
-            let calendar = Calendar.current
-            let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm:ss"
-            dateFormatter.timeZone = TimeZone.current
-
             let bgTargets = await provider.getBGTargets()
-            let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
-
-            for (index, entry) in entries.enumerated() {
-                guard let entryTime = dateFormatter.date(from: entry.start) else {
-                    print("Invalid entry start time: \(entry.start)")
-                    continue
-                }
-
-                let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
-                let entryStartTime = calendar.date(
-                    bySettingHour: entryComponents.hour!,
-                    minute: entryComponents.minute!,
-                    second: entryComponents.second!,
-                    of: now
-                )!
-
-                let entryEndTime: Date
-                if index < entries.count - 1,
-                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
-                {
-                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
-                    entryEndTime = calendar.date(
-                        bySettingHour: nextEntryComponents.hour!,
-                        minute: nextEntryComponents.minute!,
-                        second: nextEntryComponents.second!,
-                        of: now
-                    )!
-                } else {
-                    entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
-                }
-
-                if now >= entryStartTime, now < entryEndTime {
-                    await MainActor.run {
-                        currentGlucoseTarget = entry.value
-                        target = currentGlucoseTarget
-                    }
-                    return
+            if let currentTarget = bgTargets.currentTarget() {
+                await MainActor.run {
+                    currentGlucoseTarget = currentTarget
+                    target = currentGlucoseTarget
                 }
             }
         }

+ 2 - 40
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -930,46 +930,8 @@ extension Home {
         }
 
         private func getCurrentGlucoseTarget() async {
-            let now = Date()
-            let calendar = Calendar.current
-
-            let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
-
-            for (index, entry) in entries.enumerated() {
-                guard let entryTime = TherapySettingsUtil.parseTime(entry.start) else {
-                    debug(.default, "Invalid entry start time: \(entry.start)")
-                    continue
-                }
-
-                let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
-                let entryStartTime = calendar.date(
-                    bySettingHour: entryComponents.hour!,
-                    minute: entryComponents.minute!,
-                    second: entryComponents.second!,
-                    of: now
-                )!
-
-                let entryEndTime: Date
-                if index < entries.count - 1,
-                   let nextEntryTime = TherapySettingsUtil.parseTime(entries[index + 1].start)
-                {
-                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
-                    entryEndTime = calendar.date(
-                        bySettingHour: nextEntryComponents.hour!,
-                        minute: nextEntryComponents.minute!,
-                        second: nextEntryComponents.second!,
-                        of: now
-                    )!
-                } else {
-                    entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
-                }
-
-                if now >= entryStartTime, now < entryEndTime {
-                    await MainActor.run {
-                        currentGlucoseTarget = entry.value
-                    }
-                    return
-                }
+            if let target = bgTargets.currentTarget() {
+                await MainActor.run { currentGlucoseTarget = target }
             }
         }
 

+ 15 - 15
Trio/Sources/Modules/Stat/View/ChartsView.swift

@@ -64,17 +64,17 @@ struct ChartsView: View {
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.orange.gradient).symbolSize(size).interpolationMethod(.cardinal)
+                    ).foregroundStyle(Color.staticHigh.gradient).symbolSize(size).interpolationMethod(.cardinal)
                 } else if item.glucose < Int(lowLimit) {
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.red.gradient).symbolSize(size).interpolationMethod(.cardinal)
+                    ).foregroundStyle(Color.staticLow.gradient).symbolSize(size).interpolationMethod(.cardinal)
                 } else {
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.green.gradient).symbolSize(size).interpolationMethod(.cardinal)
+                    ).foregroundStyle(Color.staticInRange.gradient).symbolSize(size).interpolationMethod(.cardinal)
                 }
             }
         }
@@ -133,13 +133,13 @@ struct ChartsView: View {
                     localized:
                     "Low",
                     comment: ""
-                ) + " (<\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .red,
-                String(localized: "In Range", comment: ""): .green,
+                ) + " (<\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .staticLow,
+                String(localized: "In Range", comment: ""): .staticInRange,
                 String(
                     localized:
                     "High",
                     comment: ""
-                ) + " (>\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .orange
+                ) + " (>\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .staticHigh
             ]).frame(maxHeight: 25)
         }
         .frame(maxWidth: .infinity, alignment: .center) // Align the entire VStack to center
@@ -194,13 +194,13 @@ struct ChartsView: View {
                 localized:
                 "Low",
                 comment: ""
-            ) + " (< \(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .red,
-            "\(low.formatted(.number.precision(.fractionLength(fraction)))) - \(high.formatted(.number.precision(.fractionLength(fraction))))": .green,
+            ) + " (< \(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .staticLow,
+            "\(low.formatted(.number.precision(.fractionLength(fraction)))) - \(high.formatted(.number.precision(.fractionLength(fraction))))": .staticInRange,
             String(
                 localized:
                 "High",
                 comment: ""
-            ) + " (> \(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .orange
+            ) + " (> \(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .staticHigh
         ])
     }
 
@@ -218,7 +218,7 @@ struct ChartsView: View {
                         .foregroundColor(.secondary)
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                         .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.orange)
+                        .foregroundColor(.staticHigh)
                 }.font(.caption)
                 HStack {
                     let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
@@ -227,7 +227,7 @@ struct ChartsView: View {
                         .foregroundColor(.secondary)
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                         .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.green)
+                        .foregroundColor(.staticInRange)
                 }.font(.caption)
                 HStack {
                     let value = 100.0 * Double(mapGlucoseAcuteLow.count) / Double(mapGlucose.count)
@@ -236,7 +236,7 @@ struct ChartsView: View {
                         .foregroundColor(.secondary)
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                         .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.red)
+                        .foregroundColor(.staticLow)
                 }.font(.caption)
             }
         }
@@ -254,14 +254,14 @@ struct ChartsView: View {
                     let value = 100.0 * Double(mapGlucoseLow.count) / Double(mapGlucose.count)
                     Text(units == .mmolL ? "< 3.3" : "< 59").font(.caption2).foregroundColor(.secondary)
                     Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
-                        .foregroundColor(value == 0 ? .green : .red)
+                        .foregroundColor(value == 0 ? .staticInRange : .staticLow)
                     Text("%").font(.caption)
                 }
                 Spacer()
                 HStack {
                     let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
                     Text(units == .mmolL ? "3.9-7.8" : "70-140").foregroundColor(.secondary)
-                    Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).foregroundColor(.green)
+                    Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).foregroundColor(.staticInRange)
                     Text("%").foregroundColor(.secondary)
                 }.font(.caption)
                 Spacer()
@@ -269,7 +269,7 @@ struct ChartsView: View {
                     let value = 100.0 * Double(mapGlucoseAcuteHigh.count) / Double(mapGlucose.count)
                     Text(units == .mmolL ? "> 11.0" : "> 198").font(.caption).foregroundColor(.secondary)
                     Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
-                        .foregroundColor(value == 0 ? .green : .orange)
+                        .foregroundColor(value == 0 ? .staticInRange : .staticHigh)
                     Text("%").font(.caption)
                 }
                 Spacer()

+ 6 - 6
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyDistributionChart.swift

@@ -146,12 +146,12 @@ struct GlucoseDailyDistributionChart: View {
             }
         }
         .chartForegroundStyleScale([
-            legend("veryLow"): .purple,
-            legend("low"): .red,
-            legend("inSmallRange"): .green,
-            legend("inRange"): .darkGreen,
-            legend("high"): .loopYellow,
-            legend("veryHigh"): .orange
+            legend("veryLow"): Color.dynamicRed,
+            legend("low"): Color.dynamicOrange,
+            legend("inSmallRange"): Color.dynamicGreen,
+            legend("inRange"): Color.dynamicTeal,
+            legend("high"): Color.dynamicBlue,
+            legend("veryHigh"): Color.dynamicPurple
         ])
         .chartXSelection(value: $selectedDate.animation(.easeInOut))
         .onChange(of: selectedDate) { _, newValue in

+ 3 - 3
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift

@@ -329,9 +329,9 @@ struct GlucoseDailyPercentileChart: View {
             "10-90%": .blue.opacity(0.3),
             "25-75%": .blue.opacity(0.5),
             "Median": .blue,
-            "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": .red,
-            "\(timeInRangeType.topThreshold.formatted(withUnits: units))": .mint,
-            "\(highLimit.formatted(withUnits: units))": .orange
+            "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": .staticLow,
+            "\(timeInRangeType.topThreshold.formatted(withUnits: units))": .staticInRange,
+            "\(highLimit.formatted(withUnits: units))": .staticHigh
         ])
         .chartScrollableAxes(.horizontal)
         .chartScrollPosition(x: $scrollPosition)

+ 14 - 14
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift

@@ -25,38 +25,38 @@ struct GlucoseDistributionChart: View {
                 }
             }
             .chartForegroundStyleScale([
-                "<54": .purple.opacity(0.8),
-                "54-\(timeInRangeType.bottomThreshold)": .red.opacity(0.8),
-                "\(timeInRangeType.bottomThreshold)-\(timeInRangeType.topThreshold)": .green.opacity(0.8),
-                "\(timeInRangeType.topThreshold)-180": .darkGreen.opacity(0.8),
-                "180-200": .yellow.opacity(0.8),
-                "200-220": .orange.opacity(0.8),
-                ">220": .darkOrange.opacity(0.8)
+                "<54": Color.dynamicRed.opacity(0.8),
+                "54-\(timeInRangeType.bottomThreshold)": Color.dynamicOrange.opacity(0.8),
+                "\(timeInRangeType.bottomThreshold)-\(timeInRangeType.topThreshold)": Color.dynamicGreen.opacity(0.8),
+                "\(timeInRangeType.topThreshold)-180": Color.dynamicTeal.opacity(0.8),
+                "180-200": Color.dynamicBlue.opacity(0.8),
+                "200-220": Color.dynamicIndigo.opacity(0.8),
+                ">220": Color.dynamicPurple.opacity(0.8)
             ])
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
-                    ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .purple.opacity(0.8)),
+                    ("<\(units == .mgdL ? Decimal(54) : 54.asMmolL)", .dynamicRed.opacity(0.8)),
                     (
                         "\(units == .mgdL ? Decimal(54) : 54.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)",
-                        .red.opacity(0.8)
+                        .dynamicOrange.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(timeInRangeType.bottomThreshold) : timeInRangeType.bottomThreshold.asMmolL)-\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)",
-                        .green.opacity(0.8)
+                        .dynamicGreen.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(timeInRangeType.topThreshold) : timeInRangeType.topThreshold.asMmolL)-\(units == .mgdL ? Decimal(180) : 180.asMmolL)",
-                        .darkGreen.opacity(0.8)
+                        .dynamicTeal.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(180) : 180.asMmolL)-\(units == .mgdL ? Decimal(200) : 200.asMmolL)",
-                        .yellow.opacity(0.8)
+                        .dynamicBlue.opacity(0.8)
                     ),
                     (
                         "\(units == .mgdL ? Decimal(200) : 200.asMmolL)-\(units == .mgdL ? Decimal(220) : 220.asMmolL)",
-                        .orange.opacity(0.8)
+                        .dynamicIndigo.opacity(0.8)
                     ),
-                    (">\(units == .mgdL ? Decimal(220) : 220.asMmolL)", .darkOrange.opacity(0.8))
+                    (">\(units == .mgdL ? Decimal(220) : 220.asMmolL)", .dynamicPurple.opacity(0.8))
                 ]
 
                 let columns = [GridItem(.adaptive(minimum: 65), spacing: 4)]

+ 6 - 6
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift

@@ -123,18 +123,18 @@ struct GlucosePercentileChart: View {
                 "10-90%": Color.blue.opacity(0.3),
                 "25-75%": Color.blue.opacity(0.5),
                 "Median": Color.blue,
-                "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": Color.red,
-                "\(timeInRangeType.topThreshold.formatted(withUnits: units))": Color.mint,
-                "\(highLimit.formatted(withUnits: units))": Color.orange
+                "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": Color.staticLow,
+                "\(timeInRangeType.topThreshold.formatted(withUnits: units))": Color.staticInRange,
+                "\(highLimit.formatted(withUnits: units))": Color.staticHigh
             ])
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
                     ("10-90%", Color.blue.opacity(0.3)),
                     ("25-75%", Color.blue.opacity(0.5)),
                     (String(localized: "Median"), Color.blue),
-                    (String(localized: "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"), Color.red),
-                    (String(localized: "\(timeInRangeType.topThreshold.formatted(withUnits: units))"), Color.mint),
-                    (String(localized: "\(highLimit.formatted(withUnits: units))"), Color.orange)
+                    (String(localized: "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"), Color.staticLow),
+                    (String(localized: "\(timeInRangeType.topThreshold.formatted(withUnits: units))"), Color.staticInRange),
+                    (String(localized: "\(highLimit.formatted(withUnits: units))"), Color.staticHigh)
                 ]
 
                 let columns = [GridItem(.adaptive(minimum: 100), spacing: 4)]

+ 14 - 14
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift

@@ -72,7 +72,7 @@ struct GlucoseSectorChart: View {
                         .font(.subheadline)
                         .foregroundStyle(Color.secondary)
                         Text(formatPercentage(inRangePercentage, tight: true))
-                            .foregroundStyle(Color.loopGreen)
+                            .foregroundStyle(Color.dynamicTeal)
                     }
 
                     VStack(alignment: .leading, spacing: 5) {
@@ -82,7 +82,7 @@ struct GlucoseSectorChart: View {
                         .font(.subheadline)
                         .foregroundStyle(Color.secondary)
                         Text(formatPercentage(tightPercentage, tight: true))
-                            .foregroundStyle(Color.green)
+                            .foregroundStyle(Color.dynamicGreen)
                     }
                 }.padding(.leading, 5)
 
@@ -91,7 +91,7 @@ struct GlucoseSectorChart: View {
                         Text("> \(highLimit.formatted(for: units))").font(.subheadline)
                             .foregroundStyle(Color.secondary)
                         Text(formatPercentage(highPercentage, tight: true))
-                            .foregroundStyle(Color.loopYellow)
+                            .foregroundStyle(showChart ? Color.dynamicPurple : Color.dynamicBlue)
                     }
 
                     VStack(alignment: .leading, spacing: 5) {
@@ -101,7 +101,7 @@ struct GlucoseSectorChart: View {
                         .font(.subheadline)
                         .foregroundStyle(Color.secondary)
                         Text(formatPercentage(lowPercentage, tight: true))
-                            .foregroundStyle(Color.red)
+                            .foregroundStyle(showChart ? Color.dynamicRed : Color.dynamicOrange)
                     }
                 }
                 // If not showing chart, show extra stats
@@ -111,7 +111,7 @@ struct GlucoseSectorChart: View {
                             Text("> \(Decimal(220).formatted(for: units))").font(.subheadline)
                                 .foregroundStyle(Color.secondary)
                             Text(formatPercentage(moderatelyHighPercentage, tight: true))
-                                .foregroundStyle(Color.loopYellow)
+                                .foregroundStyle(Color.dynamicBlue)
                         }
 
                         VStack(alignment: .leading, spacing: 5) {
@@ -121,7 +121,7 @@ struct GlucoseSectorChart: View {
                             .font(.subheadline)
                             .foregroundStyle(Color.secondary)
                             Text(formatPercentage(moderatelyLowPercentage, tight: true))
-                                .foregroundStyle(Color.red)
+                                .foregroundStyle(Color.dynamicOrange)
                         }
                     }
                     VStack(alignment: .leading, spacing: 10) {
@@ -129,7 +129,7 @@ struct GlucoseSectorChart: View {
                             Text("> \(Decimal(250).formatted(for: units))").font(.subheadline)
                                 .foregroundStyle(Color.secondary)
                             Text(formatPercentage(veryHighPercentage, tight: true))
-                                .foregroundStyle(Color.orange)
+                                .foregroundStyle(Color.dynamicPurple)
                         }
 
                         VStack(alignment: .leading, spacing: 5) {
@@ -139,7 +139,7 @@ struct GlucoseSectorChart: View {
                             .font(.subheadline)
                             .foregroundStyle(Color.secondary)
                             Text(formatPercentage(veryLowPercentage, tight: true))
-                                .foregroundStyle(Color.purple)
+                                .foregroundStyle(Color.dynamicRed)
                         }
                     }
                 }
@@ -230,9 +230,9 @@ struct GlucoseSectorChart: View {
 
         // Return array of tuples with range data
         return [
-            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .loopYellow),
-            (.inRange, inRangeCount, Decimal(inRangeCount) / Decimal(total) * 100, .green),
-            (.low, lowCount, Decimal(lowCount) / Decimal(total) * 100, .red)
+            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .dynamicPurple),
+            (.inRange, inRangeCount, Decimal(inRangeCount) / Decimal(total) * 100, .dynamicGreen),
+            (.low, lowCount, Decimal(lowCount) / Decimal(total) * 100, .dynamicRed)
         ]
     }
 
@@ -279,7 +279,7 @@ struct GlucoseSectorChart: View {
 
             return RangeDetail(
                 title: String(localized: "High Glucose"),
-                color: .loopYellow,
+                color: .dynamicPurple,
                 items: [
                     (
                         String(localized: "Very High (>\(Decimal(250).formatted(for: units)))"),
@@ -304,7 +304,7 @@ struct GlucoseSectorChart: View {
 
             return RangeDetail(
                 title: String(localized: "In Range"),
-                color: .green,
+                color: .dynamicGreen,
                 items: [
                     (
                         String(
@@ -334,7 +334,7 @@ struct GlucoseSectorChart: View {
 
             return RangeDetail(
                 title: String(localized: "Low Glucose"),
-                color: .red,
+                color: .dynamicRed,
                 items: [
                     (
                         String(

+ 3 - 3
Trio/Sources/Modules/Treatments/View/ForecastChart.swift

@@ -147,11 +147,11 @@ struct ForecastChart: View {
                 .zIndex(-1)
                 .symbolSize(CGSize(width: 15, height: 15))
                 .foregroundStyle(
-                    Decimal(selectedGlucose.glucose) > state.highGlucose ? Color.orange
+                    Decimal(selectedGlucose.glucose) > state.highGlucose ? Color.staticHigh
                         .opacity(0.8) :
                         (
-                            Decimal(selectedGlucose.glucose) < state.lowGlucose ? Color.red.opacity(0.8) : Color.green
-                                .opacity(0.8)
+                            Decimal(selectedGlucose.glucose) < state.lowGlucose ? Color.staticLow.opacity(0.8)
+                                : Color.staticInRange.opacity(0.8)
                         )
                 )
 

+ 3 - 1
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsDataFlow.swift

@@ -2,4 +2,6 @@ enum UserInterfaceSettings {
     enum Config {}
 }
 
-protocol UserInterfaceSettingsProvider: Provider {}
+protocol UserInterfaceSettingsProvider: Provider {
+    func getBGTargets() async -> BGTargets
+}

+ 7 - 1
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsProvider.swift

@@ -1,3 +1,9 @@
 extension UserInterfaceSettings {
-    final class Provider: BaseProvider, UserInterfaceSettingsProvider {}
+    final class Provider: BaseProvider, UserInterfaceSettingsProvider {
+        func getBGTargets() async -> BGTargets {
+            await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+                ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+                ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+        }
+    }
 }

+ 12 - 1
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -11,10 +11,11 @@ extension UserInterfaceSettings {
         @Published var forecastDisplayType: ForecastDisplayType = .cone
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
-        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
+        @Published var glucoseColorScheme: GlucoseColorScheme = .dynamicColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
         @Published var requireAdjustmentsConfirmation: Bool = false
+        @Published var currentGlucoseTarget: Decimal = 100
 
         var units: GlucoseUnits = .mgdL
 
@@ -48,6 +49,16 @@ extension UserInterfaceSettings {
 
             subscribeSetting(\.requireAdjustmentsConfirmation, on: $requireAdjustmentsConfirmation) {
                 requireAdjustmentsConfirmation = $0 }
+
+            Task { await getCurrentGlucoseTarget() }
+        }
+
+        /// Resolves the glucose target active right now from the BG target schedule.
+        func getCurrentGlucoseTarget() async {
+            let bgTargets = await provider.getBGTargets()
+            if let target = bgTargets.currentTarget() {
+                await MainActor.run { currentGlucoseTarget = target }
+            }
         }
     }
 }

+ 99 - 7
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -127,20 +127,27 @@ extension UserInterfaceSettings {
                                                     "Set the color scheme for glucose readings on the main glucose graph, live activities, and bolus calculator. Descriptions for each option found below."
                                                 )
                                                 VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Static:").bold()
-                                                    Text("Red = Below Range")
-                                                    Text("Green = In Range")
-                                                    Text("Yellow = Above Range")
-                                                }
-                                                VStack(alignment: .leading, spacing: 5) {
-                                                    Text("Dynamic:").bold()
+                                                    Text("Dynamic (Default):").bold()
                                                     Text("Green = At Target")
                                                     Text(
                                                         "Gradient Red = As readings approach and exceed below target, they gradually become more red."
                                                     )
+                                                    .fixedSize(horizontal: false, vertical: true)
                                                     Text(
                                                         "Gradient Purple = As readings approach and exceed above target, they become more purple."
                                                     )
+                                                    .fixedSize(horizontal: false, vertical: true)
+                                                    GlucoseColorGradientPreview(
+                                                        units: state.units,
+                                                        target: state.currentGlucoseTarget
+                                                    )
+                                                    .padding(.top, 6)
+                                                }
+                                                VStack(alignment: .leading, spacing: 5) {
+                                                    Text("Static:").bold()
+                                                    Text("Red = Below Range")
+                                                    Text("Green = In Range")
+                                                    Text("Purple = Above Range")
                                                 }
                                             }
                                         )
@@ -607,3 +614,88 @@ extension UserInterfaceSettings {
         }
     }
 }
+
+/// Illustrative bar that samples `getDynamicGlucoseColor` across the glucose range,
+/// so the hint shows exactly how the dynamic scheme colors readings from low to high.
+private struct GlucoseColorGradientPreview: View {
+    let units: GlucoseUnits
+    // User's current glucose target — the green peak of the dynamic sweep.
+    let target: Decimal
+
+    private let minGlucose: Decimal = 40
+    private let maxGlucose: Decimal = 250
+
+    // Fixed anchors the dynamic scheme uses on the glucose graph: solid red at/below,
+    // solid purple at/above, sweeping red → green → purple between them.
+    private let hardCodedLow: Decimal = 55
+    private let hardCodedHigh: Decimal = 220
+
+    private var markerValues: [Decimal] {
+        [hardCodedLow, target, hardCodedHigh]
+    }
+
+    private var gradientStops: [Gradient.Stop] {
+        let steps = 60
+        return (0 ... steps).map { step in
+            let location = Double(step) / Double(steps)
+            let value = minGlucose + (maxGlucose - minGlucose) * Decimal(location)
+            return Gradient.Stop(
+                color: getDynamicGlucoseColor(
+                    glucoseValue: value,
+                    highGlucoseColorValue: hardCodedHigh,
+                    lowGlucoseColorValue: hardCodedLow,
+                    targetGlucose: target,
+                    glucoseColorScheme: .dynamicColor
+                ),
+                location: location
+            )
+        }
+    }
+
+    private func fraction(of value: Decimal) -> CGFloat {
+        let raw = (value - minGlucose) / (maxGlucose - minGlucose)
+        return min(max(CGFloat(truncating: raw as NSNumber), 0), 1)
+    }
+
+    private func axisValue(_ value: Decimal) -> String {
+        let display = units == .mgdL ? value : value.asMmolL
+        return "\(display)"
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 3) {
+            RoundedRectangle(cornerRadius: 8)
+                .fill(LinearGradient(
+                    gradient: Gradient(stops: gradientStops),
+                    startPoint: .leading,
+                    endPoint: .trailing
+                ))
+                .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(Color.primary.opacity(0.12), lineWidth: 1))
+                .frame(height: 26)
+                .shadow(color: .black.opacity(0.18), radius: 3, y: 1)
+
+            GeometryReader { geo in
+                ZStack(alignment: .topLeading) {
+                    ForEach(markerValues, id: \.self) { value in
+                        marker(value, at: fraction(of: value), width: geo.size.width)
+                    }
+                }
+            }
+            .frame(height: 22)
+        }
+        .accessibilityHidden(true)
+    }
+
+    private func marker(_ value: Decimal, at fraction: CGFloat, width: CGFloat) -> some View {
+        VStack(spacing: 1) {
+            Image(systemName: "arrowtriangle.up.fill")
+                .font(.system(size: 7))
+                .foregroundColor(.primary.opacity(0.55))
+            Text(axisValue(value))
+                .font(.caption2)
+                .foregroundColor(.secondary)
+        }
+        .fixedSize()
+        .position(x: min(max(width * fraction, 12), width - 12), y: 11)
+    }
+}