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

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 {
 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
     // Helper function to decide how to pick the glucose color
     static func getDynamicGlucoseColor(
     static func getDynamicGlucoseColor(
         glucoseValue: Decimal,
         glucoseValue: Decimal,
@@ -87,14 +92,14 @@ extension Color {
                 targetGlucose: targetGlucose
                 targetGlucose: targetGlucose
             )
             )
         }
         }
-        // Otheriwse, use static (orange = high, red = low, green = range)
+        // Otherwise, use static colors
         else {
         else {
             if glucoseValue >= highGlucoseColorValue {
             if glucoseValue >= highGlucoseColorValue {
-                return Color.orange
+                return Color.staticHigh
             } else if glucoseValue <= lowGlucoseColorValue {
             } else if glucoseValue <= lowGlucoseColorValue {
-                return Color.red
+                return Color.staticLow
             } else {
             } else {
-                return Color.green
+                return Color.staticInRange
             }
             }
         }
         }
     }
     }

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

@@ -18,14 +18,14 @@ public func getDynamicGlucoseColor(
             targetGlucose: targetGlucose
             targetGlucose: targetGlucose
         )
         )
     }
     }
-    // Otheriwse, use static (orange = high, red = low, green = range)
+    // Otherwise, use the static colors
     else {
     else {
         if glucoseValue >= highGlucoseColorValue {
         if glucoseValue >= highGlucoseColorValue {
-            return Color.orange
+            return Color.staticHigh
         } else if glucoseValue <= lowGlucoseColorValue {
         } else if glucoseValue <= lowGlucoseColorValue {
-            return Color.red
+            return Color.staticLow
         } else {
         } 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)
     let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
     return color
     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" : {
     "Dynamic Insulin Sensitivity" : {
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
@@ -112169,6 +112172,7 @@
       }
       }
     },
     },
     "Dynamic:" : {
     "Dynamic:" : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -222383,6 +222387,9 @@
         }
         }
       }
       }
     },
     },
