Browse Source

Merge pull request #613 from MikePlante1/daily-stats

Add Daily versions of glucose percentile and distribution charts
Deniz Cengiz 11 tháng trước cách đây
mục cha
commit
2dd905fdf8
22 tập tin đã thay đổi với 1775 bổ sung227 xóa
  1. 16 0
      Trio.xcodeproj/project.pbxproj
  2. 78 0
      Trio/Resources/Assets.xcassets/Colors/darkGreen.colorset/Contents.json
  3. 78 0
      Trio/Resources/Assets.xcassets/Colors/darkOrange.colorset/Contents.json
  4. 0 12
      Trio/Resources/InfoPlist.xcstrings
  5. 2 0
      Trio/Sources/Helpers/Color+Extensions.swift
  6. 50 3
      Trio/Sources/Localizations/Main/Localizable.xcstrings
  7. 32 0
      Trio/Sources/Models/BloodGlucose.swift
  8. 350 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/GlucoseStatsSetup.swift
  9. 1 1
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  10. 51 5
      Trio/Sources/Modules/Stat/StatStateModel.swift
  11. 43 9
      Trio/Sources/Modules/Stat/View/StatChartUtils.swift
  12. 80 20
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  13. 253 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyDistributionChart.swift
  14. 421 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDailyPercentileChart.swift
  15. 14 14
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift
  16. 32 32
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift
  17. 71 0
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileDetailView.swift
  18. 189 121
      Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift
  19. 4 4
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift
  20. 2 1
      Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift
  21. 5 3
      Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift
  22. 3 2
      Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

+ 16 - 0
Trio.xcodeproj/project.pbxproj

@@ -444,7 +444,11 @@
 		BDFF7A892D25F97D0016C40C /* TrioWatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A852D25F97D0016C40C /* TrioWatchApp.swift */; };
 		BDFF7A8B2D25F97D0016C40C /* Unit Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF7A8A2D25F97D0016C40C /* Unit Tests.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
+		C21FE1E72DA59C6B007D550B /* GlucoseDailyDistributionChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */; };
+		C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */; };
+		C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */; };
 		C2A0A42F2CE03131003B98E8 /* ConstantValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */; };
+		C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CC6C406E2ACDD69E009B8058 /* RawFetchedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */; };
@@ -1260,7 +1264,11 @@
 		BDFF7A922D25F97D0016C40C /* TrioWatchAppExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioWatchAppExtension.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsProvider.swift; sourceTree = "<group>"; };
+		C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyDistributionChart.swift; sourceTree = "<group>"; };
+		C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucosePercentileDetailView.swift; sourceTree = "<group>"; };
+		C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDailyPercentileChart.swift; sourceTree = "<group>"; };
 		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
+		C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStatsSetup.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
@@ -2805,6 +2813,7 @@
 		BD249D952D42FCA800412DEB /* StatStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				C2A6D1E32DB1581D0036DB66 /* GlucoseStatsSetup.swift */,
 				BD249DA02D42FD1000412DEB /* TDDSetup.swift */,
 				BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */,
 				BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */,
