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

Merge branch 'core-data-sync-trio' of https://github.com/bastiaanv/Trio-dev into core-data-sync-trio

bastiaanv 1 год назад
Родитель
Сommit
5c4f8a20dc

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit b786e8b5531cb08c259103c472dcd6a6752728f8
+Subproject commit cd8f6faec67b30231987b79daf0117dfcbb54741

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit bdfcfe83fbb9fab515a2456a4be9991420ed44bb
+Subproject commit 425a6b06b1d77d7bcdab7571b906c65618931f2e

+ 1 - 2
Trio/Sources/Application/TrioApp.swift

@@ -86,9 +86,8 @@ extension Notification.Name {
 
         debug(
             .default,
-            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [submodules: \(submodulesInfo)]"
+            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [Branch: \(BuildDetails.shared.branchAndSha)] [submodules: \(submodulesInfo)]"
         )
-
         // Fix bug in iOS 18 related to the translucent tab bar
         configureTabBarAppearance()
 

+ 9 - 3
Trio/Sources/Helpers/BuildDetails.swift

@@ -23,10 +23,16 @@ class BuildDetails: Injectable {
         dict["com-trio-build-date"] as? String
     }
 
+    var trioBranch: String {
+        dict["com-trio-branch"] as? String ?? String(localized: "Unknown")
+    }
+
+    var trioCommitSHA: String {
+        dict["com-trio-commit-sha"] as? String ?? String(localized: "Unknown")
+    }
+
     var branchAndSha: String {
-        let branch = dict["com-trio-branch"] as? String ?? String(localized: "Unknown")
-        let sha = dict["com-trio-commit-sha"] as? String ?? String(localized: "Unknown")
-        return "\(branch) \(sha)"
+        "\(trioBranch) \(trioCommitSHA)"
     }
 
     /// Returns a dictionary of submodule details.

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

@@ -211519,6 +211519,17 @@
         }
       }
     },
+    "Uses the fairly established Time in Tight Range definition, which is defined as time between %@ and %@  %@." : {
+      "comment" : "Time in Tight Range (TITR) verbose hint description",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Uses the fairly established Time in Tight Range definition, which is defined as time between %1$@ and %2$@  %3$@."
+          }
+        }
+      }
+    },
     "Uses the IOB, COB, UAM, and ZT forecast lines from OpenAPS. This option provides a more detailed view of the algorithm's forecast, but may be more confusing for some users." : {
       "localizations" : {
         "bg" : {
@@ -211619,6 +211630,17 @@
         }
       }
     },