+    "Purple = Above Range" : {
+
+    },
     "Quantity Carbs" : {
     "Quantity Carbs" : {
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
@@ -314392,6 +314399,7 @@
       }
       }
     },
     },
     "Yellow = Above Range" : {
     "Yellow = Above Range" : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {

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

@@ -24,3 +24,41 @@ struct BGTargetEntry: JSON {
     let start: String
     let start: String
     let offset: Int
     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 enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
     public var id: String { rawValue }
     public var id: String { rawValue }
-    case staticColor
     case dynamicColor
     case dynamicColor
+    case staticColor
 
 
     var displayName: String {
     var displayName: String {
         switch self {
         switch self {
-        case .staticColor:
-            return String(localized: "Static")
         case .dynamicColor:
         case .dynamicColor:
             return String(localized: "Dynamic")
             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 eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
     var high: Decimal = 180
     var high: Decimal = 180
     var low: Decimal = 70
     var low: Decimal = 70
-    var glucoseColorScheme: GlucoseColorScheme = .staticColor
+    var glucoseColorScheme: GlucoseColorScheme = .dynamicColor
     var xGridLines: Bool = true
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var yGridLines: Bool = true
     var hideInsulinBadge: Bool = false
     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.
         /// Retrieves the current glucose target based on the time of day.
         func getCurrentGlucoseTarget() async {
         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 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 {
         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(
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
                         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) {
                 } else if item.glucose < Int(lowLimit) {
                     PointMark(
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.red.gradient).symbolSize(size).interpolationMethod(.cardinal)
+                    ).foregroundStyle(Color.staticLow.gradient).symbolSize(size).interpolationMethod(.cardinal)
                 } else {
                 } else {
                     PointMark(
                     PointMark(
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         x: .value("Time", item.date ?? Date(), unit: .second),
                         y: .value("Value", Decimal(item.glucose) * conversionFactor)
                         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:
                     localized:
                     "Low",
                     "Low",
                     comment: ""
                     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(
                 String(
                     localized:
                     localized:
                     "High",
                     "High",
                     comment: ""
                     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(maxHeight: 25)
         }
         }
         .frame(maxWidth: .infinity, alignment: .center) // Align the entire VStack to center
         .frame(maxWidth: .infinity, alignment: .center) // Align the entire VStack to center
@@ -194,13 +194,13 @@ struct ChartsView: View {
                 localized:
                 localized:
                 "Low",
                 "Low",
                 comment: ""
                 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(
             String(
                 localized:
                 localized:
                 "High",
                 "High",
                 comment: ""
                 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)
                         .foregroundColor(.secondary)
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                         .frame(width: 45, alignment: .trailing)
                         .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.orange)
+                        .foregroundColor(.staticHigh)
                 }.font(.caption)
                 }.font(.caption)
                 HStack {
                 HStack {
                     let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
                     let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
@@ -227,7 +227,7 @@ struct ChartsView: View {
                         .foregroundColor(.secondary)
                         .foregroundColor(.secondary)
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                         .frame(width: 45, alignment: .trailing)
                         .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.green)
+                        .foregroundColor(.staticInRange)
                 }.font(.caption)
                 }.font(.caption)
                 HStack {
                 HStack {
                     let value = 100.0 * Double(mapGlucoseAcuteLow.count) / Double(mapGlucose.count)
                     let value = 100.0 * Double(mapGlucoseAcuteLow.count) / Double(mapGlucose.count)
@@ -236,7 +236,7 @@ struct ChartsView: View {
                         .foregroundColor(.secondary)
                         .foregroundColor(.secondary)
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                     Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
                         .frame(width: 45, alignment: .trailing)
                         .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.red)
+                        .foregroundColor(.staticLow)
                 }.font(.caption)
                 }.font(.caption)
             }
             }
         }
         }
@@ -254,14 +254,14 @@ struct ChartsView: View {
                     let value = 100.0 * Double(mapGlucoseLow.count) / Double(mapGlucose.count)
                     let value = 100.0 * Double(mapGlucoseLow.count) / Double(mapGlucose.count)
                     Text(units == .mmolL ? "< 3.3" : "< 59").font(.caption2).foregroundColor(.secondary)
                     Text(units == .mmolL ? "< 3.3" : "< 59").font(.caption2).foregroundColor(.secondary)
                     Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
                     Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
-                        .foregroundColor(value == 0 ? .green : .red)
+                        .foregroundColor(value == 0 ? .staticInRange : .staticLow)
                     Text("%").font(.caption)
                     Text("%").font(.caption)
                 }
                 }
                 Spacer()
                 Spacer()
                 HStack {
                 HStack {
                     let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
                     let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
                     Text(units == .mmolL ? "3.9-7.8" : "70-140").foregroundColor(.secondary)
                     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)
                     Text("%").foregroundColor(.secondary)
                 }.font(.caption)
                 }.font(.caption)
                 Spacer()
                 Spacer()
@@ -269,7 +269,7 @@ struct ChartsView: View {
                     let value = 100.0 * Double(mapGlucoseAcuteHigh.count) / Double(mapGlucose.count)
                     let value = 100.0 * Double(mapGlucoseAcuteHigh.count) / Double(mapGlucose.count)
                     Text(units == .mmolL ? "> 11.0" : "> 198").font(.caption).foregroundColor(.secondary)
                     Text(units == .mmolL ? "> 11.0" : "> 198").font(.caption).foregroundColor(.secondary)
                     Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
                     Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
-                        .foregroundColor(value == 0 ? .green : .orange)
+                        .foregroundColor(value == 0 ? .staticInRange : .staticHigh)
                     Text("%").font(.caption)
                     Text("%").font(.caption)
                 }
                 }
                 Spacer()
                 Spacer()

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

@@ -146,12 +146,12 @@ struct GlucoseDailyDistributionChart: View {
             }
             }
         }
         }
         .chartForegroundStyleScale([
         .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))
         .chartXSelection(value: $selectedDate.animation(.easeInOut))
         .onChange(of: selectedDate) { _, newValue in
         .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),
             "10-90%": .blue.opacity(0.3),
             "25-75%": .blue.opacity(0.5),
             "25-75%": .blue.opacity(0.5),
             "Median": .blue,
             "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)
         .chartScrollableAxes(.horizontal)
         .chartScrollPosition(x: $scrollPosition)
         .chartScrollPosition(x: $scrollPosition)

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