@@ -3470,6 +3479,9 @@
 		DDCAE97A2D79F99B00B1BB51 /* Glucose */ = {
 			isa = PBXGroup;
 			children = (
+				C21FE1E62DA59C6B007D550B /* GlucoseDailyDistributionChart.swift */,
+				C29E26892DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift */,
+				C28DD7252DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift */,
 				BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */,
 				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
 				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
@@ -4086,6 +4098,7 @@
 				DD3F1F8D2D9E0E0600DCE7B3 /* NightscoutSetupStepView.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
+				C28DD7262DBA9A9E00EC02DD /* GlucosePercentileDetailView.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
 				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
@@ -4170,6 +4183,7 @@
 				DD1745322C55AE6000211FAC /* TargetBehavoirStateModel.swift in Sources */,
 				38E44535274E411700EC9A94 /* Disk+Data.swift in Sources */,
 				BD8E6B232D9036F700ABF8FA /* OnboardingDataFlow.swift in Sources */,
+				C2A6D1E42DB1581D0036DB66 /* GlucoseStatsSetup.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
@@ -4199,6 +4213,7 @@
 				BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
+				C29E268A2DADFD2A00F87E75 /* GlucoseDailyPercentileChart.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
 				65070A332BFDCB83006F213F /* TidepoolStartView.swift in Sources */,
 				190EBCC629FF138000BA767D /* UserInterfaceSettingsProvider.swift in Sources */,
@@ -4243,6 +4258,7 @@
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
 				DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */,
 				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
+				C21FE1E72DA59C6B007D550B /* GlucoseDailyDistributionChart.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,

+ 78 - 0
Trio/Resources/Assets.xcassets/Colors/darkGreen.colorset/Contents.json

@@ -0,0 +1,78 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x47",
+          "green" : "0x9F",
+          "red" : "0x2A"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x46",
+          "green" : "0xA7",
+          "red" : "0x26"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x30",
+          "green" : "0x6E",
+          "red" : "0x1D"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        },
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x49",
+          "green" : "0xAF",
+          "red" : "0x26"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 78 - 0
Trio/Resources/Assets.xcassets/Colors/darkOrange.colorset/Contents.json

@@ -0,0 +1,78 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x00",
+          "green" : "0x77",
+          "red" : "0xCC"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x08",
+          "green" : "0x7F",
+          "red" : "0xCC"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x00",
+          "green" : "0x2A",
+          "red" : "0xA1"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        },
+        {
+          "appearance" : "contrast",
+          "value" : "high"
+        }
+      ],
+      "color" : {
+        "color-space" : "extended-srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x33",
+          "green" : "0x8F",
+          "red" : "0xCC"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 0 - 12
Trio/Resources/InfoPlist.xcstrings

@@ -457,18 +457,6 @@
         }
       }
     },
-    "NSCalendarsFullAccessUsageDescription" : {
-      "comment" : "Privacy - Calendars Full Access Usage Description",
-      "extractionState" : "extracted_with_value",
-      "localizations" : {
-        "en" : {
-          "stringUnit" : {
-            "state" : "new",
-            "value" : "To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay"
-          }
-        }
-      }
-    },
     "NSCalendarsUsageDescription" : {
       "comment" : "Privacy - Calendars Usage Description",
       "extractionState" : "extracted_with_value",

+ 2 - 0
Trio/Sources/Helpers/Color+Extensions.swift

@@ -72,4 +72,6 @@ extension Color {
     static let lemon = Color("Lemon")
     static let minus = Color("minus")
     static let darkGray = Color("darkGray")
+    static let darkGreen = Color("darkGreen")
+    static let darkOrange = Color("darkOrange")
 }

+ 50 - 3
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -8832,9 +8832,6 @@
         }
       }
     },
-    "%lld h" : {
-
-    },
     "%lld hr" : {
       "localizations" : {
         "bg" : {
@@ -44162,6 +44159,9 @@
         }
       }
     },
+    "Avg" : {
+
+    },
     "Axis" : {
       "localizations" : {
         "bg" : {
@@ -56277,6 +56277,7 @@
       }
     },
     "CGM Connection Trace Chart" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -67836,6 +67837,9 @@
         }
       }
     },
+    "DayEnd" : {
+
+    },
     "days" : {
       "comment" : "Total number of days of data for HbA1c estimation, part 2/2",
       "extractionState" : "manual",
@@ -68057,6 +68061,9 @@
         }
       }
     },
+    "DayStart" : {
+
+    },
     "Deactivate Pod" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -82356,6 +82363,9 @@
         }
       }
     },
+    "Distribution (by day)" : {
+
+    },
     "Do not enable this feature until you have optimized your CR (carb ratio) setting." : {
       "localizations" : {
         "bg" : {
@@ -139729,6 +139739,7 @@
       }
     },
     "Meal to Hypoglycemia/Hyperglycemia Distribution Chart" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -139941,6 +139952,9 @@
         }
       }
     },
+    "Med" : {
+
+    },
     "Median" : {
       "comment" : "Median BG",
       "localizations" : {
@@ -140913,6 +140927,9 @@
         }
       }
     },
+    "Mid Limit" : {
+
+    },
     "Middleware" : {
       "extractionState" : "manual",
       "localizations" : {
@@ -146767,6 +146784,9 @@
         }
       }
     },
+    "No glucose data available for this day" : {
+
+    },
     "No Glucose Notifications will be triggered." : {
       "localizations" : {
         "bg" : {
@@ -146873,6 +146893,9 @@
         }
       }
     },
+    "No glucose readings found." : {
+
+    },
     "No glucose readings." : {
       "localizations" : {
         "bg" : {
@@ -161486,6 +161509,9 @@
         }
       }
     },
+    "Percentile (by day)" : {
+
+    },
     "Period:" : {
       "localizations" : {
         "bg" : {
@@ -178954,6 +178980,12 @@
         }
       }
     },
+    "SelectedDate" : {
+
+    },
+    "SelectedValue" : {
+
+    },
     "Selection" : {
       "localizations" : {
         "bg" : {
@@ -194723,6 +194755,7 @@
       }
     },
     "Successful Loop" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -194828,6 +194861,9 @@
         }
       }
     },
+    "Successful Loops" : {
+
+    },
     "Suggested at" : {
       "comment" : "Headline in suggested pop up (at: at what time)",
       "extractionState" : "manual",
@@ -197183,6 +197219,12 @@
         }
       }
     },
+    "Tap a percentile or tap and hold a bar to reveal more details. Swipe to scroll through time." : {
+
+    },
+    "Tap and hold a bar in the chart to reveal more details. Swipe to scroll through time." : {
+
+    },
     "Tap and hold a bar to reveal more details." : {
       "localizations" : {
         "bg" : {
@@ -222313,6 +222355,7 @@
       }
     },
     "Trio Up-Time Chart" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -230413,7 +230456,11 @@
         }
       }
     },
+    "Very Low (<%@" : {
+
+    },
     "Very Low (<%@)" : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {

+ 32 - 0
Trio/Sources/Models/BloodGlucose.swift

@@ -176,9 +176,21 @@ extension Int {
     var formattedAsMmolL: String {
         NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
+
+    func formatted(for units: GlucoseUnits) -> String {
+        units == .mgdL ? description : formattedAsMmolL
+    }
+
+    func formatted(withUnits units: GlucoseUnits) -> String {
+        formatted(for: units) + " \(units.rawValue)"
+    }
 }
 
 extension Decimal {
+    func asUnit(_ unit: GlucoseUnits) -> Decimal {
+        unit == .mgdL ? self : asMmolL
+    }
+
     var asMmolL: Decimal {
         Trio.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
@@ -190,9 +202,21 @@ extension Decimal {
     var formattedAsMmolL: String {
         NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
+
+    func formatted(for units: GlucoseUnits) -> String {
+        units == .mgdL ? description : formattedAsMmolL
+    }
+
+    func formatted(withUnits units: GlucoseUnits) -> String {
+        formatted(for: units) + " \(units.rawValue)"
+    }
 }
 
 extension Double {
+    func asUnit(_ units: GlucoseUnits) -> Double {
+        units == .mgdL ? self : Double(truncating: asMmolL as NSNumber)
+    }
+
     var asMmolL: Decimal {
         Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
     }
@@ -204,6 +228,14 @@ extension Double {
     var formattedAsMmolL: String {
         NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
     }
+
+    func formatted(for units: GlucoseUnits) -> String {
+        units == .mgdL ? description : formattedAsMmolL
+    }
+
+    func formatted(withUnits units: GlucoseUnits) -> String {
+        formatted(for: units) + " \(units.rawValue)"
+    }
 }
 
 extension NumberFormatter {

+ 350 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/GlucoseStatsSetup.swift

@@ -0,0 +1,350 @@
+import CoreData
+import Foundation
+
+/// A thread-safe value type to hold glucose data without Core Data dependencies
+struct GlucoseReading: Sendable {
+    let value: Int
+    let date: Date
+}
+
+/// Represents statistical data for daily glucose metrics by distribution ranges
+struct GlucoseDailyDistributionStats: Identifiable {
+    let id = UUID()
+    /// The date this data represents
+    let date: Date
+    /// The time-in-range type used for calculations
+    let timeInRangeType: TimeInRangeType
+    /// The original glucose readings
+    let readings: [GlucoseStored]
+    /// Percentage of glucose readings below 54 mg/dL
+    let veryLowPct: Double
+    /// Percentage of glucose readings in the [54 – lowLimit] mg/dL range
+    let lowPct: Double
+    /// Percentage of glucose readings within the tighter control range of [bottomThreshold – topThreshold] mg/dL
+    let inSmallRangePct: Double
+    /// Percentage of glucose readings within the target range of [bottomThreshold – highLimit] mg/dL
+    let inRangePct: Double
+    /// Percentage of glucose readings in the (highLimit – 250] mg/dL range
+    let highPct: Double
+    /// Percentage of glucose readings above 250 mg/dL
+    let veryHighPct: Double
+
+    init(
+        date: Date,
+        timeInRangeType: TimeInRangeType,
+        readings: [GlucoseStored] = [GlucoseStored](),
+        veryLowPct: Double = 0,
+        lowPct: Double = 0,
+        inSmallRangePct: Double = 0,
+        inRangePct: Double = 0,
+        highPct: Double = 0,
+        veryHighPct: Double = 0
+    ) {
+        self.date = date
+        self.timeInRangeType = timeInRangeType
+        self.readings = readings
+        self.veryLowPct = veryLowPct
+        self.lowPct = lowPct
+        self.inSmallRangePct = inSmallRangePct
+        self.inRangePct = inRangePct
+        self.highPct = highPct
+        self.veryHighPct = veryHighPct
+    }
+}
+
+/// Represents percentile-based statistical data for daily glucose metrics
+struct GlucoseDailyPercentileStats: Identifiable {
+    let id = UUID()
+    /// The date this data represents
+    let date: Date
+    /// The original glucose readings
+    let readings: [GlucoseStored]
+    /// Minimum glucose value
+    let minimum: Double
+    /// 10th percentile glucose value
+    let percentile10: Double
+    /// 25th percentile glucose value (lower quartile)
+    let percentile25: Double
+    /// Median (50th percentile) glucose value
+    let median: Double
+    /// 75th percentile glucose value (upper quartile)
+    let percentile75: Double
+    /// 90th percentile glucose value
+    let percentile90: Double
+    /// Maximum glucose value
+    let maximum: Double
+
+    init(
+        date: Date,
+        readings: [GlucoseStored] = [GlucoseStored](),
+        minimum: Double = 0,
+        percentile10: Double = 0,
+        percentile25: Double = 0,
+        median: Double = 0,
+        percentile75: Double = 0,
+        percentile90: Double = 0,
+        maximum: Double = 0
+    ) {
+        self.date = date
+        self.readings = readings
+        self.minimum = minimum
+        self.percentile10 = percentile10
+        self.percentile25 = percentile25
+        self.median = median
+        self.percentile75 = percentile75
+        self.percentile90 = percentile90
+        self.maximum = maximum
+    }
+}
+
+extension Stat.StateModel {
+    /// Performs setup for both percentile and distribution glucose statistics from provided IDs
+    ///
+    /// This method optimizes performance by:
+    /// 1. Computing both percentile and distribution statistics concurrently
+    /// 2. Creating lookup caches for both stat types simultaneously
+    ///
+    /// - Parameter ids: Array of NSManagedObjectIDs for glucose readings
+    func setupGlucoseStats(with ids: [NSManagedObjectID]) async {
+        // Get dates for the past 90 days
+        let dates = getDates()
+
+        // Calculate both types of statistics concurrently
+        async let percentileStats = calculateDailyPercentileStats(
+            for: dates,
+            glucoseIDs: ids
+        )
+
+        async let distributionStats = calculateDailyDistributionStats(
+            for: dates,
+            glucoseIDs: ids,
+            highLimit: highLimit,
+            timeInRangeType: timeInRangeType
+        )
+
+        let (pStats, dStats) = await (percentileStats, distributionStats)
+
+        dailyGlucosePercentileStats = pStats
+        glucosePercentileCache = Dictionary(
+            uniqueKeysWithValues: pStats.map {
+                (Calendar.current.startOfDay(for: $0.date), $0)
+            }
+        )
+
+        dailyGlucoseDistributionStats = dStats
+        glucoseDistributionCache = Dictionary(
+            uniqueKeysWithValues: dStats.map {
+                (Calendar.current.startOfDay(for: $0.date), $0)
+            }
+        )
+    }
+
+    /// Generates an array of dates for the specified number of days
+    /// - Parameter daysCount: Number of days to generate
+    /// - Returns: Array of dates starting from (today - daysCount) to today
+    func getDates() -> [Date] {
+        let calendar = Calendar.current
+        let today = calendar.startOfDay(for: Date())
+
+        return (0 ..< 90).map { dayOffset -> Date in
+            calendar.startOfDay(for: calendar.date(byAdding: .day, value: -(89 - dayOffset), to: today)!)
+        }
+    }
+
+    /// Processes glucose readings for a set of dates in a thread-safe manner
+    /// - Parameters:
+    ///   - dates: Array of dates to process data for
+    ///   - glucoseIDs: Array of NSManagedObjectIDs for glucose readings
+    /// - Returns: Array of (date, readings) tuples containing filtered readings for each date
+    private func processGlucoseReadingsForDates(
+        _ dates: [Date],
+        glucoseIDs: [NSManagedObjectID]
+    ) async -> [(date: Date, readings: [GlucoseReading])] {
+        let calendar = Calendar.current
+
+        // Handle cancellation early
+        if Task.isCancelled {
+            return []
+        }
+
+        // Extract the thread-safe glucose readings
+        let privateContext = CoreDataStack.shared.newTaskContext()
+
+        // Map into Sendable struct
+        let glucoseReadings: [GlucoseReading] = await privateContext.perform {
+            // Get NSManagedObject on private context and map into GlucoseReading struct
+            glucoseIDs.compactMap { id -> GlucoseReading? in
+                guard let reading = privateContext.object(with: id) as? GlucoseStored,
+                      let date = reading.date else { return nil }
+                return GlucoseReading(value: Int(reading.glucose), date: date)
+            }
+        }
+
+        return await withTaskGroup(of: (date: Date, readings: [GlucoseReading]).self) { group in
+            for date in dates {
+                group.addTask {
+                    let dayStart = calendar.startOfDay(for: date)
+                    let dayEnd = calendar.isDateInToday(date) ?
+                        Date.now :
+                        calendar.date(byAdding: .day, value: 1, to: dayStart)!
+
+                    let filteredReadings = glucoseReadings.filter {
+                        $0.date >= dayStart && $0.date < dayEnd
+                    }
+                    return (date: date, readings: filteredReadings)
+                }
+            }
+
+            // Collect results
+            var results: [(date: Date, readings: [GlucoseReading])] = []
+            for await result in group {
+                results.append(result)
+            }
+            return results.sorted { $0.date < $1.date }
+        }
+    }
+
+    /// Creates a GlucoseDailyDistributionStats object from thread-safe reading values
+    /// - Parameters:
+    ///   - date: Date for the day
+    ///   - readings: Array of thread-safe glucose readings
+    ///   - highLimit: Upper limit for target glucose range
+    ///   - timeInRangeType: The time-in-range type to use for calculations
+    /// - Returns: GlucoseDailyDistributionStats object with calculated statistics
+    private func createGlucoseDailyDistributionStatsFromReadings(
+        date: Date,
+        readings: [GlucoseReading],
+        highLimit: Decimal,
+        timeInRangeType: TimeInRangeType
+    ) -> GlucoseDailyDistributionStats {
+        let totalReadings = Double(readings.count)
+
+        // Count readings in each range
+        let veryHighReadings = readings.filter { $0.value > 250 }.count
+        let highReadings = readings.filter { $0.value > Int(highLimit) && $0.value <= 250 }.count
+        let inRangeReadings = readings.filter { $0.value >= timeInRangeType.bottomThreshold && $0.value <= Int(highLimit) }
+            .count
+        let inSmallRangeReadings = readings
+            .filter { $0.value >= timeInRangeType.bottomThreshold && $0.value <= timeInRangeType.topThreshold }.count
+        let lowReadings = readings.filter { $0.value < timeInRangeType.bottomThreshold && $0.value >= 54 }.count
+        let veryLowReadings = readings.filter { $0.value < 54 }.count
+
+        // Calculate percentages
+        let veryLowPct = totalReadings > 0 ? Double(veryLowReadings) / totalReadings * 100 : 0
+        let lowPct = totalReadings > 0 ? Double(lowReadings) / totalReadings * 100 : 0
+        let inSmallRangePct = totalReadings > 0 ? Double(inSmallRangeReadings) / totalReadings * 100 : 0
+        let inRangePct = totalReadings > 0 ? Double(inRangeReadings) / totalReadings * 100 : 0
+        let highPct = totalReadings > 0 ? Double(highReadings) / totalReadings * 100 : 0
+        let veryHighPct = totalReadings > 0 ? Double(veryHighReadings) / totalReadings * 100 : 0
+
+        // Create empty managed object array since we don't need the actual Core Data objects
+        let emptyStoredArray: [GlucoseStored] = []
+
+        return GlucoseDailyDistributionStats(
+            date: date,
+            timeInRangeType: timeInRangeType,
+            readings: emptyStoredArray,
+            veryLowPct: veryLowPct,
+            lowPct: lowPct,
+            inSmallRangePct: inSmallRangePct,
+            inRangePct: inRangePct,
+            highPct: highPct,
+            veryHighPct: veryHighPct
+        )
+    }
+
+    /// Creates a GlucoseDailyPercentileStats object from thread-safe reading values
+    /// - Parameters:
+    ///   - date: Date for the day
+    ///   - readings: Array of thread-safe glucose readings
+    /// - Returns: GlucoseDailyPercentileStats object with calculated statistics
+    private func createGlucoseDailyPercentileStatsFromReadings(
+        date: Date,
+        readings: [GlucoseReading]
+    ) -> GlucoseDailyPercentileStats {
+        let glucoseValues = readings.map { Double($0.value) }.sorted()
+
+        // If no data, return empty data
+        guard !glucoseValues.isEmpty else {
+            return GlucoseDailyPercentileStats(date: date)
+        }
+
+        let count = glucoseValues.count
+
+        let calculatePercentile = { (p: Double) -> Double in
+            let position = Double(count - 1) * p
+            let lower = Int(floor(position))
+            let upper = Int(ceil(position))
+
+            if lower == upper {
+                return glucoseValues[lower]
+            }
+
+            let weight = position - Double(lower)
+            return glucoseValues[lower] * (1 - weight) + glucoseValues[upper] * weight
+        }
+
+        // Calculate all percentiles concurrently
+        return GlucoseDailyPercentileStats(
+            date: date,
+            readings: [],
+            minimum: glucoseValues.first ?? 0,
+            percentile10: calculatePercentile(0.10),
+            percentile25: calculatePercentile(0.25),
+            median: calculatePercentile(0.5),
+            percentile75: calculatePercentile(0.75),
+            percentile90: calculatePercentile(0.90),
+            maximum: glucoseValues.last ?? 0
+        )
+    }
+
+    func calculateDailyDistributionStats(
+        for dates: [Date],
+        glucoseIDs: [NSManagedObjectID],
+        highLimit: Decimal,
+        timeInRangeType: TimeInRangeType
+    ) async -> [GlucoseDailyDistributionStats] {
+        // Process readings for each date
+        let processedData = await processGlucoseReadingsForDates(
+            dates,
+            glucoseIDs: glucoseIDs
+        )
+
+        // Transform into distribution stats
+        return processedData.map { date, readings in
+            if readings.isEmpty {
+                return GlucoseDailyDistributionStats(date: date, timeInRangeType: timeInRangeType)
+            } else {
+                return createGlucoseDailyDistributionStatsFromReadings(
+                    date: date,
+                    readings: readings,
+                    highLimit: highLimit,
+                    timeInRangeType: timeInRangeType
+                )
+            }
+        }
+    }
+
+    func calculateDailyPercentileStats(
+        for dates: [Date],
+        glucoseIDs: [NSManagedObjectID]
+    ) async -> [GlucoseDailyPercentileStats] {
+        // Process readings for each date
+        let processedData = await processGlucoseReadingsForDates(
+            dates,
+            glucoseIDs: glucoseIDs
+        )
+
+        // Transform into percentile stats
+        return processedData.map { date, readings in
+            if readings.isEmpty {
+                return GlucoseDailyPercentileStats(date: date)
+            } else {
+                return createGlucoseDailyPercentileStatsFromReadings(
+                    date: date,
+                    readings: readings
+                )
+            }
+        }
+    }
+}

+ 1 - 1
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -39,7 +39,7 @@ enum LoopStatsDataType: String {
 
     var displayName: String {
         switch self {
-        case .successfulLoop: return String(localized: "Successful Loop")
+        case .successfulLoop: return String(localized: "Successful Loops")
         case .glucoseCount: return String(localized: "Glucose Count")
         }
     }

+ 51 - 5
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -37,6 +37,13 @@ extension Stat {
         var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
         var bolusTotalsCache: [(Date, total: Double)] = []
 
+        // Cache for Glucose Daily Stats
+        var dailyGlucosePercentileStats: [GlucoseDailyPercentileStats] = []
+        var glucosePercentileCache: [Date: GlucoseDailyPercentileStats] = [:]
+        var dailyGlucoseDistributionStats: [GlucoseDailyDistributionStats] = []
+        var glucoseDistributionCache: [Date: GlucoseDailyDistributionStats] = [:]
+        var glucoseReadings: [GlucoseStored] = []
+
         // Selected Duration for Glucose Stats
         var selectedIntervalForGlucoseStats: StatsTimeIntervalWithToday = .today {
             didSet {
@@ -58,7 +65,7 @@ extension Stat {
         }
 
         // Selected Glucose Chart Type
-        var selectedGlucoseChartType: GlucoseChartType = .percentile
+        var selectedGlucoseChartType: GlucoseChartType = .percentileByTime
 
         // Selected Insulin Chart Type
         var selectedInsulinChartType: InsulinChartType = .totalDailyDose
@@ -83,6 +90,7 @@ extension Stat {
             setupBolusStats()
             setupLoopStatRecords()
             setupMealStats()
+            setupGlucoseDailyStats()
             units = settingsManager.settings.units
             eA1cDisplayUnit = settingsManager.settings.eA1cDisplayUnit
             useFPUconversion = settingsManager.settings.useFPUconversion
@@ -91,9 +99,16 @@ extension Stat {
 
         func setupGlucoseArray(for interval: StatsTimeIntervalWithToday) {
             Task {
+                // Load data for current interval (existing code)
                 let ids = await fetchGlucose(for: interval)
                 await updateGlucoseArray(with: ids)
 
+                // Also ensure we have the full dataset loaded
+                if glucoseReadings.isEmpty {
+                    let allIds = await fetchGlucose(for: .total)
+                    await updateAllGlucoseArray(with: allIds)
+                }
+
                 // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
                 async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
                 async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
@@ -101,6 +116,16 @@ extension Stat {
             }
         }
 
+        func setupGlucoseDailyStats() {
+            Task {
+                // Get glucose IDs once (using the private fetchGlucose method)
+                let allIds = await fetchGlucose(for: .total)
+
+                // Pass the IDs to the implementation in GlucoseStatsSetup.swift
+                await setupGlucoseStats(with: allIds)
+            }
+        }
+
         private func fetchGlucose(for interval: StatsTimeIntervalWithToday) async -> [NSManagedObjectID] {
             do {
                 let predicate: NSPredicate
@@ -152,6 +177,19 @@ extension Stat {
                 )
             }
         }
+
+        @MainActor private func updateAllGlucoseArray(with IDs: [NSManagedObjectID]) {
+            do {
+                let glucoseObjects = try IDs.compactMap { id in
+                    try viewContext.existingObject(with: id) as? GlucoseStored
+                }
+                glucoseReadings = glucoseObjects
+            } catch {
+                debugPrint(
+                    "Home State: \(#function) \(DebuggingIdentifiers.failed) error while updating the all glucose array: \(error.localizedDescription)"
+                )
+            }
+        }
     }
 
     @Observable final class UpdateTimer {
@@ -179,16 +217,24 @@ extension Stat.StateModel {
     /// Defines the available types of glucose charts
     enum GlucoseChartType: String, CaseIterable {
         /// Ambulatory Glucose Profile showing percentile ranges
-        case percentile = "Percentile"
+        case percentileByTime = "Percentile"
         /// Time-based distribution of glucose ranges
-        case distribution = "Distribution"
+        case distributionByTime = "Distribution"
+        /// Day-based box plot of glucose percentile ranges
+        case percentileByDay = "Percentile (by day)"
+        /// Day-based distribution of glucose ranges
+        case distributionByDay = "Distribution (by day)"
 
         var displayName: String {
             switch self {
-            case .percentile:
+            case .percentileByTime:
                 return String(localized: "Percentile")
-            case .distribution:
+            case .distributionByTime:
                 return String(localized: "Distribution")
+            case .percentileByDay:
+                return String(localized: "Percentile (by day)")
+            case .distributionByDay:
+                return String(localized: "Distribution (by day)")
             }
         }
     }

+ 43 - 9
Trio/Sources/Modules/Stat/View/StatChartUtils.swift

@@ -24,8 +24,29 @@ struct StatChartUtils {
         from scrollPosition: Date,
         for selectedInterval: Stat.StateModel.StatsTimeInterval
     ) -> (start: Date, end: Date) {
-        let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedInterval))
-        return (scrollPosition, end)
+        let calendar = Calendar.current
+
+        if selectedInterval == .day {
+            // For day view, don't modify the scroll position
+            let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedInterval) - 1)
+            return (scrollPosition, end)
+        } else {
+            // For week and longer intervals, we need smart alignment
+            // Find the nearest day boundary
+            let startOfDay = calendar.startOfDay(for: scrollPosition)
+            let components = calendar.dateComponents([.hour, .minute, .second], from: scrollPosition)
+            let totalSeconds = Double(components.hour ?? 0) * 3600 + Double(components.minute ?? 0) * 60 +
+                Double(components.second ?? 0)
+
+            // Align start end to midnight
+            let alignedStart = totalSeconds > 12 * 3600 ?
+                calendar.date(byAdding: .day, value: 1, to: startOfDay)! : startOfDay
+            let intervalLength = visibleDomainLength(for: selectedInterval)
+            let end = alignedStart.addingTimeInterval(intervalLength + (2 * 3600))
+            let alignedEnd = calendar.startOfDay(for: end).addingTimeInterval(-1)
+
+            return (alignedStart, alignedEnd)
+        }
     }
 
     /// Returns the appropriate date format style based on the selected time interval.
@@ -46,7 +67,9 @@ struct StatChartUtils {
     static func alignmentComponents(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> DateComponents {
         switch selectedInterval {
         case .day: return DateComponents(hour: 0)
-        case .week: return DateComponents(weekday: 2)
+        case .week:
+            let calendar = Calendar.current
+            return DateComponents(weekday: calendar.firstWeekday)
         case .month,
              .total: return DateComponents(day: 1)
         }
@@ -58,14 +81,21 @@ struct StatChartUtils {
     static func getInitialScrollPosition(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Date {
         let calendar = Calendar.current
         let now = Date()
+        let today = calendar.startOfDay(for: now)
 
+        let baseDate: Date
         switch selectedInterval {
-//        case .day: return calendar.date(byAdding: .day, value: -1, to: now)!
-        case .day: return calendar.startOfDay(for: now)
-        case .week: return calendar.date(byAdding: .day, value: -7, to: now)!
-        case .month: return calendar.date(byAdding: .month, value: -1, to: now)!
-        case .total: return calendar.date(byAdding: .month, value: -3, to: now)!
+        case .day:
+            baseDate = today
+        case .week:
+            baseDate = calendar.date(byAdding: .day, value: -6, to: today)!
+        case .month:
+            baseDate = calendar.date(byAdding: .day, value: -29, to: today)!
+        case .total:
+            baseDate = calendar.date(byAdding: .day, value: -89, to: today)!
         }
+
+        return calendar.date(byAdding: .second, value: 1, to: baseDate)!
     }
 
     /// Checks if two dates belong to the same time unit based on the selected duration.
@@ -74,7 +104,11 @@ struct StatChartUtils {
     ///   - date2: The second date.
     ///   - selectedInterval: The selected time interval for statistics.
     /// - Returns: A Boolean indicating whether the two dates are in the same time unit.
-    static func isSameTimeUnit(_ date1: Date, _ date2: Date, for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Bool {
+    static func isSameTimeUnit(
+        _ date1: Date,
+        _ date2: Date,
+        for selectedInterval: Stat.StateModel.StatsTimeInterval
+    ) -> Bool {
         let calendar = Calendar.current
         switch selectedInterval {
         case .day:

+ 80 - 20
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -17,6 +17,12 @@ extension Stat {
 
         @State var state = StateModel()
         @State private var selectedView: StateModel.StatisticViewType = .glucose
+        @State private var isGlucoseDaySelected: Bool = false
+
+        private var intervalOptions: [Stat.StateModel.StatsTimeIntervalWithToday] {
+            state.selectedGlucoseChartType == .percentileByDay || state.selectedGlucoseChartType == .distributionByDay
+                ? [.week, .month, .total] : Stat.StateModel.StatsTimeIntervalWithToday.allCases
+        }
 
         var body: some View {
             VStack {
@@ -73,10 +79,18 @@ extension Stat {
                     }
                 }
                 .pickerStyle(.menu)
+                .onChange(of: state.selectedGlucoseChartType) { _, newValue in
+                    // If switching to daily chart and day/today is selected, switch to week
+                    if newValue == .percentileByDay || newValue == .distributionByDay,
+                       state.selectedIntervalForGlucoseStats == .day || state.selectedIntervalForGlucoseStats == .today
+                    {
+                        state.selectedIntervalForGlucoseStats = .week
+                    }
+                }
             }.padding(.horizontal)
 
             Picker("Duration", selection: $state.selectedIntervalForGlucoseStats) {
-                ForEach(StateModel.StatsTimeIntervalWithToday.allCases, id: \.self) { timeInterval in
+                ForEach(intervalOptions, id: \.self) { timeInterval in
                     Text(timeInterval.displayName)
                 }
             }
@@ -90,15 +104,28 @@ extension Stat {
                 )
             } else {
                 timeInRangeCard
-                glucoseStatsCard
+
+                if !isGlucoseDaySelected && state.selectedGlucoseChartType != .percentileByDay && state
+                    .selectedGlucoseChartType != .distributionByDay
+                {
+                    glucoseStatsCard
+                }
 
                 HStack {
                     var hintText: String {
                         switch state.selectedGlucoseChartType {
-                        case .percentile:
+                        case .percentileByTime:
                             String(localized: "Tap and hold the AGP graph or Time-in-Range ring to reveal more details.")
-                        case .distribution:
+                        case .distributionByTime:
                             String(localized: "Tap and hold the Time-in-Range ring to reveal more details.")
+                        case .percentileByDay:
+                            String(
+                                localized: "Tap a percentile or tap and hold a bar to reveal more details. Swipe to scroll through time."
+                            )
+                        case .distributionByDay:
+                            String(
+                                localized: "Tap and hold a bar in the chart to reveal more details. Swipe to scroll through time."
+                            )
                         }
                     }
                     Image(systemName: "hand.draw.fill")
@@ -115,18 +142,56 @@ extension Stat {
             StatCard {
                 VStack(spacing: Constants.spacing) {
                     switch state.selectedGlucoseChartType {
-                    case .percentile:
+                    case .distributionByDay,
+                         .percentileByDay:
+                        let interval: Stat.StateModel.StatsTimeInterval = {
+                            switch state.selectedIntervalForGlucoseStats {
+                            case .month,
+                                 .total:
+                                return Stat.StateModel.StatsTimeInterval(
+                                    rawValue: state.selectedIntervalForGlucoseStats.rawValue
+                                )!
+                            default:
+                                return .week
+                            }
+                        }()
+
+                        if state.selectedGlucoseChartType == .percentileByDay {
+                            GlucoseDailyPercentileChart(
+                                glucose: state.glucoseFromPersistence,
+                                highLimit: state.highLimit,
+                                units: state.units,
+                                timeInRangeType: state.timeInRangeType,
+                                selectedInterval: interval,
+                                isDaySelected: $isGlucoseDaySelected,
+                                state: state
+                            )
+                        } else { // if state.selectedGlucoseChartType == .distributionByDay
+                            GlucoseDailyDistributionChart(
+                                glucose: state.glucoseReadings,
+                                highLimit: state.highLimit,
+                                units: state.units,
+                                timeInRangeType: state.timeInRangeType,
+                                selectedInterval: interval,
+                                eA1cDisplayUnit: state.eA1cDisplayUnit,
+                                isDaySelected: $isGlucoseDaySelected,
+                                state: state
+                            )
+                        }
+
+                    case .percentileByTime:
                         GlucosePercentileChart(
                             glucose: state.glucoseFromPersistence,
                             highLimit: state.highLimit,
-                            lowLimit: state.lowLimit,
+                            timeInRangeType: state.timeInRangeType,
                             units: state.units,
                             hourlyStats: state.hourlyStats,
                             isToday: state.selectedIntervalForGlucoseStats == .today
                         )
-                    case .distribution:
+
+                    case .distributionByTime:
                         GlucoseDistributionChart(
-                            glucose: state.glucoseFromPersistence,
+                            glucose: state.glucoseReadings,
                             highLimit: state.highLimit,
                             lowLimit: state.lowLimit,
                             units: state.units,
@@ -145,7 +210,8 @@ extension Stat {
                         highLimit: state.highLimit,
                         units: state.units,
                         glucose: state.glucoseFromPersistence,
-                        timeInRangeType: state.timeInRangeType
+                        timeInRangeType: state.timeInRangeType,
+                        showChart: true
                     )
 
                     Divider()
@@ -263,19 +329,13 @@ extension Stat {
                         loopingChartView
                         loopStats
                     }
-                case .trioUpTime:
-                    // TODO: Trio Up-Time Chart
-                    ContentUnavailableView(
-                        String(localized: "Coming soon."),
-                        systemImage: "hourglass",
-                        description: Text("Trio Up-Time Chart")
-                    )
-                case .cgmConnectionTrace:
-                    // TODO: CGM Connection Trace Chart
+                case .cgmConnectionTrace,
+                     .trioUpTime:
+                    // TODO: Trio Up-Time Chart & CGM Connection Trace Chart
                     ContentUnavailableView(
                         String(localized: "Coming soon."),
                         systemImage: "hourglass",
-                        description: Text("CGM Connection Trace Chart")
+                        description: Text(state.selectedLoopingChartType.displayName)
                     )
                 }
             }
@@ -346,7 +406,7 @@ extension Stat {
                     ContentUnavailableView(
                         String(localized: "Coming soon."),
                         systemImage: "hourglass",
-                        description: Text("Meal to Hypoglycemia/Hyperglycemia Distribution Chart")
+                        description: Text(state.selectedMealChartType.displayName)
                     )
                 }
             }

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

@@ -0,0 +1,253 @@
+import Charts
+import SwiftUI
+
+struct GlucoseDailyDistributionChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let units: GlucoseUnits
+    let timeInRangeType: TimeInRangeType
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+    let eA1cDisplayUnit: EstimatedA1cDisplayUnit
+
+    @Binding var isDaySelected: Bool
+
+    // Scrolling and selection states
+    @State private var scrollPosition = Date()
+    @State private var selectedDate: Date?
+    @State private var updateTimer = Stat.UpdateTimer()
+    @State private var visibleGlucose: [GlucoseStored] = []
+
+    // State model for accessing the shared data
+    let state: Stat.StateModel
+
+    // Computes the visible date range based on the current scroll position
+    @State private var visibleDateRange: (start: Date, end: Date) = (Date(), Date())
+
+    // Gets daily distribution stats for the visible date range
+    private var visibleDailyStats: [GlucoseDailyDistributionStats] {
+        let calendar = Calendar.current
+        return state.dailyGlucoseDistributionStats.filter { stat in
+            let statDate = calendar.startOfDay(for: stat.date)
+            return statDate >= calendar.startOfDay(for: visibleDateRange.start) &&
+                statDate <= calendar.startOfDay(for: visibleDateRange.end)
+        }
+    }
+
+    private func calculateVisibleDateRange() {
+        visibleDateRange = StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    // Gets selected day stats
+    private var selectedDateStats: GlucoseDailyDistributionStats? {
+        guard let selectedDate = selectedDate else { return nil }
+        let calendar = Calendar.current
+        let startOfSelectedDate = calendar.startOfDay(for: selectedDate)
+        return state.glucoseDistributionCache[startOfSelectedDate]
+    }
+
+    private func calculateVisibleGlucose() {
+        let calendar = Calendar.current
+        visibleGlucose = glucose.filter { reading in
+            guard let date = reading.date else { return false }
+            return date >= calendar.startOfDay(for: visibleDateRange.start) &&
+                date <= calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: visibleDateRange.end))!
+        }
+    }
+
+    // Compute selected day glucose readings
+    private var selectedDateGlucose: [GlucoseStored] {
+        guard let selectedDate = selectedDate else { return [] }
+        let calendar = Calendar.current
+        let dayStart = calendar.startOfDay(for: selectedDate)
+        let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart
+
+        return glucose.filter { reading in
+            guard let date = reading.date else { return false }
+            return date >= dayStart && date < dayEnd
+        }
+    }
+
+    // Active glucose data - either selected day or visible range
+    private var activeGlucoseData: [GlucoseStored] {
+        selectedDate != nil ? selectedDateGlucose : visibleGlucose
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            chartView
+                .frame(height: 200)
+
+            // Date label with transition
+            Text(selectedDate.map { formattedDate(for: $0) } ?? StatChartUtils.formatVisibleDateRange(
+                from: visibleDateRange.start,
+                to: visibleDateRange.end,
+                for: selectedInterval
+            ))
+                .font(.subheadline)
+                .frame(maxWidth: .infinity, alignment: .center)
+                .padding(.top, 8)
+                .animation(.easeInOut, value: selectedDate)
+
+            // Single sector chart with data switching
+            GlucoseSectorChart(
+                highLimit: highLimit,
+                units: units,
+                glucose: activeGlucoseData,
+                timeInRangeType: timeInRangeType,
+                showChart: false
+            )
+            .animation(.easeInOut, value: selectedDate)
+
+            Divider().padding(.vertical, 4)
+
+            // Single metrics view with data switching
+            GlucoseMetricsView(
+                units: units,
+                eA1cDisplayUnit: eA1cDisplayUnit,
+                glucose: activeGlucoseData
+            )
+            .animation(.easeInOut, value: selectedDate)
+        }
+        .onAppear {
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            calculateVisibleDateRange()
+            calculateVisibleGlucose()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                calculateVisibleDateRange()
+                calculateVisibleGlucose()
+            }
+        }
+        .onChange(of: selectedInterval) { _, _ in
+            selectedDate = nil
+            isDaySelected = false
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+        }
+    }
+
+    /// Formatted date string for display
+    private func formattedDate(for date: Date) -> String {
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateFormat = "EEEE, MMMM d, yyyy"
+        return dateFormatter.string(from: date)
+    }
+
+    /// The main chart visualization showing glucose distribution by day
+    private var chartView: some View {
+        Chart {
+            ForEach(state.dailyGlucoseDistributionStats) { day in
+                barMark(x: day, y: day.veryLowPct, rangeName: "veryLow")
+                barMark(x: day, y: day.lowPct, rangeName: "low")
+                barMark(x: day, y: day.inSmallRangePct, rangeName: "inSmallRange")
+                barMark(x: day, y: day.inRangePct - day.inSmallRangePct, rangeName: "inRange")
+                barMark(x: day, y: day.highPct, rangeName: "high")
+                barMark(x: day, y: day.veryHighPct, rangeName: "veryHigh")
+            }
+        }
+        .chartForegroundStyleScale([
+            legend("veryLow"): .purple,
+            legend("low"): .red,
+            legend("inSmallRange"): .green,
+            legend("inRange"): .darkGreen,
+            legend("high"): .loopYellow,
+            legend("veryHigh"): .orange
+        ])
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .onChange(of: selectedDate) { _, newValue in
+            withAnimation(.easeInOut) {
+                isDaySelected = newValue != nil
+            }
+        }
+        .chartYScale(domain: 0 ... 100)
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: .day)) { value in
+                if let date = value.as(Date.self) {
+                    let calendar = Calendar.current
+
+                    switch selectedInterval {
+                    case .month:
+                        // Mark the first day of the week
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday {
+                            AxisValueLabel(format: .dateTime.day(), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Mark the start of the month
+                        let day = calendar.component(.day, from: date)
+                        if day == 1 {
+                            AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        // Mark every day
+                        AxisValueLabel(format: .dateTime.weekday(.abbreviated), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing, values: [4, 25, 50, 75, 100]) { value in
+                if let percentage = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text((percentage / 100).formatted(.percent.precision(.fractionLength(0))))
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartYAxisLabel(alignment: .trailing) {
+            Text("Percentage")
+                .foregroundStyle(.primary)
+                .font(.footnote)
+                .padding(.vertical, 3)
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartScrollPosition(x: $scrollPosition.animation(.easeInOut))
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: DateComponents(hour: 0),
+                majorAlignment: .matching(
+                    StatChartUtils.alignmentComponents(for: selectedInterval)
+                )
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+    }
+
+    /// Formats a short string with the glucose values of the requested range.
+    private func legend(_ rangeName: String) -> String {
+        switch rangeName {
+        case "veryLow":
+            return "<\(Decimal(54).formatted(for: units))"
+        case "low":
+            return "\(Decimal(54).formatted(for: units))-\(Decimal(timeInRangeType.bottomThreshold - 1).formatted(for: units))"
+        case "inSmallRange":
+            return "\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(Decimal(timeInRangeType.topThreshold).formatted(for: units))"
+        case "inRange":
+            return "\(Decimal(timeInRangeType.topThreshold + 1).formatted(for: units))-\(highLimit.formatted(for: units))"
+        case "high":
+            return "\((highLimit + 1).formatted(for: units))-\(Decimal(250).formatted(for: units))"
+        case "veryHigh":
+            return ">\(Decimal(250).formatted(for: units))"
+        default:
+            return "error"
+        }
+    }
+
+    /// Creates a bar mark for the requested date and range
+    private func barMark(x: GlucoseDailyDistributionStats, y: Double, rangeName: String) -> some ChartContent {
+        BarMark(
+            x: .value("Date", x.date, unit: .day),
+            y: .value("Percentage", y)
+        )
+        .foregroundStyle(by: .value("Range", legend(rangeName)))
+        .opacity(selectedDate == nil || Calendar.current.isDate(selectedDate!, inSameDayAs: x.date) ? 1 : 0.3)
+    }
+}

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

@@ -0,0 +1,421 @@
+import Charts
+import SwiftUI
+
+enum GlucosePercentileType: String, Identifiable {
+    case minimum = "Min"
+    case percentile10 = "10th"
+    case percentile25 = "25th"
+    case median = "Median"
+    case percentile75 = "75th"
+    case percentile90 = "90th"
+    case maximum = "Max"
+
+    var id: String { rawValue }
+
+    // Function to get the percentile value from a stats object
+    func getValue(from stats: GlucoseDailyPercentileStats) -> Double {
+        switch self {
+        case .minimum: return stats.minimum
+        case .percentile10: return stats.percentile10
+        case .percentile25: return stats.percentile25
+        case .median: return stats.median
+        case .percentile75: return stats.percentile75
+        case .percentile90: return stats.percentile90
+        case .maximum: return stats.maximum
+        }
+    }
+}
+
+struct GlucoseDailyPercentileChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let units: GlucoseUnits
+    let timeInRangeType: TimeInRangeType
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
+
+    @Binding var isDaySelected: Bool
+
+    // Scrolling and selection states
+    @State private var scrollPosition = Date()
+    @State private var selectedDate: Date?
+    @State private var updateTimer = Stat.UpdateTimer()
+    @State private var visibleDailyStats: [GlucoseDailyPercentileStats] = []
+
+    // State for selected percentile
+    @State private var selectedPercentile: GlucosePercentileType?
+
+    // State model for accessing the shared calculations
+    let state: Stat.StateModel
+
+    // Computes the visible date range based on the current scroll position
+    @State private var visibleDateRange: (start: Date, end: Date) = (Date(), Date())
+
+    private func calculateVisibleDailyStats() {
+        let calendar = Calendar.current
+        visibleDailyStats = state.dailyGlucosePercentileStats.filter { stat in
+            let statDate = calendar.startOfDay(for: stat.date)
+            return statDate >= calendar.startOfDay(for: visibleDateRange.start) &&
+                statDate <= calendar.startOfDay(for: visibleDateRange.end)
+        }
+    }
+
+    private func calculateVisibleDateRange() {
+        visibleDateRange = StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
+    }
+
+    // Gets selected day stats
+    private var selectedDateStats: GlucoseDailyPercentileStats? {
+        selectedDate.flatMap { day in
+            state.glucosePercentileCache[Calendar.current.startOfDay(for: day)]
+        }
+    }
+
+    // Aggregates data from all visible days
+    private var aggregatedVisibleStats: GlucoseDailyPercentileStats? {
+        guard !visibleDailyStats.isEmpty else { return nil }
+
+        // Collect all glucose values from visible days
+        var allMinimums: [Double] = []
+        var allMaximums: [Double] = []
+        var all10thPercentiles: [Double] = []
+        var all25thPercentiles: [Double] = []
+        var allMedians: [Double] = []
+        var all75thPercentiles: [Double] = []
+        var all90thPercentiles: [Double] = []
+
+        // Collect data from all visible days
+        for stats in visibleDailyStats where stats.median > 0 {
+            allMinimums.append(stats.minimum)
+            allMaximums.append(stats.maximum)
+            all10thPercentiles.append(stats.percentile10)
+            all25thPercentiles.append(stats.percentile25)
+            allMedians.append(stats.median)
+            all75thPercentiles.append(stats.percentile75)
+            all90thPercentiles.append(stats.percentile90)
+        }
+
+        // Calculate aggregated values
+        let aggMinimum = allMinimums.min() ?? 0
+        let aggMaximum = allMaximums.max() ?? 0
+        let aggP10 = StatChartUtils.medianCalculationDouble(array: all10thPercentiles)
+        let aggP25 = StatChartUtils.medianCalculationDouble(array: all25thPercentiles)
+        let aggMedian = StatChartUtils.medianCalculationDouble(array: allMedians)
+        let aggP75 = StatChartUtils.medianCalculationDouble(array: all75thPercentiles)
+        let aggP90 = StatChartUtils.medianCalculationDouble(array: all90thPercentiles)
+
+        // Create a new stats object with the visible date range and aggregated values
+        return GlucoseDailyPercentileStats(
+            date: visibleDateRange.start,
+            readings: [], // Empty array since this is aggregated data
+            minimum: aggMinimum,
+            percentile10: aggP10,
+            percentile25: aggP25,
+            median: aggMedian,
+            percentile75: aggP75,
+            percentile90: aggP90,
+            maximum: aggMaximum
+        )
+    }
+
+    // Format a single date for display
+    private func formatDate(_ date: Date) -> String {
+        date.formatted(.dateTime.weekday(.wide).month(.wide).day().year())
+    }
+
+    // Get the appropriate detail view data
+    private var detailViewData: (data: GlucoseDailyPercentileStats, dateText: String)? {
+        if let selectedData = selectedDateStats {
+            // Case 1: Selected specific day
+            return (selectedData, selectedData.date.formatted(.dateTime.weekday(.wide).month(.wide).day().year()))
+        } else if let aggregatedData = aggregatedVisibleStats {
+            // Case 2: Using aggregated data
+            return (aggregatedData, StatChartUtils.formatVisibleDateRange(
+                from: visibleDateRange.start,
+                to: visibleDateRange.end,
+                for: selectedInterval
+            ))
+        }
+        return nil
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            boxplotChart
+                .frame(height: 300)
+
+            // Display detail view if we have data
+            if let viewData = detailViewData {
+                GlucoseDailyPercentileDetailView(
+                    dayData: viewData.data,
+                    units: units,
+                    dateRangeText: viewData.dateText,
+                    selectedPercentile: $selectedPercentile
+                )
+                .padding(.top, 4)
+            }
+        }
+        .onAppear {
+            calculateVisibleDateRange()
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            calculateVisibleDailyStats()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                calculateVisibleDateRange()
+                calculateVisibleDailyStats()
+            }
+        }
+        .onChange(of: selectedInterval) { _, _ in
+            selectedDate = nil
+            selectedPercentile = nil
+            isDaySelected = false
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+        }
+    }
+
+    // Simple boxplot chart with improved visuals - broken down into components
+    private var boxplotChart: some View {
+        Chart {
+            // First draw all the non-interactive elements
+            ForEach(state.dailyGlucosePercentileStats) { day in
+                if day.maximum > 0 { // Check if we have valid data
+                    // Add background components for each day
+                    spacerBarMark(for: day)
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.minimum.asUnit(units),
+                        endValue: day.percentile10.asUnit(units),
+                        rangeName: "0-100%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile10.asUnit(units),
+                        endValue: day.percentile25.asUnit(units),
+                        rangeName: "10-90%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile25.asUnit(units),
+                        endValue: day.percentile75.asUnit(units),
+                        rangeName: "25-75%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile75.asUnit(units),
+                        endValue: day.percentile90.asUnit(units),
+                        rangeName: "10-90%"
+                    )
+                    percentileBarMark(
+                        for: day,
+                        startValue: day.percentile90.asUnit(units),
+                        endValue: day.maximum.asUnit(units),
+                        rangeName: "0-100%"
+                    )
+                }
+            }
+
+            // Draw median marks - these should appear above the percentile bars but below the selected percentile
+            ForEach(state.dailyGlucosePercentileStats) { day in
+                if day.maximum > 0 {
+                    medianMark(for: day)
+                }
+            }
+
+            // Draw the selected percentile elements LAST so they're on top
+            if let selectedPercentile = selectedPercentile {
+                ForEach(state.dailyGlucosePercentileStats) { day in
+                    if day.maximum > 0 {
+                        // Line connecting points
+                        LineMark(
+                            x: .value("SelectedDate", day.date, unit: .day),
+                            y: .value("SelectedValue", selectedPercentile.getValue(from: day).asUnit(units))
+                        )
+                        .foregroundStyle(Color.purple)
+                        .lineStyle(StrokeStyle(lineWidth: selectedInterval == .total ? 1 : 2))
+                        .zIndex(200) // Set very high z-index
+
+                        // Point marks
+                        PointMark(
+                            x: .value("SelectedDate", day.date, unit: .day),
+                            y: .value("SelectedValue", selectedPercentile.getValue(from: day).asUnit(units))
+                        )
+                        .symbolSize(selectedInterval == .total ? 10 : 30)
+                        .foregroundStyle(Color.purple)
+                        .zIndex(300) // Even higher z-index for points
+                    }
+                }
+            }
+
+            // Threshold lines
+            RuleMark(
+                y: .value("Low Limit", Double(timeInRangeType.bottomThreshold).asUnit(units))
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(by: .value("Range", "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"))
+            .zIndex(100)
+
+            RuleMark(
+                y: .value("Mid Limit", Double(timeInRangeType.topThreshold).asUnit(units))
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(by: .value("Range", "\(timeInRangeType.topThreshold.formatted(withUnits: units))"))
+            .zIndex(100)
+
+            RuleMark(
+                y: .value("High Limit", Double(highLimit.asUnit(units)))
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(by: .value("Range", "\(highLimit.formatted(withUnits: units))"))
+            .zIndex(100)
+        }
+        .chartYAxis {
+            AxisMarks(values: .automatic) { value in
+                AxisGridLine()
+                AxisTick()
+                AxisValueLabel {
+                    if let glucoseValue = value.as(Double.self) {
+                        Text(
+                            units == .mmolL ?
+                                glucoseValue.formatted(.number.precision(.fractionLength(1))) :
+                                glucoseValue.formatted(.number.precision(.fractionLength(0)))
+                        )
+                        .font(.caption)
+                    }
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: .day)) { value in
+                if let date = value.as(Date.self) {
+                    let calendar = Calendar.current
+
+                    switch selectedInterval {
+                    case .month:
+                        // Mark the first day of the week
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday {
+                            AxisValueLabel(format: .dateTime.day(), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .total:
+                        // Mark the start of the month
+                        let day = calendar.component(.day, from: date)
+                        if day == 1 {
+                            AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        // Mark every day
+                        AxisValueLabel(format: .dateTime.weekday(.abbreviated), centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartYScale(domain: glucoseYScaleDomain())
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .onChange(of: selectedDate) { _, newValue in
+            isDaySelected = newValue != nil
+            // Clear percentile selection when a day is selected
+            if newValue != nil {
+                selectedPercentile = nil
+            }
+        }
+        .chartForegroundStyleScale([
+            "0-100%": .blue.opacity(0.15),
+            "10-90%": .blue.opacity(0.3),
+            "25-75%": .blue.opacity(0.5),
+            "Median": .blue,
+            "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": .red,
+            "\(timeInRangeType.topThreshold.formatted(withUnits: units))": .mint,
+            "\(highLimit.formatted(withUnits: units))": .orange
+        ])
+        .chartScrollableAxes(.horizontal)
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: DateComponents(hour: 0),
+                majorAlignment: .matching(
+                    StatChartUtils.alignmentComponents(for: selectedInterval)
+                )
+            )
+        )
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
+    }
+
+    // MARK: - Chart Components
+
+    private func percentileBarMark(
+        for day: GlucoseDailyPercentileStats,
+        startValue: Double,
+        endValue: Double,
+        rangeName: String
+    ) -> some ChartContent {
+        BarMark(
+            x: .value("Day", day.date, unit: .day),
+            y: .value("Percentage", endValue - startValue)
+        )
+        .foregroundStyle(by: .value("Range", rangeName))
+        .opacity(getOpacity(for: day))
+    }
+
+    // Median mark - a horizontal line at the median point
+    private func medianMark(for day: GlucoseDailyPercentileStats) -> some ChartContent {
+        let baseDate = Calendar.current.startOfDay(for: day.date)
+        let startOffset = Int(0.15 * 24 * 60) // 15% of minutes in a day
+        let endOffset = Int(0.85 * 24 * 60) // 85% of minutes in a day
+
+        return RuleMark(
+            xStart: .value("DayStart", Calendar.current.date(byAdding: .minute, value: startOffset, to: baseDate)!),
+            xEnd: .value("DayEnd", Calendar.current.date(byAdding: .minute, value: endOffset, to: baseDate)!),
+            y: .value("Median", day.median.asUnit(units))
+        )
+        .lineStyle(StrokeStyle(lineWidth: 2))
+        .foregroundStyle(by: .value("Range", "Median"))
+        .opacity(getOpacity(for: day))
+    }
+
+    // Helper function to determine opacity based on selections
+    private func getOpacity(for day: GlucoseDailyPercentileStats) -> Double {
+        selectedDate.map { date in
+            StatChartUtils.isSameTimeUnit(day.date, date, for: .total) ? 1 : 0.3
+        } ?? 1
+    }
+
+    // Spacer box for each day
+    private func spacerBarMark(for day: GlucoseDailyPercentileStats) -> some ChartContent {
+        BarMark(
+            x: .value("Day", day.date, unit: .day),
+            y: .value("Percentage", day.minimum.asUnit(units))
+        )
+        .foregroundStyle(Color.clear)
+    }
+
+    // Calculate an appropriate Y axis domain for the chart
+    private func glucoseYScaleDomain() -> ClosedRange<Double> {
+        // Find actual min/max from data
+        if visibleDailyStats.isEmpty {
+            return 0 ... (units == .mgdL ? 250 : 14.0)
+        }
+
+        var allValues: [Double] = []
+        for day in visibleDailyStats where day.minimum > 0 {
+            allValues.append(day.minimum.asUnit(units))
+            allValues.append(day.maximum.asUnit(units))
+        }
+
+        guard !allValues.isEmpty else {
+            return 0 ... (units == .mgdL ? 250 : 14.0)
+        }
+
+        let minValue = allValues.min() ?? 0
+        let maxValue = allValues.max() ?? (units == .mgdL ? 250 : 14.0)
+
+        // Add some padding
+        let padding = units == .mgdL ? 20.0 : 1.0
+        return max(0, minValue - padding) ... maxValue + padding
+    }
+}

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

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

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

@@ -11,8 +11,8 @@ struct GlucosePercentileChart: View {
     let glucose: [GlucoseStored]
     /// The upper glucose limit for the chart.
     let highLimit: Decimal
-    /// The lower glucose limit for the chart.
-    let lowLimit: Decimal
+    /// TITR or TING
+    let timeInRangeType: TimeInRangeType
     /// The units used for glucose measurement (mg/dL or mmol/L).
     let units: GlucoseUnits
     /// The hourly glucose statistics.
@@ -47,28 +47,28 @@ struct GlucosePercentileChart: View {
                     // 10-90 percentile area
                     AreaMark(
                         x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
-                        yStart: .value("10th Percentile", stats.percentile10),
-                        yEnd: .value("90th Percentile", stats.percentile90),
+                        yStart: .value("10th Percentile", stats.percentile10.asUnit(units)),
+                        yEnd: .value("90th Percentile", stats.percentile90.asUnit(units)),
                         series: .value("10-90", "10-90")
                     )
-                    .foregroundStyle(by: .value("Series", "10-90"))
+                    .foregroundStyle(by: .value("Series", "10-90%"))
                     .opacity(stats.median > 0 ? 0.3 : 0)
 
                     // 25-75 percentile area
                     AreaMark(
                         x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
-                        yStart: .value("25th Percentile", stats.percentile25),
-                        yEnd: .value("75th Percentile", stats.percentile75),
+                        yStart: .value("25th Percentile", stats.percentile25.asUnit(units)),
+                        yEnd: .value("75th Percentile", stats.percentile75.asUnit(units)),
                         series: .value("25-75", "25-75")
                     )
-                    .foregroundStyle(by: .value("Series", "25-75"))
+                    .foregroundStyle(by: .value("Series", "25-75%"))
                     .opacity(stats.median > 0 ? 0.5 : 0)
 
                     // Median line
                     if stats.median > 0 {
                         LineMark(
                             x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
-                            y: .value("Median", stats.median),
+                            y: .value("Median", stats.median.asUnit(units)),
                             series: .value("Median", "Median")
                         )
                         .lineStyle(StrokeStyle(lineWidth: 2))
@@ -77,13 +77,17 @@ struct GlucosePercentileChart: View {
                 }
 
                 // High/Low limit lines
-                RuleMark(y: .value("High Limit", Double(highLimit)))
+                RuleMark(y: .value("Low Limit", Double(timeInRangeType.bottomThreshold).asUnit(units)))
                     .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
-                    .foregroundStyle(by: .value("Series", "High"))
+                    .foregroundStyle(by: .value("Series", "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"))
 
-                RuleMark(y: .value("Low Limit", Double(lowLimit)))
+                RuleMark(y: .value("Mid Limit", Double(timeInRangeType.topThreshold).asUnit(units)))
                     .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
-                    .foregroundStyle(by: .value("Series", "Low"))
+                    .foregroundStyle(by: .value("Series", "\(timeInRangeType.topThreshold.formatted(withUnits: units))"))
+
+                RuleMark(y: .value("High Limit", Double(highLimit.asUnit(units))))
+                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                    .foregroundStyle(by: .value("Series", "\(highLimit.formatted(withUnits: units))"))
 
                 if let selectedStats, let selection {
                     RuleMark(x: .value("Selection", selection))
@@ -102,19 +106,21 @@ struct GlucosePercentileChart: View {
                 }
             }
             .chartForegroundStyleScale([
-                "10-90": Color.blue.opacity(0.3),
-                "25-75": Color.blue.opacity(0.5),
+                "10-90%": Color.blue.opacity(0.3),
+                "25-75%": Color.blue.opacity(0.5),
                 "Median": Color.blue,
-                "High": Color.orange,
-                "Low": Color.red
+                "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))": Color.red,
+                "\(timeInRangeType.topThreshold.formatted(withUnits: units))": Color.mint,
+                "\(highLimit.formatted(withUnits: units))": Color.orange
             ])
             .chartLegend(position: .bottom, alignment: .leading, spacing: 12) {
                 let legendItems: [(String, Color)] = [
                     ("10-90%", Color.blue.opacity(0.3)),
-                    ("20-75%", Color.blue.opacity(0.5)),
+                    ("25-75%", Color.blue.opacity(0.5)),
                     (String(localized: "Median"), Color.blue),
-                    (String(localized: "High Threshold"), Color.orange),
-                    (String(localized: "Low Threshold"), Color.red)
+                    (String(localized: "\(timeInRangeType.bottomThreshold.formatted(withUnits: units))"), Color.red),
+                    (String(localized: "\(timeInRangeType.topThreshold.formatted(withUnits: units))"), Color.mint),
+                    (String(localized: "\(highLimit.formatted(withUnits: units))"), Color.orange)
                 ]
 
                 let columns = [GridItem(.adaptive(minimum: 100), spacing: 4)]
@@ -130,7 +136,7 @@ struct GlucosePercentileChart: View {
                     if let glucose = value.as(Double.self) {
                         AxisValueLabel {
                             Text(
-                                units == .mmolL ? glucose.asMmolL.formatted(.number.precision(.fractionLength(0))) : glucose
+                                units == .mmolL ? glucose.formatted(.number.precision(.fractionLength(1))) : glucose
                                     .formatted(.number.precision(.fractionLength(0)))
                             )
                             .font(.footnote)
@@ -183,12 +189,6 @@ struct AGPSelectionPopover: View {
         }
     }
 
-    /// A helper function to format glucose values based on the selected unit.
-    private func formattedGlucoseValue(_ value: Double) -> String {
-        units == .mmolL ? value.formattedAsMmolL :
-            value.formatted()
-    }
-
     var body: some View {
         VStack(alignment: .leading, spacing: 4) {
             Text(timeText).bold().font(.subheadline)
@@ -196,27 +196,27 @@ struct AGPSelectionPopover: View {
             Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 4) {
                 GridRow {
                     Text("Median:").bold()
-                    Text(formattedGlucoseValue(stats.median))
+                    Text(stats.median.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("90%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile90))
+                    Text(stats.percentile90.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("75%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile75))
+                    Text(stats.percentile75.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("25%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile25))
+                    Text(stats.percentile25.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
                 GridRow {
                     Text("10%:").bold()
-                    Text(formattedGlucoseValue(stats.percentile10))
+                    Text(stats.percentile10.formatted(for: units))
                     Text(units.rawValue).foregroundStyle(.secondary)
                 }
             }.font(.headline)

+ 71 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileDetailView.swift

@@ -0,0 +1,71 @@
+import SwiftUI
+
+struct GlucoseDailyPercentileDetailView: View {
+    let dayData: GlucoseDailyPercentileStats
+    let units: GlucoseUnits
+    let dateRangeText: String
+
+    // Binding to the parent's selectedPercentile
+    @Binding var selectedPercentile: GlucosePercentileType?
+
+    var body: some View {
+        VStack(alignment: .center, spacing: 8) {
+            Text(dateRangeText)
+                .font(.subheadline.weight(.medium))
+                .padding(.bottom, 4)
+
+            // Only show percentile details if we have valid data
+            if dayData.median > 0 {
+                // Improved percentile display
+                HStack(spacing: 0) {
+                    percentileItem(label: "Min", value: round(dayData.minimum), type: .minimum)
+                    percentileItem(label: "10%", value: round(dayData.percentile10), type: .percentile10)
+                    percentileItem(label: "25%", value: round(dayData.percentile25), type: .percentile25)
+                    percentileItem(label: "Median", value: round(dayData.median), type: .median)
+                    percentileItem(label: "75%", value: round(dayData.percentile75), type: .percentile75)
+                    percentileItem(label: "90%", value: round(dayData.percentile90), type: .percentile90)
+                    percentileItem(label: "Max", value: round(dayData.maximum), type: .maximum)
+                }
+                .padding(.vertical, 8)
+            } else {
+                Text("No glucose data available for this day")
+                    .foregroundStyle(.secondary)
+                    .padding()
+            }
+        }
+    }
+
+    /// Creates a single percentile item for the detail view
+    private func percentileItem(
+        label: String,
+        value: Double,
+        type: GlucosePercentileType
+    ) -> some View {
+        VStack(spacing: 2) {
+            Text(Decimal(value).formatted(for: units))
+                .font(.callout.monospacedDigit())
+                .foregroundStyle(type == selectedPercentile ? Color.purple : .primary)
+
+            Text(label)
+                .font(.caption2)
+                .foregroundStyle(type == selectedPercentile ? Color.purple : .secondary)
+        }
+        .frame(maxWidth: .infinity)
+        .padding(4)
+        .background(
+            RoundedRectangle(cornerRadius: 4)
+                .fill(type == selectedPercentile ? Color.purple.opacity(0.1) : Color.clear)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .strokeBorder(type == selectedPercentile ? Color.purple : Color.clear, lineWidth: 1)
+                )
+        )
+        .contentShape(Rectangle())
+        .onTapGesture {
+            withAnimation {
+                // Toggle selection on tap
+                selectedPercentile = (selectedPercentile == type) ? nil : type
+            }
+        }
+    }
+}

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

@@ -8,6 +8,7 @@ struct GlucoseSectorChart: View {
     let units: GlucoseUnits
     let glucose: [GlucoseStored]
     let timeInRangeType: TimeInRangeType
+    let showChart: Bool
 
     @State private var selectedCount: Int?
     @State private var selectedRange: GlucoseRange?
@@ -23,119 +24,181 @@ struct GlucoseSectorChart: View {
     }
 
     var body: some View {
-        HStack(alignment: .center, spacing: 20) {
-            // Calculate total number of glucose readings
-            let total = Decimal(glucose.count)
-            // 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 >= 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, +)
-
-            let glucoseAverage = Decimal(sumReadings) / total
-            let medianGlucose = StatChartUtils.medianCalculation(array: justGlucoseArray)
-
-            let lowPercentage = Decimal(low) / total * 100
-            let tightPercentage = Decimal(tight) / total * 100
-            let inRangePercentage = Decimal(normal) / total * 100
-            let highPercentage = Decimal(high) / total * 100
-
-            VStack(alignment: .leading, spacing: 10) {
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(highLimit))").font(.subheadline)
+        if glucose.count < 1 {
+            Text("No glucose readings found.")
+        } else {
+            HStack(alignment: .center, spacing: 20) {
+                // Calculate total number of glucose readings
+                let total = Decimal(glucose.count)
+                // 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 >= timeInRangeType.bottomThreshold && $0.glucose <= Int(highLimit) }
+                    .count
+                // Count readings less than low limit (low) (70 mg/dL if not showing chart, otherwise 70 for TITR and 63 for TING)
+                let low = glucose.filter { $0.glucose < (showChart ? Int(timeInRangeType.bottomThreshold) : 70) }.count
+                // Count readings less than moderately low limit (63 mg/dL)
+                let moderatelyLow = glucose.filter { $0.glucose < 63 }.count
+                // Count readings less than moderately high limit (220 mg/dL)
+                let moderatelyHigh = glucose.filter { $0.glucose > 220 }.count
+                // Count readings less than very low limit (54 mg/dL)
+                let veryLow = glucose.filter { $0.glucose < 54 }.count
+                // Count readings less than very high limit (250 mg/dL)
+                let veryHigh = glucose.filter { $0.glucose > 250 }.count
+
+                let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
+                let sumReadings = justGlucoseArray.reduce(0, +)
+
+                let glucoseAverage = Decimal(sumReadings) / total
+                let medianGlucose = StatChartUtils.medianCalculation(array: justGlucoseArray)
+
+                let lowPercentage = Decimal(low) / total * 100
+                let tightPercentage = Decimal(tight) / total * 100
+                let inRangePercentage = Decimal(normal) / total * 100
+                let highPercentage = Decimal(high) / total * 100
+                let moderatelyLowPercentage = Decimal(moderatelyLow) / total * 100
+                let moderatelyHighPercentage = Decimal(moderatelyHigh) / total * 100
+                let veryLowPercentage = Decimal(veryLow) / total * 100
+                let veryHighPercentage = Decimal(veryHigh) / total * 100
+
+                VStack(alignment: .leading, spacing: 10) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(highLimit.formatted(for: units))"
+                        )
+                        .font(.subheadline)
                         .foregroundStyle(Color.secondary)
-                    Text(inRangePercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.loopGreen)
-                }
+                        Text(formatPercentage(inRangePercentage, tight: true))
+                            .foregroundStyle(Color.loopGreen)
+                    }
 
-                VStack(alignment: .leading, spacing: 5) {
-                    Text(
-                        "\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold)))"
-                    )
-                    .font(.subheadline)
-                    .foregroundStyle(Color.secondary)
-                    Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.green)
-                }
-            }.padding(.leading, 5)
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(Decimal(timeInRangeType.topThreshold).formatted(for: units))"
+                        )
+                        .font(.subheadline)
+                        .foregroundStyle(Color.secondary)
+                        Text(formatPercentage(tightPercentage, tight: true))
+                            .foregroundStyle(Color.green)
+                    }
+                }.padding(.leading, 5)
+
+                VStack(alignment: .leading, spacing: 10) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text("> \(highLimit.formatted(for: units))").font(.subheadline)
+                            .foregroundStyle(Color.secondary)
+                        Text(formatPercentage(highPercentage, tight: true))
+                            .foregroundStyle(Color.loopYellow)
+                    }
 
-            VStack(alignment: .leading, spacing: 10) {
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("> \(formatValue(highLimit))").font(.subheadline)
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(
+                            "< \(Decimal(showChart ? timeInRangeType.bottomThreshold : 70).formatted(for: units))"
+                        )
+                        .font(.subheadline)
                         .foregroundStyle(Color.secondary)
-                    Text(highPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.orange)
+                        Text(formatPercentage(lowPercentage, tight: true))
+                            .foregroundStyle(Color.red)
+                    }
                 }
+                // If not showing chart, show extra stats
+                if !showChart {
+                    VStack(alignment: .leading, spacing: 10) {
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text("> \(Decimal(220).formatted(for: units))").font(.subheadline)
+                                .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(moderatelyHighPercentage, tight: true))
+                                .foregroundStyle(Color.loopYellow)
+                        }
 
-                VStack(alignment: .leading, spacing: 5) {
-                    Text(
-                        "< \(formatValue(Decimal(timeInRangeType.bottomThreshold)))"
-                    )
-                    .font(.subheadline)
-                    .foregroundStyle(Color.secondary)
-                    Text(lowPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                        .foregroundStyle(Color.loopRed)
-                }
-            }
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text(
+                                "< \(Decimal(63).formatted(for: units))"
+                            )
+                            .font(.subheadline)
+                            .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(moderatelyLowPercentage, tight: true))
+                                .foregroundStyle(Color.red)
+                        }
+                    }
+                    VStack(alignment: .leading, spacing: 10) {
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text("> \(Decimal(250).formatted(for: units))").font(.subheadline)
+                                .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(veryHighPercentage, tight: true))
+                                .foregroundStyle(Color.orange)
+                        }
 
-            VStack(alignment: .leading, spacing: 10) {
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("Average").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(
-                        units == .mgdL ? glucoseAverage
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : glucoseAverage.asMmolL
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                    )
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text(
+                                "< \(Decimal(54).formatted(for: units))"
+                            )
+                            .font(.subheadline)
+                            .foregroundStyle(Color.secondary)
+                            Text(formatPercentage(veryLowPercentage, tight: true))
+                                .foregroundStyle(Color.purple)
+                        }
+                    }
                 }
 
-                VStack(alignment: .leading, spacing: 5) {
-                    Text("Median").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(
-                        units == .mgdL ? medianGlucose
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : medianGlucose.asMmolL
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                    )
+                VStack(alignment: .leading, spacing: 10) {
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(showChart ? "Average" : "Avg").font(.subheadline).foregroundStyle(Color.secondary)
+                        Text(
+                            units == .mgdL ? glucoseAverage
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : glucoseAverage
+                                .asMmolL
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                        )
+                    }
+
+                    VStack(alignment: .leading, spacing: 5) {
+                        Text(showChart ? "Median" : "Med").font(.subheadline).foregroundStyle(Color.secondary)
+                        Text(
+                            units == .mgdL ? medianGlucose
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) : medianGlucose
+                                .asMmolL
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                        )
+                    }
                 }
-            }
 
-            Chart {
-                ForEach(rangeData, id: \.range) { data in
-                    SectorMark(
-                        angle: .value("Percentage", data.count),
-                        innerRadius: .ratio(0.618),
-                        outerRadius: selectedRange == data.range ? 100 : 80,
-                        angularInset: 1.5
-                    )
-                    .foregroundStyle(data.color)
+                if showChart {
+                    Chart {
+                        ForEach(rangeData, id: \.range) { data in
+                            SectorMark(
+                                angle: .value("Percentage", data.count),
+                                innerRadius: .ratio(0.618),
+                                outerRadius: selectedRange == data.range ? 100 : 80
+                            )
+                            .foregroundStyle(data.color)
+                        }
+                    }
+                    .chartAngleSelection(value: $selectedCount)
+                    .frame(height: 100)
                 }
             }
-            .chartAngleSelection(value: $selectedCount)
-            .frame(height: 100)
-        }
-        .onChange(of: selectedCount) { _, newValue in
-            if let newValue {
-                withAnimation {
-                    getSelectedRange(value: newValue)
-                }
-            } else {
-                withAnimation {
-                    selectedRange = nil
+            .onChange(of: selectedCount) { _, newValue in
+                if let newValue {
+                    withAnimation {
+                        getSelectedRange(value: newValue)
+                    }
+                } else {
+                    withAnimation {
+                        selectedRange = nil
+                    }
                 }
             }
-        }
-        .overlay(alignment: .top) {
-            if let selectedRange {
-                let data = getDetailedData(for: selectedRange)
-                RangeDetailPopover(data: data)
-                    .transition(.scale.combined(with: .opacity))
-                    .offset(y: -150) // TODO: make this dynamic
+            .overlay(alignment: .top) {
+                if let selectedRange {
+                    let data = getDetailedData(for: selectedRange)
+                    RangeDetailPopover(data: data)
+                        .transition(.scale.combined(with: .opacity))
+                        .offset(y: -150) // TODO: make this dynamic
+                }
             }
         }
     }
@@ -167,7 +230,7 @@ struct GlucoseSectorChart: View {
 
         // Return array of tuples with range data
         return [
-            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .orange),
+            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .loopYellow),
             (.inRange, inRangeCount, Decimal(inRangeCount) / Decimal(total) * 100, .green),
             (.low, lowCount, Decimal(lowCount) / Decimal(total) * 100, .red)
         ]
@@ -216,15 +279,18 @@ struct GlucoseSectorChart: View {
 
             return RangeDetail(
                 title: String(localized: "High Glucose"),
-                color: .orange,
+                color: .loopYellow,
                 items: [
-                    (String(localized: "Very High (>\(formatValue(250)))"), formatPercentage(Decimal(veryHigh) / total * 100)),
                     (
-                        String(localized: "High (\(formatValue(highLimit))-\(formatValue(250)))"),
+                        String(localized: "Very High (>\(Decimal(250).formatted(for: units)))"),
+                        formatPercentage(Decimal(veryHigh) / total * 100)
+                    ),
+                    (
+                        String(localized: "High (\(highLimit.formatted(for: units))-\(Decimal(250).formatted(for: units)))"),
                         formatPercentage(Decimal(high) / total * 100)
                     ),
-                    (String(localized: "Average"), formatValue(average)),
-                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "Average"), average.formatted(for: units)),
+                    (String(localized: "Median"), median.formatted(for: units)),
                     (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
@@ -242,18 +308,18 @@ struct GlucoseSectorChart: View {
                 items: [
                     (
                         String(
-                            localized: "Normal (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(highLimit)))"
+                            localized: "Normal (\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(highLimit.formatted(for: units)))"
                         ),
                         formatPercentage(Decimal(glucoseValues.count) / total * 100)
                     ),
                     (
                         String(
-                            localized: "\(timeInRangeType == .timeInTightRange ? "TITR" : "TING") (\(formatValue(Decimal(timeInRangeType.bottomThreshold)))-\(formatValue(Decimal(timeInRangeType.topThreshold))))"
+                            localized: "\(timeInRangeType == .timeInTightRange ? "TITR" : "TING") (\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units))-\(Decimal(timeInRangeType.topThreshold).formatted(for: units)))"
                         ),
                         formatPercentage(Decimal(tight) / total * 100)
                     ),
-                    (String(localized: "Average"), formatValue(average)),
-                    (String(localized: "Median"), formatValue(median)),
+                    (String(localized: "Average"), average.formatted(for: units)),
+                    (String(localized: "Median"), median.formatted(for: units)),
                     (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
@@ -271,12 +337,17 @@ struct GlucoseSectorChart: View {
                 color: .red,
                 items: [
                     (
-                        String(localized: "Low (\(formatValue(54))-\(formatValue(Decimal(timeInRangeType.bottomThreshold))))"),
+                        String(
+                            localized: "Low (\(Decimal(54).formatted(for: units))-\(Decimal(timeInRangeType.bottomThreshold).formatted(for: units)))"
+                        ),
                         formatPercentage(Decimal(low) / total * 100)
                     ),
-                    (String(localized: "Very Low (<\(formatValue(54)))"), formatPercentage(Decimal(veryLow) / total * 100)),
-                    (String(localized: "Average"), formatValue(average)),
-                    (String(localized: "Median"), formatValue(median)),
+                    (
+                        String(localized: "Very Low (<\(Decimal(54).formatted(for: units))"),
+                        formatPercentage(Decimal(veryLow) / total * 100)
+                    ),
+                    (String(localized: "Average"), average.formatted(for: units)),
+                    (String(localized: "Median"), median.formatted(for: units)),
                     (String(localized: "SD"), formatSD(standardDeviation))
                 ]
             )
@@ -286,10 +357,14 @@ struct GlucoseSectorChart: View {
     /// Formats a percentage value to a string with one decimal place.
     /// - Parameter value: A decimal value representing the percentage.
     /// - Returns: A formatted percentage string
-    private func formatPercentage(_ value: Decimal) -> String {
+    private func formatPercentage(_ value: Decimal, tight: Bool = false) -> String {
         let formatter = NumberFormatter()
         formatter.numberStyle = .percent
-        formatter.maximumFractionDigits = 1
+        formatter.minimumFractionDigits = value == 100 ? 0 : 1
+        formatter.maximumFractionDigits = value == 100 ? 0 : 1
+        if tight {
+            formatter.positiveSuffix = "%"
+        }
         return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
     }
 
@@ -319,13 +394,6 @@ struct GlucoseSectorChart: View {
             .number.grouping(.never).rounded().precision(.fractionLength(0))
         ) : sd.formattedAsMmolL
     }
-
-    /// Formats a glucose value based on the current units.
-    /// - Parameter value: A decimal value representing the glucose level.
-    /// - Returns: A formatted string of the glucose value.
-    private func formatValue(_ value: Decimal) -> String {
-        units == .mgdL ? value.description : value.formattedAsMmolL
-    }
 }
 
 /// Represents details about a specific glucose range category including title, color and percentage breakdowns

+ 4 - 4
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -250,24 +250,24 @@ struct BolusStatsView: View {
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
-                    let day = Calendar.current.component(.day, from: date)
-                    let hour = Calendar.current.component(.hour, from: date)
-
                     switch selectedInterval {
                     case .day:
+                        let hour = Calendar.current.component(.hour, from: date)
                         if hour % 6 == 0 { // Show only every 6 hours
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .month:
-                        if day % 3 == 0 { // Only show every 3rd day
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday { // Only show the first day of the week
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
+                        let day = Calendar.current.component(.day, from: date)
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)

+ 2 - 1
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -211,7 +211,8 @@ struct TotalDailyDoseChart: View {
                             AxisGridLine()
                         }
                     case .month:
-                        if day % 3 == 0 { // Only show every 3rd day
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday { // Only show the first day of the week
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()

+ 5 - 3
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopBarChartView.swift

@@ -36,9 +36,11 @@ struct LoopBarChartView: View {
             .chartXAxis {
                 AxisMarks(position: .bottom) { value in
                     if let percentage = value.as(Double.self) {
-                        AxisValueLabel {
-                            Text("\(Int(percentage))%")
-                                .font(.footnote)
+                        if selectedInterval != .today {
+                            AxisValueLabel {
+                                Text("\(Int(percentage))%")
+                                    .font(.footnote)
+                            }
                         }
                         AxisGridLine()
                     }

+ 3 - 2
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -231,7 +231,6 @@ struct MealStatsView: View {
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedInterval == .day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
-                    let day = Calendar.current.component(.day, from: date)
                     let hour = Calendar.current.component(.hour, from: date)
 
                     switch selectedInterval {
@@ -242,13 +241,15 @@ struct MealStatsView: View {
                             AxisGridLine()
                         }
                     case .month:
-                        if day % 3 == 0 { // Only show every 3rd day
+                        let weekday = calendar.component(.weekday, from: date)
+                        if weekday == calendar.firstWeekday { // Only show the first day of the week
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
+                        let day = Calendar.current.component(.day, from: date)
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
                             AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)