Browse Source

Merge pull request #613 from MikePlante1/daily-stats

Add Daily versions of glucose percentile and distribution charts
Deniz Cengiz 11 months ago
parent
commit
2dd905fdf8
22 changed files with 1775 additions and 227 deletions
  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)