@@ -25,38 +25,38 @@ struct GlucoseDistributionChart: View {
                 }
                 }
             }
             }
             .chartForegroundStyleScale([
             .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) {
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
                 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)",
                         "\(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)",
                         "\(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)",
                         "\(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)",
                         "\(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)",
                         "\(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)]
                 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),
                 "10-90%": Color.blue.opacity(0.3),
                 "25-75%": Color.blue.opacity(0.5),
                 "25-75%": Color.blue.opacity(0.5),
                 "Median": Color.blue,
                 "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) {
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
                 let legendItems: [(String, Color)] = [
                     ("10-90%", Color.blue.opacity(0.3)),
                     ("10-90%", Color.blue.opacity(0.3)),
                     ("25-75%", Color.blue.opacity(0.5)),
                     ("25-75%", Color.blue.opacity(0.5)),
                     (String(localized: "Median"), Color.blue),
                     (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)]
                 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)
                         .font(.subheadline)
                         .foregroundStyle(Color.secondary)
                         .foregroundStyle(Color.secondary)
                         Text(formatPercentage(inRangePercentage, tight: true))
                         Text(formatPercentage(inRangePercentage, tight: true))
-                            .foregroundStyle(Color.loopGreen)
+                            .foregroundStyle(Color.dynamicTeal)
                     }
                     }
 
 
                     VStack(alignment: .leading, spacing: 5) {
                     VStack(alignment: .leading, spacing: 5) {
@@ -82,7 +82,7 @@ struct GlucoseSectorChart: View {
                         .font(.subheadline)
                         .font(.subheadline)
                         .foregroundStyle(Color.secondary)
                         .foregroundStyle(Color.secondary)
                         Text(formatPercentage(tightPercentage, tight: true))
                         Text(formatPercentage(tightPercentage, tight: true))
-                            .foregroundStyle(Color.green)
+                            .foregroundStyle(Color.dynamicGreen)
                     }
                     }
                 }.padding(.leading, 5)
                 }.padding(.leading, 5)
 
 
@@ -91,7 +91,7 @@ struct GlucoseSectorChart: View {
                         Text("> \(highLimit.formatted(for: units))").font(.subheadline)
                         Text("> \(highLimit.formatted(for: units))").font(.subheadline)
                             .foregroundStyle(Color.secondary)
                             .foregroundStyle(Color.secondary)
                         Text(formatPercentage(highPercentage, tight: true))
                         Text(formatPercentage(highPercentage, tight: true))
-                            .foregroundStyle(Color.loopYellow)
+                            .foregroundStyle(showChart ? Color.dynamicPurple : Color.dynamicBlue)
                     }
                     }
 
 
                     VStack(alignment: .leading, spacing: 5) {
                     VStack(alignment: .leading, spacing: 5) {
@@ -101,7 +101,7 @@ struct GlucoseSectorChart: View {
                         .font(.subheadline)
                         .font(.subheadline)
                         .foregroundStyle(Color.secondary)
                         .foregroundStyle(Color.secondary)
                         Text(formatPercentage(lowPercentage, tight: true))
                         Text(formatPercentage(lowPercentage, tight: true))
-                            .foregroundStyle(Color.red)
+                            .foregroundStyle(showChart ? Color.dynamicRed : Color.dynamicOrange)
                     }
                     }
                 }
                 }
                 // If not showing chart, show extra stats
                 // If not showing chart, show extra stats