+    "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (%@ %@) and %@ %@." : {
+      "comment" : "Time in Normoglycemia (TING) verbose hint description",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (%1$@ %2$@) and %3$@ %4$@."
+          }
+        }
+      }
+    },
     "Uses your Nightscout as CGM" : {
       "comment" : "Online or internal server",
       "localizations" : {

+ 12 - 5
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -10,6 +10,7 @@ extension BasalProfileEditor {
 
         let chartScale = Calendar.current
             .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+        let tzOffset = TimeZone.current.secondsFromGMT()
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
@@ -28,12 +29,17 @@ extension BasalProfileEditor {
             return formatter
         }
 
+        var now = Date()
         var basalScheduleChart: some View {
             Chart {
                 ForEach(state.chartData!, id: \.self) { profile in
+                    let startDate = Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(profile.startDate.timeIntervalSinceReferenceDate + Double(tzOffset))
+                    let endDate = Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(profile.endDate!.timeIntervalSinceReferenceDate + Double(tzOffset))
                     RectangleMark(
-                        xStart: .value("start", profile.startDate),
-                        xEnd: .value("end", profile.endDate!),
+                        xStart: .value("start", startDate),
+                        xEnd: .value("end", endDate),
                         yStart: .value("rate-start", profile.amount),
                         yEnd: .value("rate-end", 0)
                     ).foregroundStyle(
@@ -47,10 +53,10 @@ extension BasalProfileEditor {
                         )
                     ).alignsMarkStylesWithPlotArea()
 
-                    LineMark(x: .value("End Date", profile.endDate!), y: .value("Amount", profile.amount))
+                    LineMark(x: .value("End Date", endDate), y: .value("Amount", profile.amount))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
 
-                    LineMark(x: .value("Start Date", profile.startDate), y: .value("Amount", profile.amount))
+                    LineMark(x: .value("Start Date", startDate), y: .value("Amount", profile.amount))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
                 }
             }
@@ -67,7 +73,8 @@ extension BasalProfileEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar
+                    .current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
         }

+ 11 - 11
Trio/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -177,23 +177,22 @@ extension CarbRatioEditor {
             }
         }
 
-        let chartScale = Calendar.current
-            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
-
+        var now = Date()
         var chart: some View {
             Chart {
                 ForEach(state.items.indexed(), id: \.1.id) { index, item in
                     let displayValue = state.rateValues[item.rateIndex]
 
-                    let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                    let startDate = Date(timeIntervalSinceReferenceDate: state.timeValues[item.timeIndex])
-                        .addingTimeInterval(TimeInterval(tzOffset))
+                    let startDate = Calendar.current
+                        .startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[item.timeIndex])
                     let endDate = state.items
                         .count > index + 1 ?
-                        Date(timeIntervalSinceReferenceDate: state.timeValues[state.items[index + 1].timeIndex])
-                        .addingTimeInterval(TimeInterval(tzOffset)) :
-                        Date(timeIntervalSinceReferenceDate: state.timeValues.last!).addingTimeInterval(30 * 60)
-                        .addingTimeInterval(TimeInterval(tzOffset))
+                        Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[state.items[index + 1].timeIndex])
+                        :
+                        Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues.last! + 30 * 60)
                     RectangleMark(
                         xStart: .value("start", startDate),
                         xEnd: .value("end", endDate),
@@ -224,7 +223,8 @@ extension CarbRatioEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar
+                    .current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
             .chartYAxis {

+ 29 - 29
Trio/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -169,35 +169,6 @@ extension DynamicSettings {
                                 )
                             }
                         )
-
-                        SettingInputSection(
-                            decimalValue: $state.weightPercentage,
-                            booleanValue: $booleanPlaceholder,
-                            shouldDisplayHint: $shouldDisplayHint,
-                            selectedVerboseHint: Binding(
-                                get: { selectedVerboseHint },
-                                set: {
-                                    selectedVerboseHint = $0.map { AnyView($0) }
-                                    hintLabel = String(localized: "Weighted Average of TDD")
-                                }
-                            ),
-                            units: state.units,
-                            type: .decimal("weightPercentage"),
-                            label: String(localized: "Weighted Average of TDD"),
-                            miniHint: String(localized: "Weight of 24-hr TDD against 10-day TDD."),
-                            verboseHint:
-                            VStack(alignment: .leading, spacing: 10) {
-                                Text("Default: 35%").bold()
-                                Text(
-                                    "This setting adjusts how much weight is given to your recent total daily insulin dose when calculating Dynamic ISF and Dynamic CR."
-                                )
-                                Text(
-                                    "At the default setting, 35% of the calculation is based on the last 24 hours of insulin use, with the remaining 65% considering the last 10 days of data."
-                                )
-                                Text("Setting this to 100% means only the past 24 hours will be used.")
-                                Text("A lower value smooths out these variations for more stability.")
-                            }
-                        )
                     } else {
                         SettingInputSection(
                             decimalValue: $state.adjustmentFactorSigmoid,
@@ -234,6 +205,35 @@ extension DynamicSettings {
                     }
 
                     SettingInputSection(
+                        decimalValue: $state.weightPercentage,
+                        booleanValue: $booleanPlaceholder,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0.map { AnyView($0) }
+                                hintLabel = String(localized: "Weighted Average of TDD")
+                            }
+                        ),
+                        units: state.units,
+                        type: .decimal("weightPercentage"),
+                        label: String(localized: "Weighted Average of TDD"),
+                        miniHint: String(localized: "Weight of 24-hr TDD against 10-day TDD."),
+                        verboseHint:
+                        VStack(alignment: .leading, spacing: 10) {
+                            Text("Default: 35%").bold()
+                            Text(
+                                "This setting adjusts how much weight is given to your recent total daily insulin dose when calculating Dynamic ISF and Dynamic CR."
+                            )
+                            Text(
+                                "At the default setting, 35% of the calculation is based on the last 24 hours of insulin use, with the remaining 65% considering the last 10 days of data."
+                            )
+                            Text("Setting this to 100% means only the past 24 hours will be used.")
+                            Text("A lower value smooths out these variations for more stability.")
+                        }
+                    )
+
+                    SettingInputSection(
                         decimalValue: $decimalPlaceholder,
                         booleanValue: $state.tddAdjBasal,
                         shouldDisplayHint: $shouldDisplayHint,

+ 30 - 3
Trio/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -2,6 +2,16 @@ import CoreData
 import Observation
 import SwiftUI
 
+extension [Decimal] {
+    func findClosestIndex(to target: Element) -> Int? {
+        guard !isEmpty else { return nil }
+
+        return enumerated().min(by: {
+            abs($0.element - target) < abs($1.element - target)
+        })?.offset
+    }
+}
+
 extension ISFEditor {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
@@ -19,7 +29,16 @@ extension ISFEditor {
             var values = stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
 
             if units == .mmolL {
-                values = values.filter { Int(truncating: $0 as NSNumber) % 2 == 0 }
+                var mmolValues = values.filter { Int(truncating: $0 as NSNumber) % 2 == 0 }
+                // check for any missing values
+                var valuesInMmolSet = Set(mmolValues.map(\.asMmolL))
+                for value in values {
+                    let valueInMmmol = value.asMmolL
+                    if valuesInMmolSet.insert(valueInMmmol).inserted {
+                        mmolValues.append(value)
+                    }
+                }
+                values = mmolValues.sorted()
             }
 
             return values
@@ -43,8 +62,16 @@ extension ISFEditor {
 
             items = profile.sensitivities.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
-                let rateIndex = rateValues.firstIndex(of: value.sensitivity) ?? 0
-                return Item(rateIndex: rateIndex, timeIndex: timeIndex)
+                var rateIndex = rateValues.firstIndex(of: value.sensitivity)
+                if rateIndex == nil {
+                    // try to look up the closest value
+                    if let min = rateValues.first, let max = rateValues.last {
+                        if value.sensitivity >= (min - 1), value.sensitivity <= (max + 1) {
+                            rateIndex = rateValues.findClosestIndex(to: value.sensitivity)
+                        }
+                    }
+                }
+                return Item(rateIndex: rateIndex ?? 0, timeIndex: timeIndex)
             }
 
             initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }

+ 12 - 10
Trio/Sources/Modules/ISFEditor/View/ISFEditorRootView.swift

@@ -177,8 +177,7 @@ extension ISFEditor {
             }
         }
 
-        let chartScale = Calendar.current
-            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
+        var now = Date()
 
         var chart: some View {
             Chart {
@@ -190,15 +189,17 @@ extension ISFEditor {
                     // However, swift doesn't understand languages that use comma as decimal delminator
                     let displayValueFloat = Double(displayValue.replacingOccurrences(of: ",", with: "."))
 
-                    let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                    let startDate = Date(timeIntervalSinceReferenceDate: state.timeValues[item.timeIndex])
-                        .addingTimeInterval(TimeInterval(tzOffset))
+                    let startDate = Calendar.current
+                        .startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[item.timeIndex])
+
                     let endDate = state.items
                         .count > index + 1 ?
-                        Date(timeIntervalSinceReferenceDate: state.timeValues[state.items[index + 1].timeIndex])
-                        .addingTimeInterval(TimeInterval(tzOffset)) :
-                        Date(timeIntervalSinceReferenceDate: state.timeValues.last!).addingTimeInterval(30 * 60)
-                        .addingTimeInterval(TimeInterval(tzOffset))
+                        Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[state.items[index + 1].timeIndex])
+                        :
+                        Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues.last! + 30 * 60)
                     RectangleMark(
                         xStart: .value("start", startDate),
                         xEnd: .value("end", endDate),
@@ -229,7 +230,8 @@ extension ISFEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar
+                    .current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
             .chartYAxis {

+ 0 - 2
Trio/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -26,7 +26,6 @@ extension NightscoutConfig {
         @Published var isUploadEnabled = false // Allow uploads
         @Published var isDownloadEnabled = false // Allow downloads
         @Published var uploadGlucose = true // Upload Glucose
-        @Published var changeUploadGlucose = true // if plugin, need to be change in CGM configuration
         @Published var useLocalSource = false
         @Published var localPort: Decimal = 0
         @Published var units: GlucoseUnits = .mgdL
@@ -55,7 +54,6 @@ extension NightscoutConfig {
             dia = settingsManager.pumpSettings.insulinActionCurve
             maxBasal = settingsManager.pumpSettings.maxBasal
             maxBolus = settingsManager.pumpSettings.maxBolus
-            changeUploadGlucose = (cgmManager.cgmGlucoseSourceType != CGMType.plugin)
 
             subscribeSetting(\.isUploadEnabled, on: $isUploadEnabled) { isUploadEnabled = $0 }
             subscribeSetting(\.isDownloadEnabled, on: $isDownloadEnabled) { isDownloadEnabled = $0 }

+ 20 - 22
Trio/Sources/Modules/NightscoutConfig/View/NightscoutUploadView.swift

@@ -47,29 +47,27 @@ struct NightscoutUploadView: View {
                 }
             )
 
-            if state.changeUploadGlucose {
-                SettingInputSection(
-                    decimalValue: $decimalPlaceholder,
-                    booleanValue: $state.uploadGlucose,
-                    shouldDisplayHint: $shouldDisplayHint,
-                    selectedVerboseHint: Binding(
-                        get: { selectedVerboseHint },
-                        set: {
-                            selectedVerboseHint = $0.map { AnyView($0) }
-                            hintLabel = String(localized: "Upload Glucose")
-                            shouldDisplayHint = true
-                        }
-                    ),
-                    units: state.units,
-                    type: .boolean,
-                    label: String(localized: "Upload Glucose"),
-                    miniHint: String(localized: "Enable uploading of CGM readings to Nightscout."),
-                    verboseHint: VStack(alignment: .leading, spacing: 10) {
-                        Text("Default: OFF").bold()
-                        Text("Enabling this setting allows CGM readings from Trio to be used in Nightscout.")
+            SettingInputSection(
+                decimalValue: $decimalPlaceholder,
+                booleanValue: $state.uploadGlucose,
+                shouldDisplayHint: $shouldDisplayHint,
+                selectedVerboseHint: Binding(
+                    get: { selectedVerboseHint },
+                    set: {
+                        selectedVerboseHint = $0.map { AnyView($0) }
+                        hintLabel = String(localized: "Upload Glucose")
+                        shouldDisplayHint = true
                     }
-                )
-            }
+                ),
+                units: state.units,
+                type: .boolean,
+                label: String(localized: "Upload Glucose"),
+                miniHint: String(localized: "Enable uploading of CGM readings to Nightscout."),
+                verboseHint: VStack(alignment: .leading, spacing: 10) {
+                    Text("Default: OFF").bold()
+                    Text("Enabling this setting allows CGM readings from Trio to be used in Nightscout.")
+                }
+            )
         }
         .listSectionSpacing(sectionSpacing)
         .sheet(isPresented: $shouldDisplayHint) {

+ 12 - 10
Trio/Sources/Modules/Settings/SettingItems.swift

@@ -65,7 +65,7 @@ enum SettingItems {
         SettingItem(
             title: "Units and Limits",
             view: .unitsAndLimits,
-            searchContents: ["Glucose Units", "Max Basal", "Max Bolus", "Max IOB", "Max COB"],
+            searchContents: ["Glucose Units", "Max Basal", "Max Bolus", "Max IOB", "Max COB", "Minimum Safety Threshold"],
             path: ["Therapy Settings", "Units and Limits"]
         ),
         SettingItem(title: "Basal Rates", view: .basalProfileEditor, path: ["Therapy Settings"]),
@@ -97,9 +97,7 @@ enum SettingItems {
                 "Enable UAM",
                 "Max SMB Basal Minutes",
                 "Max UAM SMB Basal Minutes",
-                "Max Delta-BG Threshold SMB",
-                "SMB Delivery Ratio",
-                "SMB Interval"
+                "Max Delta-BG Threshold SMB"
             ],
             path: ["Algorithm", "Super Micro Bolus (SMB)"]
         ),
@@ -114,8 +112,7 @@ enum SettingItems {
                 "AF",
                 "Sigmoid Adjustment Factor",
                 "Weighted Average of TDD",
-                "Adjust Basal",
-                "Minimum Safety Threshold"
+                "Adjust Basal"
             ],
             path: ["Algorithm", "Dynamic Sensitivity"]
         ),
@@ -143,6 +140,8 @@ enum SettingItems {
                 "Skip Neutral Temps",
                 "Unsuspend If No Temp",
                 "Suspend Zeros IOB",
+                "SMB Delivery Ratio",
+                "SMB Interval",
                 "Min 5m Carbimpact",
                 "Remaining Carbs Fraction",
                 "Remaining Carbs Cap",
@@ -162,7 +161,8 @@ enum SettingItems {
                 "Enable Fatty Meal Factor",
                 "Fatty Meal Factor",
                 "Enable Super Bolus",
-                "Super Bolus Factor"
+                "Super Bolus Factor",
+                "Very Low Glucose Warning"
             ],
             path: ["Features", "Bolus Calculator"]
         ),
@@ -205,8 +205,7 @@ enum SettingItems {
                 "Low Threshold",
                 "High Threshold",
                 "X-Axis Interval Step",
-                "Override eA1c Unit",
-                "Standing / Laying TIR Chart",
+                "eA1c/GMI Display Unit",
                 "Show Carbs Required Badge",
                 "Carbs Required Threshold",
                 "Forecast Display Type",
@@ -217,7 +216,10 @@ enum SettingItems {
                 "Appearance",
                 "Dark Scheme",
                 "Light Scheme",
-                "Glucose Color Scheme"
+                "Glucose Color Scheme",
+                "Time in Range Type",
+                "Time in Tight Range (TITR)",
+                "Time in Normoglycemia (TING)"
             ],
             path: ["Features", "User Interface"]
         ),

+ 4 - 1
Trio/Sources/Modules/Settings/View/Subviews/SubmodulesView.swift

@@ -5,7 +5,10 @@ struct SubmodulesView: View {
 
     var body: some View {
         List {
-            Section {
+            Section(header: Text("Trio")) {
+                KeyValueRow(key: buildDetails.trioBranch, value: buildDetails.trioCommitSHA)
+            }
+            Section(header: Text("Submodules")) {
                 ForEach(buildDetails.submodules.sorted(by: { $0.key < $1.key }), id: \.key) { name, info in
                     KeyValueRow(key: name, value: info.commitSHA)
                 }

+ 0 - 3
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -143,7 +143,6 @@ extension Stat {
                 VStack(spacing: Constants.spacing) {
                     GlucoseSectorChart(
                         highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
                         units: state.units,
                         glucose: state.glucoseFromPersistence,
                         timeInRangeType: state.timeInRangeType
@@ -152,8 +151,6 @@ extension Stat {
                     Divider()
 
                     GlucoseMetricsView(
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
                         units: state.units,
                         eA1cDisplayUnit: state.eA1cDisplayUnit,
                         glucose: state.glucoseFromPersistence

+ 0 - 4
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift

@@ -4,10 +4,6 @@ import SwiftUI
 
 /// A SwiftUI view displaying various glucose-related statistics based on stored glucose readings.
 struct GlucoseMetricsView: View {
-    /// The upper glucose limit for evaluation.
-    let highLimit: Decimal
-    /// The lower glucose limit for evaluation.
-    let lowLimit: Decimal
     /// The unit of measurement for blood glucose values (e.g., mg/dL or mmol/L).
     let units: GlucoseUnits
     /// The display unit for estimated HbA1c values.

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

@@ -5,7 +5,6 @@ import SwiftUI
 
 struct GlucoseSectorChart: View {
     let highLimit: Decimal
-    let lowLimit: Decimal
     let units: GlucoseUnits
     let glucose: [GlucoseStored]
     let timeInRangeType: TimeInRangeType
@@ -27,15 +26,15 @@ struct GlucoseSectorChart: View {
         HStack(alignment: .center, spacing: 20) {
             // Calculate total number of glucose readings
             let total = Decimal(glucose.count)
-            // Count readings between high limit and 250 mg/dL (high)
+            // Count readings greater than high limit (180 mg/dL)
             let high = glucose.filter { $0.glucose > Int(highLimit) }.count
             // Count readings between low limit (TITR: 70 mg/dL, TING 63 mg/dL) and 140 mg/dL (tight control)
             let tight = glucose
                 .filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= timeInRangeType.topThreshold }.count
             // Count readings between 140 and high limit (normal range)
-            let normal = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }.count
-            // Count readings between 54 and low limit (low)
-            let low = glucose.filter { $0.glucose < Int(lowLimit) }.count
+            let normal = glucose.filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= Int(highLimit) }.count
+            // Count readings less than low limit (low)
+            let low = glucose.filter { $0.glucose < timeInRangeType.bottomThreshold }.count
 
             let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
             let sumReadings = justGlucoseArray.reduce(0, +)
@@ -50,7 +49,8 @@ struct GlucoseSectorChart: View {
 
             VStack(alignment: .leading, spacing: 10) {
                 VStack(alignment: .leading, spacing: 5) {
-                    Text("\(formatValue(lowLimit))-\(formatValue(highLimit))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text("\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(highLimit))").font(.subheadline)
+                        .foregroundStyle(Color.secondary)
                     Text(inRangePercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
                         .foregroundStyle(Color.loopGreen)
                 }
@@ -68,13 +68,18 @@ struct GlucoseSectorChart: View {
 
             VStack(alignment: .leading, spacing: 10) {
                 VStack(alignment: .leading, spacing: 5) {
-                    Text("> \(formatValue(highLimit))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text("> \(formatValue(highLimit))").font(.subheadline)
+                        .foregroundStyle(Color.secondary)
                     Text(highPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
                         .foregroundStyle(Color.orange)
                 }
 
                 VStack(alignment: .leading, spacing: 5) {
-                    Text("< \(formatValue(lowLimit))").font(.subheadline).foregroundStyle(Color.secondary)
+                    Text(
+                        "< \(formatValue(Decimal(timeInRangeType.bottomThreshold)))"
+                    )
+                    .font(.subheadline)
+                    .foregroundStyle(Color.secondary)
                     Text(lowPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
                         .foregroundStyle(Color.loopRed)
                 }
@@ -156,7 +161,7 @@ struct GlucoseSectorChart: View {
         // Count readings above high limit
         let highCount = glucose.filter { $0.glucose > Int(highLimit) }.count
         // Count readings below low limit
-        let lowCount = glucose.filter { $0.glucose < Int(lowLimit) }.count
+        let lowCount = glucose.filter { $0.glucose < timeInRangeType.bottomThreshold }.count
         // Calculate in-range readings by subtracting high and low counts from total
         let inRangeCount = total - highCount - lowCount
 
@@ -227,7 +232,7 @@ struct GlucoseSectorChart: View {
         case .inRange:
             let tight = glucose
                 .filter { $0.glucose >= Int(timeInRangeType.bottomThreshold) && $0.glucose <= timeInRangeType.topThreshold }.count
-            let glucoseValues = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= Int(highLimit) }
+            let glucoseValues = glucose.filter { $0.glucose >= timeInRangeType.bottomThreshold && $0.glucose <= Int(highLimit) }
             let glucoseValuesAsInt = glucoseValues.map { Int($0.glucose) }
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: glucoseValuesAsInt)
 
@@ -236,7 +241,9 @@ struct GlucoseSectorChart: View {
                 color: .green,
                 items: [
                     (
-                        String(localized: "Normal (\(formatValue(lowLimit))-\(formatValue(highLimit)))"),
+                        String(
+                            localized: "Normal (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(highLimit)))"
+                        ),
                         formatPercentage(Decimal(glucoseValues.count) / total * 100)
                     ),
                     (
@@ -253,9 +260,9 @@ struct GlucoseSectorChart: View {
 
         case .low:
             let veryLow = glucose.filter { $0.glucose <= 54 }.count
-            let low = glucose.filter { $0.glucose > 54 && $0.glucose < Int(lowLimit) }.count
+            let low = glucose.filter { $0.glucose > 54 && $0.glucose < timeInRangeType.bottomThreshold }.count
 
-            let lowGlucoseValues = glucose.filter { $0.glucose < Int(lowLimit) }
+            let lowGlucoseValues = glucose.filter { $0.glucose < timeInRangeType.bottomThreshold }
             let lowGlucoseValuesAsInt = lowGlucoseValues.map { Int($0.glucose) }
             let (average, median, standardDeviation) = calculateDetailedStatistics(for: lowGlucoseValuesAsInt)
 
@@ -264,7 +271,7 @@ struct GlucoseSectorChart: View {
                 color: .red,
                 items: [
                     (
-                        String(localized: "Low (\(formatValue(54))-\(formatValue(lowLimit)))"),
+                        String(localized: "Low (\(formatValue(54))-\(formatValue(Decimal(timeInRangeType.bottomThreshold))))"),
                         formatPercentage(Decimal(low) / total * 100)
                     ),
                     (String(localized: "Very Low (<\(formatValue(54)))"), formatPercentage(Decimal(veryLow) / total * 100)),

+ 12 - 11
Trio/Sources/Modules/TargetsEditor/View/TargetsEditorRootView.swift

@@ -169,9 +169,7 @@ extension TargetsEditor {
             }
         }
 
-        let chartScale = Calendar.current
-            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
-
+        var now = Date()
         var chart: some View {
             Chart {
                 ForEach(state.items.indexed(), id: \.1.id) { index, item in
@@ -182,15 +180,17 @@ extension TargetsEditor {
                     // However, swift doesn't understand languages that use comma as decimal delminator
                     let displayValueFloat = Double(displayValue.replacingOccurrences(of: ",", with: "."))
 
-                    let tzOffset = TimeZone.current.secondsFromGMT() * -1
-                    let startDate = Date(timeIntervalSinceReferenceDate: state.timeValues[item.timeIndex])
-                        .addingTimeInterval(TimeInterval(tzOffset))
+                    let startDate = Calendar.current
+                        .startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[item.timeIndex])
+
                     let endDate = state.items
                         .count > index + 1 ?
-                        Date(timeIntervalSinceReferenceDate: state.timeValues[state.items[index + 1].timeIndex])
-                        .addingTimeInterval(TimeInterval(tzOffset)) :
-                        Date(timeIntervalSinceReferenceDate: state.timeValues.last!).addingTimeInterval(30 * 60)
-                        .addingTimeInterval(TimeInterval(tzOffset))
+                        Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[state.items[index + 1].timeIndex])
+                        :
+                        Calendar.current.startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues.last! + 30 * 60)
 
                     LineMark(x: .value("End Date", startDate), y: .value("Target", displayValueFloat ?? 0.0))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.green.gradient)
@@ -206,7 +206,8 @@ extension TargetsEditor {
                 }
             }
             .chartXScale(
-                domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
+                domain: Calendar.current.startOfDay(for: now) ... Calendar
+                    .current.startOfDay(for: now)
                     .addingTimeInterval(60 * 60 * 24)
             )
             .chartYAxis {

+ 16 - 6
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -458,9 +458,14 @@ extension UserInterfaceSettings {
                                                         "Time in Tight Range (TITR):"
                                                     )
                                                     .bold()
-                                                    Text(
-                                                        "Uses the fairly established Time in Tight Range definition, which is defined as time between \(state.units == .mgdL ? Decimal(70) : 70.asMmolL) and \(state.units == .mgdL ? Decimal(140) : 140.asMmolL) \(state.units.rawValue)."
-                                                    )
+                                                    let titrBottomThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(70) : 70.asMmolL)"
+                                                    let titrTopThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(140) : 140.asMmolL)"
+                                                    Text(String(
+                                                        localized: "Uses the fairly established Time in Tight Range definition, which is defined as time between \(titrBottomThreshold) and \(titrTopThreshold)  \(state.units.rawValue).",
+                                                        comment: "Time in Tight Range (TITR) verbose hint description"
+                                                    ))
                                                 }
                                                 VStack(
                                                     alignment: .leading,
@@ -470,9 +475,14 @@ extension UserInterfaceSettings {
                                                         "Time in Normoglycemia (TING):"
                                                     )
                                                     .bold()
-                                                    Text(
-                                                        "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (\(state.units == .mgdL ? Decimal(63) : 63.asMmolL) \(state.units.rawValue)) and \(state.units == .mgdL ? Decimal(140) : 140.asMmolL) \(state.units.rawValue)."
-                                                    )
+                                                    let tingBottomThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(63) : 63.asMmolL)"
+                                                    let tingTopThreshold =
+                                                        "\(state.units == .mgdL ? Decimal(140) : 140.asMmolL)"
+                                                    Text(String(
+                                                        localized: "Uses the very new – first discussed at ATTD 2025 in Amsterdam, NL – Time in Normoglycemia definition, which adopts its range as all values between the normoglycemic minimum threshold (\(tingBottomThreshold) \(state.units.rawValue)) and \(tingTopThreshold) \(state.units.rawValue).",
+                                                        comment: "Time in Normoglycemia (TING) verbose hint description"
+                                                    ))
                                                 }
                                             }
                                         )