Kaynağa Gözat

Pull in latest 'Core-data-sync-trio'

polscm32 1 yıl önce
ebeveyn
işleme
18ee9a3fb6
25 değiştirilmiş dosya ile 555 ekleme ve 96 silme
  1. 8 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 8 0
      FreeAPS.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  3. 1 0
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  4. 67 0
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  5. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  6. 22 0
      FreeAPS/Sources/Models/GlucoseColorScheme.swift
  7. 2 0
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  8. 19 8
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  9. 6 0
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  10. 55 0
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  11. 21 2
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  12. 15 3
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  13. 14 4
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  14. 50 37
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  15. 4 0
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  16. 3 1
      FreeAPS/Sources/Modules/Settings/SettingItems.swift
  17. 2 0
      FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift
  18. 1 0
      FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift
  19. 3 0
      FreeAPS/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift
  20. 37 1
      FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift
  21. 3 3
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  22. 3 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  23. 3 0
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  24. 202 34
      LiveActivity/LiveActivity.swift
  25. 1 1
      Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -449,6 +449,8 @@
 		DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
 		DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
+		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
+		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
@@ -1112,6 +1114,8 @@
 		DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
 		DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
+		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
+		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
 		DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
@@ -1912,6 +1916,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
 				DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
 				385CEAC025F2EA52002D6D5B /* Announcement.swift */,
 				388E5A5F25B6F2310019842D /* Autosens.swift */,
@@ -1963,6 +1968,7 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
 				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 				38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
@@ -3328,6 +3334,7 @@
 				CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
 				19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
 				582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
+				DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
@@ -3372,6 +3379,7 @@
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
+				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,

+ 8 - 0
FreeAPS.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>

+ 1 - 0
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -37,6 +37,7 @@
   "high" : 180,
   "low" : 70,
   "hours" : 6,
+  "glucoseColorScheme" : "staticColor",
   "xGridLines" : true,
   "yGridLines" : true,
   "oneDimensionalGraph" : false,

+ 67 - 0
FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift

@@ -0,0 +1,67 @@
+import Foundation
+import SwiftUI
+
+// Helper function to decide how to pick the glucose color
+public func getDynamicGlucoseColor(
+    glucoseValue: Decimal,
+    highGlucoseColorValue: Decimal,
+    lowGlucoseColorValue: Decimal,
+    targetGlucose: Decimal,
+    glucoseColorScheme: GlucoseColorScheme,
+    offset: Decimal
+) -> Color {
+    // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
+    if glucoseColorScheme == .dynamicColor {
+        return calculateHueBasedGlucoseColor(
+            glucoseValue: glucoseValue,
+            highGlucose: highGlucoseColorValue + (offset * 1.75),
+            lowGlucose: lowGlucoseColorValue - offset,
+            targetGlucose: targetGlucose
+        )
+    }
+    // Otheriwse, use static (orange = high, red = low, green = range)
+    else {
+        if glucoseValue >= highGlucoseColorValue {
+            return Color.orange
+        } else if glucoseValue <= lowGlucoseColorValue {
+            return Color.red
+        } else {
+            return Color.green
+        }
+    }
+}
+
+// Dynamic color - Define the hue values for the key points
+// We'll shift color gradually one glucose point at a time
+// We'll shift through the rainbow colors of ROY-G-BIV from low to high
+// Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
+public func calculateHueBasedGlucoseColor(
+    glucoseValue: Decimal,
+    highGlucose: Decimal,
+    lowGlucose: Decimal,
+    targetGlucose: Decimal
+) -> Color {
+    let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
+    let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
+    let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
+
+    // Calculate the hue based on the bgLevel
+    var hue: CGFloat
+    if glucoseValue <= lowGlucose {
+        hue = redHue
+    } else if glucoseValue >= highGlucose {
+        hue = purpleHue
+    } else if glucoseValue <= targetGlucose {
+        // Interpolate between red and green
+        let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
+
+        hue = redHue + ratio * (greenHue - redHue)
+    } else {
+        // Interpolate between green and purple
+        let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
+        hue = greenHue + ratio * (purpleHue - greenHue)
+    }
+    // Return the color with full saturation and brightness
+    let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
+    return color
+}

+ 5 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -53,6 +53,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var high: Decimal = 180
     var low: Decimal = 70
     var hours: Int = 6
+    var glucoseColorScheme: GlucoseColorScheme = .staticColor
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var oneDimensionalGraph: Bool = false
@@ -250,6 +251,10 @@ extension FreeAPSSettings: Decodable {
             settings.hours = hours
         }
 
+        if let glucoseColorScheme = try? container.decode(GlucoseColorScheme.self, forKey: .glucoseColorScheme) {
+            settings.glucoseColorScheme = glucoseColorScheme
+        }
+
         if let xGridLines = try? container.decode(Bool.self, forKey: .xGridLines) {
             settings.xGridLines = xGridLines
         }