@@ -111,7 +111,7 @@ struct GlucoseSectorChart: View {
                             Text("> \(Decimal(220).formatted(for: units))").font(.subheadline)
                             Text("> \(Decimal(220).formatted(for: units))").font(.subheadline)
                                 .foregroundStyle(Color.secondary)
                                 .foregroundStyle(Color.secondary)
                             Text(formatPercentage(moderatelyHighPercentage, tight: true))
                             Text(formatPercentage(moderatelyHighPercentage, tight: true))
-                                .foregroundStyle(Color.loopYellow)
+                                .foregroundStyle(Color.dynamicBlue)
                         }
                         }
 
 
                         VStack(alignment: .leading, spacing: 5) {
                         VStack(alignment: .leading, spacing: 5) {
@@ -121,7 +121,7 @@ struct GlucoseSectorChart: View {
                             .font(.subheadline)
                             .font(.subheadline)
                             .foregroundStyle(Color.secondary)
                             .foregroundStyle(Color.secondary)
                             Text(formatPercentage(moderatelyLowPercentage, tight: true))
                             Text(formatPercentage(moderatelyLowPercentage, tight: true))
-                                .foregroundStyle(Color.red)
+                                .foregroundStyle(Color.dynamicOrange)
                         }
                         }
                     }
                     }
                     VStack(alignment: .leading, spacing: 10) {
                     VStack(alignment: .leading, spacing: 10) {
@@ -129,7 +129,7 @@ struct GlucoseSectorChart: View {
                             Text("> \(Decimal(250).formatted(for: units))").font(.subheadline)
                             Text("> \(Decimal(250).formatted(for: units))").font(.subheadline)
                                 .foregroundStyle(Color.secondary)
                                 .foregroundStyle(Color.secondary)
                             Text(formatPercentage(veryHighPercentage, tight: true))
                             Text(formatPercentage(veryHighPercentage, tight: true))
-                                .foregroundStyle(Color.orange)
+                                .foregroundStyle(Color.dynamicPurple)
                         }
                         }
 
 
                         VStack(alignment: .leading, spacing: 5) {
                         VStack(alignment: .leading, spacing: 5) {
@@ -139,7 +139,7 @@ struct GlucoseSectorChart: View {
                             .font(.subheadline)
                             .font(.subheadline)
                             .foregroundStyle(Color.secondary)
                             .foregroundStyle(Color.secondary)
                             Text(formatPercentage(veryLowPercentage, tight: true))
                             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 array of tuples with range data
         return [
         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(
             return RangeDetail(
                 title: String(localized: "High Glucose"),
                 title: String(localized: "High Glucose"),
-                color: .loopYellow,
+                color: .dynamicPurple,
                 items: [
                 items: [
                     (
                     (
                         String(localized: "Very High (>\(Decimal(250).formatted(for: units)))"),
                         String(localized: "Very High (>\(Decimal(250).formatted(for: units)))"),
@@ -304,7 +304,7 @@ struct GlucoseSectorChart: View {
 
 
             return RangeDetail(
             return RangeDetail(
                 title: String(localized: "In Range"),
                 title: String(localized: "In Range"),
-                color: .green,
+                color: .dynamicGreen,
                 items: [
                 items: [
                     (
                     (
                         String(
                         String(
@@ -334,7 +334,7 @@ struct GlucoseSectorChart: View {
 
 
             return RangeDetail(
             return RangeDetail(
                 title: String(localized: "Low Glucose"),
                 title: String(localized: "Low Glucose"),
-                color: .red,
+                color: .dynamicRed,
                 items: [
                 items: [
                     (
                     (
                         String(
                         String(

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

@@ -147,11 +147,11 @@ struct ForecastChart: View {
                 .zIndex(-1)
                 .zIndex(-1)
                 .symbolSize(CGSize(width: 15, height: 15))
                 .symbolSize(CGSize(width: 15, height: 15))
                 .foregroundStyle(
                 .foregroundStyle(
-                    Decimal(selectedGlucose.glucose) > state.highGlucose ? Color.orange
+                    Decimal(selectedGlucose.glucose) > state.highGlucose ? Color.staticHigh
                         .opacity(0.8) :
                         .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 {}
     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 {
 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 forecastDisplayType: ForecastDisplayType = .cone
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
         @Published var carbsRequiredThreshold: Decimal = 0
-        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
+        @Published var glucoseColorScheme: GlucoseColorScheme = .dynamicColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
         @Published var requireAdjustmentsConfirmation: Bool = false
         @Published var requireAdjustmentsConfirmation: Bool = false
+        @Published var currentGlucoseTarget: Decimal = 100
 
 
         var units: GlucoseUnits = .mgdL
         var units: GlucoseUnits = .mgdL
 
 
@@ -48,6 +49,16 @@ extension UserInterfaceSettings {
 
 
             subscribeSetting(\.requireAdjustmentsConfirmation, on: $requireAdjustmentsConfirmation) {
             subscribeSetting(\.requireAdjustmentsConfirmation, on: $requireAdjustmentsConfirmation) {
                 requireAdjustmentsConfirmation = $0 }
                 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."
                                                     "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) {
                                                 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("Green = At Target")
                                                     Text(
                                                     Text(
                                                         "Gradient Red = As readings approach and exceed below target, they gradually become more red."
                                                         "Gradient Red = As readings approach and exceed below target, they gradually become more red."
                                                     )
                                                     )
+                                                    .fixedSize(horizontal: false, vertical: true)
                                                     Text(
                                                     Text(
                                                         "Gradient Purple = As readings approach and exceed above target, they become more purple."
                                                         "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)
+    }
+}