+ 22 - 0
FreeAPS/Sources/Models/GlucoseColorScheme.swift

@@ -0,0 +1,22 @@
+//
+//  GlucoseColorScheme.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 27.09.24.
+//
+import Foundation
+
+public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    public var id: String { rawValue }
+    case staticColor
+    case dynamicColor
+
+    var displayName: String {
+        switch self {
+        case .staticColor:
+            return "Static"
+        case .dynamicColor:
+            return "Dynamic"
+        }
+    }
+}

+ 2 - 0
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -20,6 +20,7 @@ extension Bolus {
 
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
+        var glucoseColorScheme: GlucoseColorScheme = .staticColor
 
         var predictions: Predictions?
         var amount: Decimal = 0
@@ -233,6 +234,7 @@ extension Bolus {
             maxProtein = settings.settings.maxProtein
             useFPUconversion = settingsManager.settings.useFPUconversion
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         }
 
         private func getCurrentSettingValue(for type: SettingType) async {

+ 19 - 8
FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift

@@ -125,9 +125,20 @@ struct ForecastChart: View {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-            let pointMarkColor: Color = glucoseToDisplay > state.highGlucose ? Color.orange :
-                glucoseToDisplay < state.lowGlucose ? Color.red :
-                Color.green
+
+            // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+            let lowGlucose = state.units == .mgdL ? state.lowGlucose : state.lowGlucose.asMgdL
+            let highGlucose = state.units == .mgdL ? state.highGlucose : state.highGlucose.asMgdL
+            let targetGlucose = (state.determination.first?.currentTarget ?? state.currentBGTarget as NSDecimalNumber) as Decimal
+
+            let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
+                glucoseValue: Decimal(item.glucose),
+                highGlucoseColorValue: highGlucose,
+                lowGlucoseColorValue: lowGlucose,
+                targetGlucose: targetGlucose,
+                glucoseColorScheme: state.glucoseColorScheme,
+                offset: 20
+            )
 
             if !state.isSmoothingEnabled {
                 PointMark(
@@ -135,7 +146,7 @@ struct ForecastChart: View {
                     y: .value("Value", glucoseToDisplay)
                 )
                 .foregroundStyle(pointMarkColor)
-                .symbolSize(20)
+                .symbolSize(18)
             } else {
                 PointMark(
                     x: .value("Time", item.date ?? Date(), unit: .second),
@@ -143,7 +154,7 @@ struct ForecastChart: View {
                 )
                 .symbol {
                     Image(systemName: "record.circle.fill")
-                        .font(.system(size: 8))
+                        .font(.system(size: 6))
                         .bold()
                         .foregroundStyle(pointMarkColor)
                 }
@@ -235,8 +246,8 @@ struct ForecastChart: View {
         AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
-                .font(.footnote)
-                .foregroundStyle(Color.primary)
+                .font(.caption2)
+                .foregroundStyle(Color.secondary)
         }
     }
 
@@ -244,7 +255,7 @@ struct ForecastChart: View {
         AxisMarks(position: .trailing) { _ in
             AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
             AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
-            AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
+            AxisValueLabel().font(.caption2).foregroundStyle(Color.secondary)
         }
     }
 }

+ 6 - 0
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -53,5 +53,11 @@ extension Home {
             storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
                 ?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
         }
+
+        func getBGTarget() 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: [])
+        }
     }
 }

+ 55 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -50,6 +50,8 @@ extension Home {
         var maxValue: Decimal = 1.2
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
+        var currentGlucoseTarget: Decimal = 100
+        var glucoseColorScheme: GlucoseColorScheme = .staticColor
         var overrideUnit: Bool = false
         var displayXgridLines: Bool = false
         var displayYgridLines: Bool = false
@@ -327,6 +329,7 @@ extension Home {
             manualTempBasal = apsManager.isManualTempBasal
             setupCurrentTempTarget()
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
+            glucoseColorScheme = settingsManager.settings.glucoseColorScheme
             maxValue = settingsManager.preferences.autosensMax
             lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
             highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
@@ -492,6 +495,54 @@ extension Home {
             }
         }
 
+        private 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.getBGTarget()
+            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 = units == .mgdL ? entry.value : entry.value.asMmolL
+                    }
+                    return
+                }
+            }
+        }
+
         func openCGM() {
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
         }
@@ -535,7 +586,11 @@ extension Home.StateModel:
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
         lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
         highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+        Task {
+            await getCurrentGlucoseTarget()
+        }
         overrideUnit = settingsManager.settings.overrideHbA1cUnit
+        glucoseColorScheme = settingsManager.settings.glucoseColorScheme
         displayXgridLines = settingsManager.settings.xGridLines
         displayYgridLines = settingsManager.settings.yGridLines
         thresholdLines = settingsManager.settings.rulerMarks

+ 21 - 2
FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift

@@ -8,9 +8,28 @@ extension MainChartView {
         Chart {
             /// high and low threshold lines
             if thresholdLines {
-                RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
+                let highColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: highGlucose,
+                    highGlucoseColorValue: highGlucose,
+                    lowGlucoseColorValue: highGlucose,
+                    targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
+                    glucoseColorScheme: glucoseColorScheme,
+                    offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
+                )
+                let lowColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: lowGlucose,
+                    highGlucoseColorValue: highGlucose,
+                    lowGlucoseColorValue: lowGlucose,
+                    targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
+                    glucoseColorScheme: glucoseColorScheme,
+                    offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
+                )
+
+                RuleMark(y: .value("High", highGlucose))
+                    .foregroundStyle(highColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
-                RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(Color.loopRed)
+                RuleMark(y: .value("Low", lowGlucose))
+                    .foregroundStyle(lowColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
             }
         }

+ 15 - 3
FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift

@@ -7,7 +7,9 @@ struct GlucoseChartView: ChartContent {
     let units: GlucoseUnits
     let highGlucose: Decimal
     let lowGlucose: Decimal
+    let currentGlucoseTarget: Decimal
     let isSmoothingEnabled: Bool
+    let glucoseColorScheme: GlucoseColorScheme
 
     var body: some ChartContent {
         drawGlucoseChart()
@@ -16,9 +18,19 @@ struct GlucoseChartView: ChartContent {
     private func drawGlucoseChart() -> some ChartContent {
         ForEach(glucoseData) { item in
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-            let pointMarkColor: Color = glucoseToDisplay > highGlucose ? Color.orange :
-                glucoseToDisplay < lowGlucose ? Color.red :
-                Color.green
+
+            // low glucose and high glucose is parsed in state to mmol/L; parse it back to mg/dL here for comparison
+            let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+            let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+
+            let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
+                glucoseValue: Decimal(item.glucose),
+                highGlucoseColorValue: highGlucose,
+                lowGlucoseColorValue: lowGlucose,
+                targetGlucose: currentGlucoseTarget,
+                glucoseColorScheme: glucoseColorScheme,
+                offset: 20
+            )
 
             if !isSmoothingEnabled {
                 PointMark(

+ 14 - 4
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -12,11 +12,12 @@ struct MainChartView: View {
     var tempTargets: [TempTarget]
     var highGlucose: Decimal
     var lowGlucose: Decimal
+    var currentGlucoseTarget: Decimal
+    var glucoseColorScheme: GlucoseColorScheme
     var screenHours: Int16
     var displayXgridLines: Bool
     var displayYgridLines: Bool
     var thresholdLines: Bool
-
     var state: Home.StateModel
 
     @State var basalProfiles: [BasalProfile] = []
@@ -141,7 +142,9 @@ extension MainChartView {
                     units: state.units,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
-                    isSmoothingEnabled: state.isSmoothingEnabled
+                    currentGlucoseTarget: state.currentGlucoseTarget,
+                    isSmoothingEnabled: state.isSmoothingEnabled,
+                    glucoseColorScheme: state.glucoseColorScheme
                 )
 
                 InsulinView(
@@ -248,13 +251,20 @@ extension MainChartView {
                         .font(.body).bold()
                 }.font(.body).padding(.bottom, 5)
 
+                let glucoseColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: glucoseToShow,
+                    highGlucoseColorValue: highGlucose,
+                    lowGlucoseColorValue: lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme,
+                    offset: units == .mgdL ? 20 : 20.asMmolL
+                )
                 HStack {
                     Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
                         .bold()
                         + Text(" \(units.rawValue)")
                 }.foregroundStyle(
-                    glucoseToShow < lowGlucose ? Color
-                        .red : (glucoseToShow > highGlucose ? Color.orange : Color.primary)
+                    Color(glucoseColor)
                 ).font(.body)
 
                 if let selectedIOBValue, let iob = selectedIOBValue.iob {

+ 50 - 37
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -8,7 +8,8 @@ struct CurrentGlucoseView: View {
     let lowGlucose: Decimal
     let highGlucose: Decimal
     let cgmAvailable: Bool
-
+    var currentGlucoseTarget: Decimal
+    let glucoseColorScheme: GlucoseColorScheme
     let glucose: [GlucoseStored] // This contains the last two glucose values, no matter if its manual or a cgm reading
 
     @State private var rotationDegrees: Double = 0.0
@@ -79,15 +80,34 @@ struct CurrentGlucoseView: View {
                         if let glucoseValue = glucose.last?.glucose {
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                                 .formattedAsMmolL
-                            Text(
+
+                            // low glucose, high glucose and target is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+                            let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+                            let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+                            let targetGlucose = units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMgdL
+
+                            var glucoseDisplayColor = Color.primary
+
+                            if Decimal(glucoseValue) <= lowGlucose || Decimal(glucoseValue) >= highGlucose {
+                                glucoseDisplayColor = FreeAPS.getDynamicGlucoseColor(
+                                    glucoseValue: Decimal(glucoseValue),
+                                    highGlucoseColorValue: highGlucose,
+                                    lowGlucoseColorValue: lowGlucose,
+                                    targetGlucose: targetGlucose,
+                                    glucoseColorScheme: glucoseColorScheme,
+                                    offset: 20
+                                )
+                            }
+
+                            return Text(
                                 glucoseValue == 400 ? "HIGH" : displayGlucose
                             )
                             .font(.system(size: 40, weight: .bold, design: .rounded))
-                            .foregroundColor(alarm == nil ? glucoseDisplayColor : .loopRed)
+                            .foregroundStyle(glucoseDisplayColor)
                         } else {
-                            Text("--")
+                            return Text("--")
                                 .font(.system(size: 40, weight: .bold, design: .rounded))
-                                .foregroundColor(.secondary)
+                                .foregroundStyle(.secondary)
                         }
                     }
                     HStack {
@@ -99,18 +119,18 @@ struct CurrentGlucoseView: View {
                                     NSLocalizedString("min", comment: "Short form for minutes") + " "
                             )
                         )
-                        .font(.caption2).foregroundColor(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
+                        .font(.caption2).foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
 
                         Text(
                             delta
                         )
-                        .font(.caption2).foregroundColor(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
+                        .font(.caption2).foregroundStyle(colorScheme == .dark ? Color.white.opacity(0.9) : Color.secondary)
                     }.frame(alignment: .top)
                 }
             }
-            .onChange(of: glucose.last?.directionEnum) { newDirection in
+            .onChange(of: glucose.last?.directionEnum) {
                 withAnimation {
-                    switch newDirection {
+                    switch glucose.last?.directionEnum {
                     case .doubleUp,
                          .singleUp,
                          .tripleUp:
@@ -160,34 +180,27 @@ struct CurrentGlucoseView: View {
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
 
-    var glucoseDisplayColor: Color {
-        guard let lastGlucose = glucose.last?.glucose else { return .primary }
-
-        // Convert the lastest glucose value to Int for comparison
-        let whichGlucose = Int(lastGlucose)
-
-        // Define default color based on the color scheme
-        let defaultColor: Color = colorScheme == .dark ? .white : .black
-
-        // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
-        let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
-        let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
-
-        // Ensure the thresholds are logical
-        guard lowGlucose < highGlucose else { return .primary }
-
-        // Perform range checks using Int converted values
-        switch whichGlucose {
-        case 0 ..< Int(lowGlucose):
-            return .loopRed
-        case Int(lowGlucose) ..< Int(highGlucose):
-            return defaultColor
-        case Int(highGlucose)...:
-            return .loopYellow
-        default:
-            return defaultColor
-        }
-    }
+//    var glucoseDisplayColor: Color {
+//        guard let lastGlucose = glucose.last?.glucose else { return .primary }
+//
+//        // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
+//        let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
+//        let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+//
+//        // Ensure the thresholds are logical
+//        guard lowGlucose < highGlucose else { return .primary }
+//
+//        guard Decimal(lastGlucose) <= lowGlucose && Decimal(lastGlucose) >= highGlucose else { return .primary }
+//
+//        return FreeAPS.getDynamicGlucoseColor(
+//            glucoseValue: Decimal(lastGlucose),
+//            highGlucoseColorValue: highGlucose,
+//            lowGlucoseColorValue: lowGlucose,
+//            targetGlucose: currentGlucoseTarget,
+//            glucoseColorScheme: glucoseColorScheme,
+//            offset: 20
+//        )
+//    }
 }
 
 struct Triangle: Shape {

+ 4 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -134,6 +134,8 @@ extension Home {
                 lowGlucose: state.lowGlucose,
                 highGlucose: state.highGlucose,
                 cgmAvailable: state.cgmAvailable,
+                currentGlucoseTarget: state.currentGlucoseTarget,
+                glucoseColorScheme: state.glucoseColorScheme,
                 glucose: state.latestTwoGlucoseValues
             ).scaleEffect(0.9)
                 .onTapGesture {
@@ -339,6 +341,8 @@ extension Home {
                     tempTargets: state.tempTargets,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
+                    currentGlucoseTarget: state.currentGlucoseTarget,
+                    glucoseColorScheme: state.glucoseColorScheme,
                     screenHours: state.hours,
                     displayXgridLines: state.displayXgridLines,
                     displayYgridLines: state.displayYgridLines,

+ 3 - 1
FreeAPS/Sources/Modules/Settings/SettingItems.swift

@@ -191,7 +191,9 @@ enum SettingItems {
                 "Standing / Laying TIR Chart",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
-                "Forecast Display Type"
+                "Forecast Display Type",
+                "Trio Color Scheme",
+                "Glucose Color Scheme"
             ],
             path: ["Features", "User Interface"]
         ),

+ 2 - 0
FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -12,6 +12,7 @@ extension StatConfig {
         @Published var low: Decimal = 70
         @Published var high: Decimal = 180
         @Published var hours: Decimal = 6
+        @Published var dynamicGlucoseColor = false
         @Published var xGridLines = false
         @Published var yGridLines: Bool = false
         @Published var oneDimensionalGraph = false
@@ -25,6 +26,7 @@ extension StatConfig {
             self.units = units
 
             subscribeSetting(\.overrideHbA1cUnit, on: $overrideHbA1cUnit) { overrideHbA1cUnit = $0 }
+            subscribeSetting(\.dynamicGlucoseColor, on: $dynamicGlucoseColor) { dynamicGlucoseColor = $0 }
             subscribeSetting(\.xGridLines, on: $xGridLines) { xGridLines = $0 }
             subscribeSetting(\.yGridLines, on: $yGridLines) { yGridLines = $0 }
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }

+ 1 - 0
FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -45,6 +45,7 @@ extension StatConfig {
         var body: some View {
             Form {
                 Section {
+                    Toggle("Use Dynamic BG Color", isOn: $state.dynamicGlucoseColor)
                     Toggle("Display Chart X - Grid lines", isOn: $state.xGridLines)
                     Toggle("Display Chart Y - Grid lines", isOn: $state.yGridLines)
                     Toggle("Display Chart Threshold lines for Low and High", isOn: $state.rulerMarks)

+ 3 - 0
FreeAPS/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -14,6 +14,7 @@ extension UserInterfaceSettings {
         @Published var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         @Published var showCarbsRequiredBadge: Bool = true
         @Published var carbsRequiredThreshold: Decimal = 0
+        @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
 
         var units: GlucoseUnits = .mgdL
 
@@ -41,6 +42,8 @@ extension UserInterfaceSettings {
                 \.carbsRequiredThreshold,
                 on: $carbsRequiredThreshold
             ) { carbsRequiredThreshold = $0 }
+
+            subscribeSetting(\.glucoseColorScheme, on: $glucoseColorScheme) { glucoseColorScheme = $0 }
         }
     }
 }

+ 37 - 1
FreeAPS/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -61,7 +61,7 @@ extension UserInterfaceSettings {
                         VStack {
                             Picker(
                                 selection: $colorSchemePreference,
-                                label: Text("Color Scheme")
+                                label: Text("Trio Color Scheme")
                             ) {
                                 ForEach(ColorSchemeOption.allCases) { selection in
                                     Text(selection.displayName).tag(selection)
@@ -93,6 +93,42 @@ extension UserInterfaceSettings {
                     }
                 ).listRowBackground(Color.chart)
 
+                Section {
+                    VStack {
+                        Picker(
+                            selection: $state.glucoseColorScheme,
+                            label: Text("Glucose Color Scheme")
+                        ) {
+                            ForEach(GlucoseColorScheme.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+
+                        HStack(alignment: .top) {
+                            Text(
+                                "Glucose Color Scheme ... dynamic or static ... Lorem ipsum dolor"
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = "Glucose Color Scheme"
+                                    selectedVerboseHint =
+                                        "Glucose Color Scheme... Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.listRowBackground(Color.chart)
+
                 Section(
                     header: Text("Home View Settings"),
                     content: {

+ 3 - 3
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -7,7 +7,9 @@ struct LiveActivityAttributes: ActivityAttributes {
         let direction: String?
         let change: String
         let date: Date
-
+        let highGlucose: Decimal
+        let lowGlucose: Decimal
+        let glucoseColorScheme: String
         let detailedViewState: ContentAdditionalState?
 
         /// true for the first state that is set on the activity
@@ -18,8 +20,6 @@ struct LiveActivityAttributes: ActivityAttributes {
         let chart: [Decimal]
         let chartDate: [Date?]
         let rotationDegrees: Double
-        let highGlucose: Decimal
-        let lowGlucose: Decimal
         let cob: Decimal
         let iob: Decimal
         let unit: String

+ 3 - 2
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -87,8 +87,6 @@ extension LiveActivityAttributes.ContentState {
                 chart: chartBG,
                 chartDate: chartDate,
                 rotationDegrees: rotationDegrees,
-                highGlucose: settings.high,
-                lowGlucose: settings.low,
                 cob: Decimal(determination?.cob ?? 0),
                 iob: determination?.iob ?? 0 as Decimal,
                 unit: settings.units.rawValue,
@@ -104,6 +102,9 @@ extension LiveActivityAttributes.ContentState {
             direction: trendString,
             change: change,
             date: bg.date,
+            highGlucose: settings.high,
+            lowGlucose: settings.low,
+            glucoseColorScheme: settings.glucoseColorScheme.rawValue,
             detailedViewState: detailedState,
             isInitialState: false
         )

+ 3 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -192,6 +192,9 @@ import UIKit
                         direction: nil,
                         change: "--",
                         date: Date.now,
+                        highGlucose: settings.high,
+                        lowGlucose: settings.low,
+                        glucoseColorScheme: settings.glucoseColorScheme.rawValue,
                         detailedViewState: nil,
                         isInitialState: true
                     ),

+ 202 - 34
LiveActivity/LiveActivity.swift

@@ -17,6 +17,11 @@ enum GlucoseUnits: String, Equatable {
     static let exchangeRate: Decimal = 0.0555
 }
 
+enum GlucoseColorScheme: String, Equatable {
+    case staticColor
+    case dynamicColor
+}
+
 func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
     var result = Decimal()
     var toRound = value
@@ -60,6 +65,76 @@ extension NumberFormatter {
 }
 
 struct LiveActivity: Widget {
+    // Helper function to decide how to pick the glucose color
+    func getDynamicGlucoseColor(
+        glucoseValue: Decimal,
+        highGlucoseColorValue: Decimal,
+        lowGlucoseColorValue: Decimal,
+        targetGlucose: Decimal,
+        glucoseColorScheme: String,
+        offset: Decimal
+    ) -> Color {
+        // Convert Decimal to Int for high and low glucose values
+        let lowGlucose = lowGlucoseColorValue - offset
+        let highGlucose = highGlucoseColorValue + (offset * 1.75)
+        let targetGlucose = targetGlucose
+
+        // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
+        if glucoseColorScheme == "dynamicColor" {
+            return calculateHueBasedGlucoseColor(
+                glucoseValue: glucoseValue,
+                highGlucose: highGlucose,
+                lowGlucose: lowGlucose,
+                targetGlucose: targetGlucose
+            )
+        }
+        // Otheriwse, use static (orange = high, red = low, green = range)
+        else {
+            if glucoseValue > highGlucose {
+                return Color.orange
+            } else if glucoseValue < lowGlucose {
+                return Color.red
+            } else {
+                return Color.green
+            }
+        }
+    }
+
+    // Dynamic color - Define the hue values for the key points
+    // We'll shift color gradually one glucose point at a time
+    // We'll shift through the rainbow colors of ROY-G-BIV from low to high
+    // Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
+    func calculateHueBasedGlucoseColor(
+        glucoseValue: Decimal,
+        highGlucose: Decimal,
+        lowGlucose: Decimal,
+        targetGlucose: Decimal
+    ) -> Color {
+        let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
+        let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
+        let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
+
+        // Calculate the hue based on the bgLevel
+        var hue: CGFloat
+        if glucoseValue <= lowGlucose {
+            hue = redHue
+        } else if glucoseValue >= highGlucose {
+            hue = purpleHue
+        } else if glucoseValue <= targetGlucose {
+            // Interpolate between red and green
+            let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
+
+            hue = redHue + ratio * (greenHue - redHue)
+        } else {
+            // Interpolate between green and purple
+            let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
+            hue = greenHue + ratio * (purpleHue - greenHue)
+        }
+        // Return the color with full saturation and brightness
+        let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
+        return color
+    }
+
     private let dateFormatter: DateFormatter = {
         var f = DateFormatter()
         f.dateStyle = .none
@@ -179,7 +254,12 @@ struct LiveActivity: Widget {
         }
     }
 
-    private func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>, size: Size) -> (some View, Int) {
+    private func bgAndTrend(
+        context: ActivityViewContext<LiveActivityAttributes>,
+        size: Size,
+        hasStaticColorScheme: Bool,
+        glucoseColor: Color
+    ) -> (some View, Int) {
         var characters = 0
 
         let bgText = context.state.bg
@@ -188,16 +268,11 @@ struct LiveActivity: Widget {
         // narrow mode is for the minimal dynamic island view
         // there is not enough space to show all three arrow there
         // and everything has to be squeezed together to some degree
-        // only display the first arrow character and make it red in case there were more characters
+        // only display the first arrow character
         var directionText: String?
-        var warnColor: Color?
         if let direction = context.state.direction {
             if size == .compact {
                 directionText = String(direction[direction.startIndex ... direction.startIndex])
-
-                if direction.count > 1 {
-                    warnColor = Color.red
-                }
             } else {
                 directionText = direction
             }
@@ -214,17 +289,15 @@ struct LiveActivity: Widget {
 
         let stack = HStack(spacing: spacing) {
             Text(bgText)
+                .foregroundColor(hasStaticColorScheme ? .primary : glucoseColor)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
             if let direction = directionText {
                 let text = Text(direction)
                 switch size {
                 case .minimal:
                     let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
-                    if let warnColor {
-                        scaledText.foregroundStyle(warnColor)
-                    } else {
-                        scaledText
-                    }
+                    scaledText.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
                 case .compact:
                     text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
 
@@ -233,7 +306,7 @@ struct LiveActivity: Widget {
                 }
             }
         }
-        .foregroundStyle(context.isStale ? Color.primary.opacity(0.5) : Color.primary)
+        .foregroundColor(context.isStale ? Color.primary.opacity(0.5) : (hasStaticColorScheme ? .primary : glucoseColor))
 
         return (stack, characters)
     }
@@ -268,33 +341,61 @@ struct LiveActivity: Widget {
             let min = min(additionalState.chart.min() ?? 45, 40) - 20
             let max = max(additionalState.chart.max() ?? 270, 300) + 50
 
-            let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? additionalState.lowGlucose : additionalState.lowGlucose
+            let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? context.state.lowGlucose : context.state.lowGlucose
                 .asMmolL
-            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? additionalState.highGlucose : additionalState.highGlucose
+            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? context.state.highGlucose : context.state.highGlucose
                 .asMmolL
 
+            // TODO: grab target from proper targets, do not hard code.
+            let highColor = getDynamicGlucoseColor(
+                glucoseValue: yAxisRuleMarkMax,
+                highGlucoseColorValue: yAxisRuleMarkMax,
+                lowGlucoseColorValue: yAxisRuleMarkMin,
+                targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
+                glucoseColorScheme: context.state.glucoseColorScheme,
+                offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
+            )
+
+            // TODO: grab target from proper targets, do not hard code.
+            let lowColor = getDynamicGlucoseColor(
+                glucoseValue: yAxisRuleMarkMin,
+                highGlucoseColorValue: yAxisRuleMarkMax,
+                lowGlucoseColorValue: yAxisRuleMarkMin,
+                targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
+                glucoseColorScheme: context.state.glucoseColorScheme,
+                offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
+            )
+
             Chart {
-                RuleMark(y: .value("Low", yAxisRuleMarkMin))
-                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
                 RuleMark(y: .value("High", yAxisRuleMarkMax))
+                    .foregroundStyle(highColor)
+                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
+                RuleMark(y: .value("Low", yAxisRuleMarkMin))
+                    .foregroundStyle(lowColor)
                     .lineStyle(.init(lineWidth: 0.5, dash: [5]))
 
                 ForEach(additionalState.chart.indices, id: \.self) { index in
                     let currentValue = additionalState.chart[index]
                     let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
+
+                    // TODO: grab target from proper targets, do not hard code.
+                    let pointMarkColor = self.getDynamicGlucoseColor(
+                        glucoseValue: currentValue,
+                        highGlucoseColorValue: context.state.highGlucose,
+                        lowGlucoseColorValue: context.state.lowGlucose,
+                        targetGlucose: 90,
+                        glucoseColorScheme: context.state.glucoseColorScheme,
+                        offset: 20
+                    )
+
                     let chartDate = additionalState.chartDate[index] ?? Date()
+
                     let pointMark = PointMark(
                         x: .value("Time", chartDate),
                         y: .value("Value", displayValue)
                     ).symbolSize(15)
 
-                    if displayValue > yAxisRuleMarkMax {
-                        pointMark.foregroundStyle(Color.orange.gradient)
-                    } else if displayValue < yAxisRuleMarkMin {
-                        pointMark.foregroundStyle(Color.red.gradient)
-                    } else {
-                        pointMark.foregroundStyle(Color.green.gradient)
-                    }
+                    pointMark.foregroundStyle(pointMarkColor)
                 }
             }
             .chartYAxis {
@@ -313,9 +414,21 @@ struct LiveActivity: Widget {
     }
 
     @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+        // TODO: grab target from proper targets, do not hard code.
+        let glucoseColor = getDynamicGlucoseColor(
+            glucoseValue: Decimal(string: context.state.bg) ?? 100,
+            highGlucoseColorValue: context.state.highGlucose,
+            lowGlucoseColorValue: context.state.lowGlucose,
+            targetGlucose: 90,
+            glucoseColorScheme: context.state.glucoseColorScheme,
+            offset: 20
+        )
+
         if let detailedViewState = context.state.detailedViewState {
             HStack(spacing: 12) {
-                chart(context: context, additionalState: detailedViewState).frame(maxWidth: UIScreen.main.bounds.width / 1.8)
+                chart(context: context, additionalState: detailedViewState)
+                    .frame(maxWidth: UIScreen.main.bounds.width / 1.8)
                 VStack(alignment: .leading) {
                     Spacer()
                     bgLabel(context: context, additionalState: detailedViewState)
@@ -347,11 +460,21 @@ struct LiveActivity: Widget {
                     }
                 } else {
                     HStack(spacing: 3) {
-                        bgAndTrend(context: context, size: .expanded).0.font(.title)
+                        bgAndTrend(
+                            context: context,
+                            size: .expanded,
+                            hasStaticColorScheme: hasStaticColorScheme,
+                            glucoseColor: glucoseColor
+                        ).0.font(.title)
                         Spacer()
                         VStack(alignment: .trailing, spacing: 5) {
                             changeLabel(context: context).font(.title3)
-                            updatedLabel(context: context).font(.caption).foregroundStyle(.primary.opacity(0.7))
+                                .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
+                            updatedLabel(context: context).font(.caption)
+                                .foregroundStyle(
+                                    hasStaticColorScheme ? .primary
+                                        .opacity(0.7) : glucoseColor
+                                )
                         }
                     }
                 }
@@ -368,12 +491,33 @@ struct LiveActivity: Widget {
     }
 
     func dynamicIsland(context: ActivityViewContext<LiveActivityAttributes>) -> DynamicIsland {
-        DynamicIsland {
+        let glucoseValueForColor = context.state.bg
+        let highGlucose = context.state.highGlucose
+        let lowGlucose = context.state.lowGlucose
+
+        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+        // TODO: grab target from proper targets, do not hard code.
+        let glucoseColor = getDynamicGlucoseColor(
+            glucoseValue: Decimal(string: glucoseValueForColor) ?? 100,
+            highGlucoseColorValue: highGlucose,
+            lowGlucoseColorValue: lowGlucose,
+            targetGlucose: 90,
+            glucoseColorScheme: context.state.glucoseColorScheme,
+            offset: 20
+        )
+
+        return DynamicIsland {
             DynamicIslandExpandedRegion(.leading) {
-                bgAndTrend(context: context, size: .expanded).0.font(.title2).padding(.leading, 5)
+                bgAndTrend(
+                    context: context,
+                    size: .expanded,
+                    hasStaticColorScheme: hasStaticColorScheme,
+                    glucoseColor: glucoseColor
+                ).0.font(.title2).padding(.leading, 5)
             }
             DynamicIslandExpandedRegion(.trailing) {
                 changeLabel(context: context).font(.title2).padding(.trailing, 5)
+                    .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
             }
             DynamicIslandExpandedRegion(.bottom) {
                 if context.state.isInitialState {
@@ -396,11 +540,17 @@ struct LiveActivity: Widget {
                 }
             }
         } compactLeading: {
-            bgAndTrend(context: context, size: .compact).0.padding(.leading, 4)
+            bgAndTrend(context: context, size: .compact, hasStaticColorScheme: hasStaticColorScheme, glucoseColor: glucoseColor).0
+                .padding(.leading, 4)
         } compactTrailing: {
-            changeLabel(context: context).padding(.trailing, 4)
+            changeLabel(context: context).padding(.trailing, 4).foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
         } minimal: {
-            let (_label, characterCount) = bgAndTrend(context: context, size: .minimal)
+            let (_label, characterCount) = bgAndTrend(
+                context: context,
+                size: .minimal,
+                hasStaticColorScheme: hasStaticColorScheme,
+                glucoseColor: glucoseColor
+            )
             let label = _label.padding(.leading, 7).padding(.trailing, 3)
 
             if characterCount < 4 {
@@ -412,7 +562,7 @@ struct LiveActivity: Widget {
             }
         }
         .widgetURL(URL(string: "Trio://"))
-        .keylineTint(Color.purple)
+        .keylineTint(hasStaticColorScheme ? Color.purple : glucoseColor)
         .contentMargins(.horizontal, 0, for: .minimal)
         .contentMargins(.trailing, 0, for: .compactLeading)
         .contentMargins(.leading, 0, for: .compactTrailing)
@@ -435,10 +585,13 @@ private extension LiveActivityAttributes.ContentState {
     // Use mmol/l notation with decimal point as well for the same reason, it uses up to 4 characters, while mg/dl uses up to 3
     static var testWide: LiveActivityAttributes.ContentState {
         LiveActivityAttributes.ContentState(
-            bg: "00.0",
+            bg: 00.0.description,
             direction: "→",
             change: "+0.0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -450,6 +603,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑↑",
             change: "+0.0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -461,6 +617,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑↑↑",
             change: "+0.0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -473,6 +632,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↑",
             change: "+0",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -484,6 +646,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: "↗︎",
             change: "+00",
             date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
         )
@@ -495,6 +660,9 @@ private extension LiveActivityAttributes.ContentState {
             direction: nil,
             change: "--",
             date: Date().addingTimeInterval(-60 * 60),
+            highGlucose: 180,
+            lowGlucose: 70,
+            glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: true
         )

+ 1 - 1
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -49,7 +49,7 @@
     {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
-      "location" : "https://github.com/ivanschuetz/SwiftCharts.git",
+      "location" : "https://github.com/ivanschuetz/SwiftCharts",
       "state" : {
         "branch" : "master",
         "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2"