ソースを参照

Various refactoring & adjustments
* Rename selectedDuration to selectedInterval, as they are intervals not durations…
* Adjust date formatting for horizontal scrolling charts to better handle full days and partial days
* Force horizontal scrolling charts to begin at midnight and show full day via dummy PointMark
* Add totals to bolus distribution chart
* Emphasize that for .week, .month and .total, calculated values are averages by prepending 'ø'
* Misc localize stuff

Deniz Cengiz 1 年間 前
コミット
c188a46135

+ 9 - 6
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -749,6 +749,9 @@
         }
       }
     },
+    " " : {
+
+    },
     " -  Manual Basal ⚠️" : {
       "comment" : "Current Manual Temp basal",
       "extractionState" : "manual",
@@ -69746,6 +69749,9 @@
         }
       }
     },
+    "Dummy" : {
+
+    },
     "Duration" : {
       "comment" : "Duration of target temp or temp basalMedian loop interval",
       "localizations" : {
@@ -128027,6 +128033,9 @@
         }
       }
     },
+    "ø" : {
+
+    },
     "of" : {
       "comment" : "Bolus progress view",
       "extractionState" : "manual",
@@ -179436,9 +179445,6 @@
         }
       }
     },
-    "Tomorrow" : {
-
-    },
     "Top target" : {
       "comment" : "Upper temp target limit",
       "extractionState" : "manual",
@@ -195159,9 +195165,6 @@
         }
       }
     },
-    "Yesterday" : {
-
-    },
     "You can connect Trio to seamlessly upload and manage your diabetes data on Tidepool." : {
       "localizations" : {
         "bg" : {

+ 50 - 2
Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift

@@ -32,7 +32,7 @@ extension Stat.StateModel {
                 }
 
                 // Initially calculate and cache daily averages
-                await calculateAndCacheBolusAverages()
+                await calculateAndCacheBolusAveragesAndTotals()
             } catch {
                 debug(.default, "\(DebuggingIdentifiers.failed) failed to setup bolus stats: \(error.localizedDescription)")
             }
@@ -154,7 +154,7 @@ extension Stat.StateModel {
     /// 3. Caches the results for later use
     ///
     /// This only needs to be called once during subscribe.
-    private func calculateAndCacheBolusAverages() async {
+    private func calculateAndCacheBolusAveragesAndTotals() async {
         let calendar = Calendar.current
 
         // Calculate averages in context
@@ -176,9 +176,27 @@ extension Stat.StateModel {
             return averages
         }
 
+        // Calculate averages in context
+        let dailyTotals = await bolusTaskContext.perform { [dailyBolusStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyBolusStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate totals for each day
+            var totals: [(Date, Double)] = []
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce(0.0) { _, stat in
+                    stat.manualBolus + stat.smb + stat.external
+                }
+            }
+            return totals
+        }
+
         // Update cache on main thread
         await MainActor.run {
             self.bolusAveragesCache = dailyAverages
+            self.bolusTotalsCache = dailyTotals
         }
     }
 
@@ -189,6 +207,13 @@ extension Stat.StateModel {
         return calculateBolusAveragesForDateRange(from: range.start, to: range.end)
     }
 
+    /// Returns the total bolus values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: Totals for bolus (sum of manual, smb and external) for the date range
+    func getCachedBolusTotals(for range: (start: Date, end: Date)) -> Double {
+        calculateBolusTotalsForDateRange(from: range.start, to: range.end)
+    }
+
     /// Calculates the average bolus values for a given date range
     /// - Parameters:
     ///   - startDate: The start date of the range to calculate averages for
@@ -216,6 +241,29 @@ extension Stat.StateModel {
 
         return (total.0 / count, total.1 / count, total.2 / count)
     }
+
+    /// Calculates the total bolus values for a given date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: A total bolus (sum of manual, smb and external) for the date range
+    func calculateBolusTotalsForDateRange(
+        from startDate: Date,
+        to endDate: Date
+    ) -> Double {
+        // Filter cached values to only include those within the date range
+        let relevantStats = bolusAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return 0 }
+
+        // Calculate total bolus across all days
+        return relevantStats.values.reduce(0.0) { _, totalPerCategory in
+            totalPerCategory.0 + totalPerCategory.1 + totalPerCategory.2
+        }
+    }
 }
 
 /// Extension to convert Decimal to Double

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

@@ -44,7 +44,7 @@ extension Stat.StateModel {
     func setupLoopStatRecords() {
         Task {
             do {
-                let (recordIDs, failedRecordIDs) = try await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
+                let (recordIDs, failedRecordIDs) = try await self.fetchLoopStatRecords(for: selectedIntervalForLoopStats)
 
                 // Update loop records for duration chart
                 await self.updateLoopStatRecords(allLoopIds: recordIDs)
@@ -53,7 +53,7 @@ extension Stat.StateModel {
                 let stats = try await self.getLoopStats(
                     allLoopIds: recordIDs,
                     failedLoopIds: failedRecordIDs,
-                    interval: selectedDurationForLoopStats
+                    interval: selectedIntervalForLoopStats
                 )
 
                 await MainActor.run {

+ 7 - 6
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -41,22 +41,23 @@ extension Stat {
         var hourlyBolusStats: [BolusStats] = []
         var dailyBolusStats: [BolusStats] = []
         var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
+        var bolusTotalsCache: [(Date, total: Double)] = []
 
         // Selected Duration for Glucose Stats
-        var selectedDurationForGlucoseStats: StatsTimeIntervalWithToday = .today {
+        var selectedIntervalForGlucoseStats: StatsTimeIntervalWithToday = .today {
             didSet {
-                setupGlucoseArray(for: selectedDurationForGlucoseStats)
+                setupGlucoseArray(for: selectedIntervalForGlucoseStats)
             }
         }
 
         // Selected Duration for Insulin Stats
-        var selectedDurationForInsulinStats: StatsTimeInterval = .day
+        var selectedIntervalForInsulinStats: StatsTimeInterval = .day
 
         // Selected Duration for Meal Stats
-        var selectedDurationForMealStats: StatsTimeInterval = .day
+        var selectedIntervalForMealStats: StatsTimeInterval = .day
 
         // Selected Duration for Loop Stats
-        var selectedDurationForLoopStats: StatsTimeIntervalWithToday = .today {
+        var selectedIntervalForLoopStats: StatsTimeIntervalWithToday = .today {
             didSet {
                 setupLoopStatRecords()
             }
@@ -173,7 +174,7 @@ extension Stat {
             }
             workItem = newWorkItem
 
-            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: newWorkItem)
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: newWorkItem)
         }
     }
 }

+ 57 - 35
Trio/Sources/Modules/Stat/View/StatChartUtils.swift

@@ -1,12 +1,13 @@
+import Charts
 import Foundation
 import SwiftUI
 
 struct StatChartUtils {
     /// Returns the time interval length for the visible domain based on the selected duration.
-    /// - Parameter selectedDuration: The selected time interval for statistics.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
     /// - Returns: The time interval in seconds.
-    static func visibleDomainLength(for selectedDuration: Stat.StateModel.StatsTimeInterval) -> TimeInterval {
-        switch selectedDuration {
+    static func visibleDomainLength(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> TimeInterval {
+        switch selectedInterval {
         case .day: return 24 * 3600
         case .week: return 7 * 24 * 3600
         case .month: return 30 * 24 * 3600
@@ -17,21 +18,21 @@ struct StatChartUtils {
     /// Computes the visible date range based on the scroll position and selected duration.
     /// - Parameters:
     ///   - scrollPosition: The current scroll position in the chart.
-    ///   - selectedDuration: The selected time interval for statistics.
+    ///   - selectedInterval: The selected time interval for statistics.
     /// - Returns: A tuple containing the start and end dates of the visible range.
     static func visibleDateRange(
         from scrollPosition: Date,
-        for selectedDuration: Stat.StateModel.StatsTimeInterval
+        for selectedInterval: Stat.StateModel.StatsTimeInterval
     ) -> (start: Date, end: Date) {
-        let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedDuration))
+        let end = scrollPosition.addingTimeInterval(visibleDomainLength(for: selectedInterval))
         return (scrollPosition, end)
     }
 
     /// Returns the appropriate date format style based on the selected time interval.
-    /// - Parameter selectedDuration: The selected time interval for statistics.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
     /// - Returns: A Date.FormatStyle configured for the current time interval.
-    static func dateFormat(for selectedDuration: Stat.StateModel.StatsTimeInterval) -> Date.FormatStyle {
-        switch selectedDuration {
+    static func dateFormat(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Date.FormatStyle {
+        switch selectedInterval {
         case .day: return .dateTime.hour()
         case .week: return .dateTime.weekday(.abbreviated)
         case .month: return .dateTime.day()
@@ -40,10 +41,10 @@ struct StatChartUtils {
     }
 
     /// Returns DateComponents for aligning dates based on the selected duration.
-    /// - Parameter selectedDuration: The selected time interval for statistics.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
     /// - Returns: DateComponents configured for the appropriate alignment.
-    static func alignmentComponents(for selectedDuration: Stat.StateModel.StatsTimeInterval) -> DateComponents {
-        switch selectedDuration {
+    static func alignmentComponents(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> DateComponents {
+        switch selectedInterval {
         case .day: return DateComponents(hour: 0)
         case .week: return DateComponents(weekday: 2)
         case .month,
@@ -52,14 +53,15 @@ struct StatChartUtils {
     }
 
     /// Returns the initial scroll position date based on the selected duration.
-    /// - Parameter selectedDuration: The selected time interval for statistics.
+    /// - Parameter selectedInterval: The selected time interval for statistics.
     /// - Returns: A Date representing the initial scroll position.
-    static func getInitialScrollPosition(for selectedDuration: Stat.StateModel.StatsTimeInterval) -> Date {
+    static func getInitialScrollPosition(for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Date {
         let calendar = Calendar.current
         let now = Date()
 
-        switch selectedDuration {
-        case .day: return calendar.date(byAdding: .day, value: -1, to: now)!
+        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)!
@@ -70,11 +72,11 @@ struct StatChartUtils {
     /// - Parameters:
     ///   - date1: The first date.
     ///   - date2: The second date.
-    ///   - selectedDuration: The selected time interval for statistics.
+    ///   - 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 selectedDuration: Stat.StateModel.StatsTimeInterval) -> Bool {
+    static func isSameTimeUnit(_ date1: Date, _ date2: Date, for selectedInterval: Stat.StateModel.StatsTimeInterval) -> Bool {
         let calendar = Calendar.current
-        switch selectedDuration {
+        switch selectedInterval {
         case .day:
             return calendar.isDate(date1, equalTo: date2, toGranularity: .hour)
         default:
@@ -86,28 +88,48 @@ struct StatChartUtils {
     /// - Parameters:
     ///   - start: The start date of the range.
     ///   - end: The end date of the range.
-    ///   - selectedDuration: The selected time interval for statistics.
+    ///   - selectedInterval: The selected time interval for statistics.
     /// - Returns: A formatted string representing the visible date range.
-    static func formatVisibleDateRange(from start: Date, to end: Date, for _: Stat.StateModel.StatsTimeInterval) -> String {
+    static func formatVisibleDateRange(
+        from start: Date,
+        to end: Date,
+        for selectedInterval: Stat.StateModel.StatsTimeInterval
+    ) -> String {
         let calendar = Calendar.current
-        let today = Date()
-
-        let formatDate: (Date) -> String = { date in
-            if calendar.isDate(date, inSameDayAs: today) {
-                return String(localized: "Today")
-            } else if calendar.isDate(date, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
-                return String(localized: "Yesterday")
-            } else if calendar.isDate(date, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
-                return String(localized: "Tomorrow")
-            } else {
-                return date.formatted(.dateTime.day().month())
+
+        // If not .day, we just return "startText - endText", e.g. "Jan 1 - Jan 8"
+        guard selectedInterval == .day else {
+            let formatDate: (Date) -> String = { date in
+                date.formatted(.dateTime.day().month())
             }
+            let startText = formatDate(start)
+            let endText = formatDate(end)
+            return "\(startText) - \(endText)"
         }
 
-        let startText = formatDate(start)
-        let endText = formatDate(end)
+        // For .day mode, we figure out if we are near the boundaries for a "full day" (00:00 - 23:59)
+        let dayStart = calendar.startOfDay(for: start)
+        let nextDayStart = calendar.date(byAdding: .day, value: 1, to: dayStart)!
+
+        // Allow +/- 15 minutes from midnight as buffer, so slow scrolling doesn't break the "full day"
+        let tolerance: TimeInterval = 60 * 15
+
+        let isStartNearMidnight = abs(start.timeIntervalSince(dayStart)) < tolerance
+        let isEndNearNextMidnight = abs(end.timeIntervalSince(nextDayStart)) < tolerance
 
-        return startText == endText ? startText : "\(startText) - \(endText)"
+        let formatDay: (Date) -> String = { date in
+            date.formatted(.dateTime.day().month(.abbreviated))
+        }
+
+        if isStartNearMidnight, isEndNearNextMidnight {
+            // Full day: show just start as "Mon, Jan 1"
+            return dayStart.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated))
+        } else {
+            // Partial day: show start and end
+            let startText = formatDay(start)
+            let endText = formatDay(end)
+            return "\(startText) - \(endText)"
+        }
     }
 
     /// A helper function to create a `VStack` for each statistic.

+ 12 - 12
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -75,7 +75,7 @@ extension Stat {
                 .pickerStyle(.menu)
             }.padding(.horizontal)
 
-            Picker("Duration", selection: $state.selectedDurationForGlucoseStats) {
+            Picker("Duration", selection: $state.selectedIntervalForGlucoseStats) {
                 ForEach(StateModel.StatsTimeIntervalWithToday.allCases, id: \.self) { timeInterval in
                     Text(timeInterval.displayName)
                 }
@@ -122,7 +122,7 @@ extension Stat {
                             lowLimit: state.lowLimit,
                             units: state.units,
                             hourlyStats: state.hourlyStats,
-                            isToday: state.selectedDurationForGlucoseStats == .today
+                            isToday: state.selectedIntervalForGlucoseStats == .today
                         )
                     case .distribution:
                         GlucoseDistributionChart(
@@ -174,7 +174,7 @@ extension Stat {
                 }.pickerStyle(.menu)
             }.padding(.horizontal)
 
-            Picker("Duration", selection: $state.selectedDurationForInsulinStats) {
+            Picker("Duration", selection: $state.selectedIntervalForInsulinStats) {
                 ForEach(StateModel.StatsTimeInterval.allCases) { timeInterval in
                     Text(timeInterval.rawValue).tag(timeInterval)
                 }
@@ -192,8 +192,8 @@ extension Stat {
                         )
                     } else {
                         TotalDailyDoseChart(
-                            selectedDuration: $state.selectedDurationForInsulinStats,
-                            tddStats: state.selectedDurationForInsulinStats == .day ?
+                            selectedInterval: $state.selectedIntervalForInsulinStats,
+                            tddStats: state.selectedIntervalForInsulinStats == .day ?
                                 state.hourlyTDDStats : state.dailyTDDStats,
                             state: state
                         )
@@ -212,8 +212,8 @@ extension Stat {
                         )
                     } else {
                         BolusStatsView(
-                            selectedDuration: $state.selectedDurationForInsulinStats,
-                            bolusStats: state.selectedDurationForInsulinStats == .day ?
+                            selectedInterval: $state.selectedIntervalForInsulinStats,
+                            bolusStats: state.selectedIntervalForInsulinStats == .day ?
                                 state.hourlyBolusStats : state.dailyBolusStats,
                             state: state
                         )
@@ -244,7 +244,7 @@ extension Stat {
                 }.pickerStyle(.menu)
             }.padding(.horizontal)
 
-            Picker("Duration", selection: $state.selectedDurationForLoopStats) {
+            Picker("Duration", selection: $state.selectedIntervalForLoopStats) {
                 ForEach(StateModel.StatsTimeIntervalWithToday.allCases, id: \.self) { interval in
                     Text(interval.displayName)
                 }
@@ -286,7 +286,7 @@ extension Stat {
             VStack(spacing: Constants.spacing) {
                 LoopBarChartView(
                     loopStatRecords: state.loopStatRecords,
-                    selectedDuration: state.selectedDurationForLoopStats,
+                    selectedInterval: state.selectedIntervalForLoopStats,
                     statsData: state.loopStats
                 )
             }
@@ -316,7 +316,7 @@ extension Stat {
                 }.pickerStyle(.menu)
             }.padding(.horizontal)
 
-            Picker("Duration", selection: $state.selectedDurationForMealStats) {
+            Picker("Duration", selection: $state.selectedIntervalForMealStats) {
                 ForEach(StateModel.StatsTimeInterval.allCases, id: \.self) { timeInterval in
                     Text(timeInterval.rawValue)
                 }
@@ -338,8 +338,8 @@ extension Stat {
                         )
                     } else {
                         MealStatsView(
-                            selectedDuration: $state.selectedDurationForMealStats,
-                            mealStats: state.selectedDurationForMealStats == .day ?
+                            selectedInterval: $state.selectedIntervalForMealStats,
+                            mealStats: state.selectedIntervalForMealStats == .day ?
                                 state.hourlyMealStats : state.dailyMealStats,
                             state: state
                         )

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

@@ -7,7 +7,7 @@ import SwiftUI
 /// allowing users to adjust the time interval and scroll through historical data.
 struct BolusStatsView: View {
     /// The selected time interval for displaying statistics.
-    @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
+    @Binding var selectedInterval: Stat.StateModel.StatsTimeInterval
     /// The list of bolus statistics data.
     let bolusStats: [BolusStats]
     /// The state model containing cached statistics data.
@@ -19,12 +19,14 @@ struct BolusStatsView: View {
     @State private var selectedDate: Date?
     /// The calculated bolus insulin averages for the visible range.
     @State private var currentAverages: (manual: Double, smb: Double, external: Double) = (0, 0, 0)
+    /// The calculated total bolus insulin for the visible range.
+    @State private var currentTotal: Double = 0
     /// Timer to throttle updates when scrolling.
     @State private var updateTimer = Stat.UpdateTimer()
 
     /// Computes the visible date range based on the current scroll position.
     private var visibleDateRange: (start: Date, end: Date) {
-        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedDuration)
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
     }
 
     /// Retrieves the bolus statistic for a given date.
@@ -32,13 +34,14 @@ struct BolusStatsView: View {
     /// - Returns: The `BolusStats` object if available, otherwise `nil`.
     private func getBolusForDate(_ date: Date) -> BolusStats? {
         bolusStats.first { stat in
-            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration)
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval)
         }
     }
 
     /// Updates the bolus insulin averages based on the visible date range.
-    private func updateAverages() {
+    private func updateCalculatedValues() {
         currentAverages = state.getCachedBolusAverages(for: visibleDateRange)
+        currentTotal = state.getCachedBolusTotals(for: visibleDateRange)
     }
 
     /// A view displaying the statistics summary including bolus insulin averages.
@@ -46,19 +49,39 @@ struct BolusStatsView: View {
         HStack {
             Grid(alignment: .leading) {
                 GridRow {
-                    Text("Manual:")
+                    if selectedInterval != .day {
+                        Text("ø") + Text("\u{00A0}") + Text("Manual:")
+                    } else {
+                        Text("Manual:")
+                    }
                     Text(currentAverages.manual.formatted(.number.precision(.fractionLength(1))))
-                    Text("U")
+                        + Text("\u{00A0}") + Text("U")
                 }
                 GridRow {
-                    Text("SMB:")
+                    if selectedInterval != .day {
+                        Text("ø") + Text("\u{00A0}") + Text("SMB:")
+                    } else {
+                        Text("SMB:")
+                    }
                     Text(currentAverages.smb.formatted(.number.precision(.fractionLength(1))))
-                    Text("U")
+                        + Text("\u{00A0}") + Text("U")
                 }
                 GridRow {
-                    Text("External:")
+                    if selectedInterval != .day {
+                        Text("ø") + Text("\u{00A0}") + Text("External:")
+                    } else {
+                        Text("External:")
+                    }
                     Text(currentAverages.external.formatted(.number.precision(.fractionLength(1))))
-                    Text("U")
+                        + Text("\u{00A0}") + Text("U")
+                }
+                Divider()
+                GridRow {
+                    Text("Total:")
+                    Text(
+                        currentTotal.formatted(.number.precision(.fractionLength(1)))
+                    )
+                        + Text("\u{00A0}") + Text("U")
                 }
             }
             .font(.headline)
@@ -67,7 +90,7 @@ struct BolusStatsView: View {
 
             Text(
                 StatChartUtils
-                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedInterval)
             )
             .font(.callout)
             .foregroundStyle(.secondary)
@@ -88,18 +111,18 @@ struct BolusStatsView: View {
             }
         }
         .onAppear {
-            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
-            updateAverages()
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+            updateCalculatedValues()
         }
         .onChange(of: scrollPosition) {
             updateTimer.scheduleUpdate {
-                updateAverages()
+                updateCalculatedValues()
             }
         }
-        .onChange(of: selectedDuration) {
+        .onChange(of: selectedInterval) {
             Task {
-                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
-                updateAverages()
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
+                updateCalculatedValues()
             }
         }
     }
@@ -110,43 +133,57 @@ struct BolusStatsView: View {
             ForEach(bolusStats) { stat in
                 // Total Bolus Bar
                 BarMark(
-                    x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                     y: .value("Amount", stat.manualBolus)
                 )
                 .foregroundStyle(by: .value("Type", "Manual"))
                 .position(by: .value("Type", "Boluses"))
                 .opacity(
                     selectedDate.map { date in
-                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                     } ?? 1
                 )
 
                 // Carb Bolus Bar
                 BarMark(
-                    x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                     y: .value("Amount", stat.smb)
                 )
                 .foregroundStyle(by: .value("Type", "SMB"))
                 .position(by: .value("Type", "Boluses"))
                 .opacity(
                     selectedDate.map { date in
-                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                     } ?? 1
                 )
                 // Correction Bolus Bar
                 BarMark(
-                    x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                     y: .value("Amount", stat.external)
                 )
                 .foregroundStyle(by: .value("Type", "External"))
                 .position(by: .value("Type", "Boluses"))
                 .opacity(
                     selectedDate.map { date in
-                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                     } ?? 1
                 )
             }
 
+            // Dummy PointMark to force SwiftCharts to render a visible domain of 00:00-23:59
+            // i.e. single day from midnight to midnight
+            if selectedInterval == .day {
+                let calendar = Calendar.current
+                let midnight = calendar.startOfDay(for: Date())
+                let nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
+
+                PointMark(
+                    x: .value("Time", nextMidnight),
+                    y: .value("Dummy", 0)
+                )
+                .opacity(0) // ensures dummy ChartContent is hidden
+            }
+
             // Selection popover outside of the ForEach loop!
             if let selectedDate, let selectedBolus = getBolusForDate(selectedDate)
             {
@@ -155,11 +192,11 @@ struct BolusStatsView: View {
                 )
                 .foregroundStyle(.secondary.opacity(0.5))
                 .annotation(
-                    position: .top,
+                    position: .automatic,
                     spacing: 0,
                     overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
                 ) {
-                    BolusSelectionPopover(date: selectedDate, bolus: selectedBolus, selectedDuration: selectedDuration)
+                    BolusSelectionPopover(date: selectedDate, bolus: selectedBolus, selectedInterval: selectedInterval)
                 }
             }
         }
@@ -195,53 +232,54 @@ struct BolusStatsView: View {
             }
         }
         .chartXAxis {
-            AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .day ? .hour : .day)) { value in
+            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 selectedDuration {
+                    switch selectedInterval {
                     case .day:
                         if hour % 6 == 0 { // Show only every 6 hours
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .month:
                         if day % 3 == 0 { // Only show every 3rd day
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     default:
-                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                             .font(.footnote)
                         AxisGridLine()
                     }
                 }
             }
         }
-        .chartScrollableAxes(.horizontal)
         .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollableAxes(.horizontal)
         .chartScrollPosition(x: $scrollPosition)
         .chartScrollTargetBehavior(
             .valueAligned(
-                matching: selectedDuration == .day ?
+                matching:
+                selectedInterval == .day ?
                     DateComponents(minute: 0) : // Align to next hour for Day view
                     DateComponents(hour: 0), // Align to start of day for other views
                 majorAlignment: .matching(
-                    StatChartUtils.alignmentComponents(for: selectedDuration)
+                    StatChartUtils.alignmentComponents(for: selectedInterval)
                 )
             )
         )
-        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedDuration))
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
         .frame(height: 250)
     }
 }
@@ -249,10 +287,10 @@ struct BolusStatsView: View {
 private struct BolusSelectionPopover: View {
     let date: Date
     let bolus: BolusStats
-    let selectedDuration: Stat.StateModel.StatsTimeInterval
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
 
     private var timeText: String {
-        if selectedDuration == .day {
+        if selectedInterval == .day {
             let hour = Calendar.current.component(.hour, from: date)
             return "\(hour):00-\(hour + 1):00"
         } else {
@@ -285,6 +323,14 @@ private struct BolusSelectionPopover: View {
                         .gridColumnAlignment(.trailing)
                     Text("U")
                 }
+                Divider()
+                GridRow {
+                    Text("Total:")
+                    Text(
+                        (bolus.manualBolus + bolus.smb + bolus.external).formatted(.number.precision(.fractionLength(1)))
+                    )
+                    Text("U")
+                }
             }
             .font(.headline.bold())
         }

+ 52 - 44
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -7,7 +7,7 @@ import SwiftUI
 /// and scroll through historical data.
 struct TotalDailyDoseChart: View {
     /// The selected time interval for displaying statistics.
-    @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
+    @Binding var selectedInterval: Stat.StateModel.StatsTimeInterval
     /// The list of TDD statistics data.
     let tddStats: [TDDStats]
     /// The state model containing cached statistics data.
@@ -26,7 +26,7 @@ struct TotalDailyDoseChart: View {
 
     /// Computes the visible date range based on the current scroll position.
     private var visibleDateRange: (start: Date, end: Date) {
-        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedDuration)
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
     }
 
     /// Retrieves the TDD statistic for a given date.
@@ -34,7 +34,7 @@ struct TotalDailyDoseChart: View {
     /// - Returns: The `TDDStats` object if available, otherwise `nil`.
     private func getTDDForDate(_ date: Date) -> TDDStats? {
         tddStats.first { stat in
-            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration)
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval)
         }
     }
 
@@ -65,23 +65,23 @@ struct TotalDailyDoseChart: View {
             }
         }
         .onAppear {
-            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
             updateAverages()
             updateTotalDoses()
         }
         .onChange(of: scrollPosition) {
             updateTimer.scheduleUpdate {
                 updateAverages()
-                if selectedDuration == .day {
+                if selectedInterval == .day {
                     updateTotalDoses()
                 }
             }
         }
-        .onChange(of: selectedDuration) {
+        .onChange(of: selectedInterval) {
             Task {
-                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
                 updateAverages()
-                if selectedDuration == .day {
+                if selectedInterval == .day {
                     updateTotalDoses()
                 }
             }
@@ -91,39 +91,33 @@ struct TotalDailyDoseChart: View {
     /// A view displaying the statistics summary including average TDD.
     private var statsView: some View {
         HStack {
-            if selectedDuration == .day {
+            if selectedInterval == .day {
                 Grid(alignment: .leading) {
                     GridRow {
-                        Text("Total:")
-                            .font(.headline)
-                        Text(sumOfHourlyDoses.formatted(.number.precision(.fractionLength(1))))
-                            .font(.headline)
-                        Text("U")
-                            .font(.headline)
-                    }
-                    GridRow {
                         Text("Average:")
-                            .font(.headline)
                         Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
-                            .font(.headline)
-                        Text("U")
-                            .font(.headline)
+                            + Text("\u{00A0}") + Text("U")
+                    }
+                    GridRow {
+                        Text("Total:")
+                        Text(sumOfHourlyDoses.formatted(.number.precision(.fractionLength(1))))
+                            + Text("\u{00A0}") + Text("U")
                     }
                 }
                 .font(.headline)
             } else {
-                Text("Average:")
-                    .font(.headline)
-                Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
-                    .font(.headline)
-                Text("U")
-                    .font(.headline)
+                Group {
+                    Text("Average:")
+                    Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
+                        + Text("\u{00A0}") + Text("U")
+                }
+                .font(.headline)
             }
             Spacer()
 
             Text(
                 StatChartUtils
-                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedInterval)
             )
             .font(.callout)
             .foregroundStyle(.secondary)
@@ -135,13 +129,13 @@ struct TotalDailyDoseChart: View {
         Chart {
             ForEach(tddStats) { stat in
                 BarMark(
-                    x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                     y: .value("Amount", stat.amount)
                 )
                 .foregroundStyle(Color.insulin)
                 .opacity(
                     selectedDate.map { date in
-                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                     } ?? 1
                 )
             }
@@ -159,9 +153,23 @@ struct TotalDailyDoseChart: View {
                     spacing: 0,
                     overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
                 ) {
-                    TDDSelectionPopover(date: selectedDate, tdd: selectedTDD, selectedDuration: selectedDuration)
+                    TDDSelectionPopover(date: selectedDate, tdd: selectedTDD, selectedInterval: selectedInterval)
                 }
             }
+
+            // Dummy PointMark to force SwiftCharts to render a visible domain of 00:00-23:59
+            // i.e. single day from midnight to midnight
+            if selectedInterval == .day {
+                let calendar = Calendar.current
+                let midnight = calendar.startOfDay(for: Date())
+                let nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
+
+                PointMark(
+                    x: .value("Time", nextMidnight),
+                    y: .value("Dummy", 0)
+                )
+                .opacity(0) // ensures dummy ChartContent is hidden
+            }
         }
         .chartYAxis {
             AxisMarks(position: .trailing) { value in
@@ -175,33 +183,33 @@ struct TotalDailyDoseChart: View {
             }
         }
         .chartXAxis {
-            AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .day ? .hour : .day)) { value in
+            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 selectedDuration {
+                    switch selectedInterval {
                     case .day:
                         if hour % 6 == 0 { // Show only every 6 hours
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .month:
                         if day % 3 == 0 { // Only show every 3rd day
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     default:
-                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                             .font(.footnote)
                         AxisGridLine()
                     }
@@ -213,31 +221,31 @@ struct TotalDailyDoseChart: View {
         .chartScrollPosition(x: $scrollPosition)
         .chartScrollTargetBehavior(
             .valueAligned(
-                matching: selectedDuration == .day ?
+                matching: selectedInterval == .day ?
                     DateComponents(minute: 0) :
                     DateComponents(hour: 0),
-                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedDuration))
+                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedInterval))
             )
         )
-        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedDuration))
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
         .frame(height: 250)
     }
 }
 
 /// A popover view displaying TDD (Total Daily Dose) for a given time period.
-/// Shows the insulin amount in units (U) for an hourly or daily interval, depending on `selectedDuration`.
+/// Shows the insulin amount in units (U) for an hourly or daily interval, depending on `selectedInterval`.
 ///
 /// - Parameters:
 ///   - date: The reference date for determining the displayed time range.
 ///   - tdd: The TDDStats containing insulin usage data.
-///   - selectedDuration: The selected time interval (hourly or daily).
+///   - selectedInterval: The selected time interval (hourly or daily).
 private struct TDDSelectionPopover: View {
     let date: Date
     let tdd: TDDStats
-    let selectedDuration: Stat.StateModel.StatsTimeInterval
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
 
     private var timeText: String {
-        if selectedDuration == .day {
+        if selectedInterval == .day {
             let hour = Calendar.current.component(.hour, from: date)
             return "\(hour):00-\(hour + 1):00"
         } else {

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

@@ -3,7 +3,7 @@ import SwiftUI
 
 struct LoopBarChartView: View {
     let loopStatRecords: [LoopStatRecord]
-    let selectedDuration: Stat.StateModel.StatsTimeIntervalWithToday
+    let selectedInterval: Stat.StateModel.StatsTimeIntervalWithToday
     let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
 
     var body: some View {
@@ -58,7 +58,7 @@ struct LoopBarChartView: View {
         medianInterval: Double
     )) -> String {
         if data.category == .successfulLoop {
-            switch selectedDuration {
+            switch selectedInterval {
             case .day,
                  .today:
                 return "\(data.count) " + String(localized: "Loops")
@@ -69,7 +69,7 @@ struct LoopBarChartView: View {
             }
         } else {
             // For Glucose Count, show different text based on duration
-            switch selectedDuration {
+            switch selectedInterval {
             case .day,
                  .today:
                 return "\(data.count) " + String(localized: "Readings")

+ 42 - 28
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -7,7 +7,7 @@ import SwiftUI
 /// allowing users to adjust the time interval and scroll through historical data.
 struct MealStatsView: View {
     /// The selected time interval for displaying statistics.
-    @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
+    @Binding var selectedInterval: Stat.StateModel.StatsTimeInterval
     /// The list of meal statistics data.
     let mealStats: [MealStats]
     /// The state model containing cached statistics data.
@@ -24,7 +24,7 @@ struct MealStatsView: View {
 
     /// Computes the visible date range based on the current scroll position.
     private var visibleDateRange: (start: Date, end: Date) {
-        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedDuration)
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedInterval)
     }
 
     /// Retrieves the meal statistic for a given date.
@@ -32,7 +32,7 @@ struct MealStatsView: View {
     /// - Returns: The `MealStats` object if available, otherwise `nil`.
     private func getMealForDate(_ date: Date) -> MealStats? {
         mealStats.first { stat in
-            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration)
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval)
         }
     }
 
@@ -48,18 +48,18 @@ struct MealStatsView: View {
                 GridRow {
                     Text("Carbs:")
                     Text(currentAverages.carbs.formatted(.number.precision(.fractionLength(1))))
-                    Text("g")
+                        + Text("\u{00A0}") + Text("g")
                 }
                 if state.useFPUconversion {
                     GridRow {
                         Text("Fat:")
                         Text(currentAverages.fat.formatted(.number.precision(.fractionLength(1))))
-                        Text("g")
+                            + Text("\u{00A0}") + Text("g")
                     }
                     GridRow {
                         Text("Protein:")
                         Text(currentAverages.protein.formatted(.number.precision(.fractionLength(1))))
-                        Text("g")
+                            + Text("\u{00A0}") + Text("g")
                     }
                 }
             }
@@ -69,7 +69,7 @@ struct MealStatsView: View {
 
             Text(
                 StatChartUtils
-                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedInterval)
             )
             .font(.callout)
             .foregroundStyle(.secondary)
@@ -90,7 +90,7 @@ struct MealStatsView: View {
             }
         }
         .onAppear {
-            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
             updateAverages()
         }
         .onChange(of: scrollPosition) {
@@ -98,9 +98,9 @@ struct MealStatsView: View {
                 updateAverages()
             }
         }
-        .onChange(of: selectedDuration) {
+        .onChange(of: selectedInterval) {
             Task {
-                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedInterval)
                 updateAverages()
             }
         }
@@ -112,39 +112,39 @@ struct MealStatsView: View {
             ForEach(mealStats) { stat in
                 // Carbs Bar (bottom)
                 BarMark(
-                    x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                    x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                     y: .value("Amount", stat.carbs)
                 )
                 .foregroundStyle(by: .value("Type", "Carbs"))
                 .position(by: .value("Type", "Macros"))
                 .opacity(
                     selectedDate.map { date in
-                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                     } ?? 1
                 )
                 if state.useFPUconversion {
                     // Fat Bar (middle)
                     BarMark(
-                        x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                        x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                         y: .value("Amount", stat.fat)
                     )
                     .foregroundStyle(by: .value("Type", "Fat"))
                     .position(by: .value("Type", "Macros"))
                     .opacity(
                         selectedDate.map { date in
-                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                         } ?? 1
                     )
                     // Protein Bar (top)
                     BarMark(
-                        x: .value("Date", stat.date, unit: selectedDuration == .day ? .hour : .day),
+                        x: .value("Date", stat.date, unit: selectedInterval == .day ? .hour : .day),
                         y: .value("Amount", stat.protein)
                     )
                     .foregroundStyle(by: .value("Type", "Protein"))
                     .position(by: .value("Type", "Macros"))
                     .opacity(
                         selectedDate.map { date in
-                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedInterval) ? 1 : 0.3
                         } ?? 1
                     )
                 }
@@ -166,11 +166,25 @@ struct MealStatsView: View {
                     MealSelectionPopover(
                         date: selectedDate,
                         meal: selectedMeal,
-                        selectedDuration: selectedDuration,
+                        selectedInterval: selectedInterval,
                         isFpuEnabled: state.useFPUconversion
                     )
                 }
             }
+
+            // Dummy PointMark to force SwiftCharts to render a visible domain of 00:00-23:59
+            // i.e. single day from midnight to midnight
+            if selectedInterval == .day {
+                let calendar = Calendar.current
+                let midnight = calendar.startOfDay(for: Date())
+                let nextMidnight = calendar.date(byAdding: .day, value: 1, to: midnight)!
+
+                PointMark(
+                    x: .value("Time", nextMidnight),
+                    y: .value("Dummy", 0)
+                )
+                .opacity(0) // ensures dummy ChartContent is hidden
+            }
         }
         .chartForegroundStyleScale([
             "Carbs": Color.orange,
@@ -204,33 +218,33 @@ struct MealStatsView: View {
             }
         }
         .chartXAxis {
-            AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .day ? .hour : .day)) { value in
+            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 selectedDuration {
+                    switch selectedInterval {
                     case .day:
                         if hour % 6 == 0 { // Show only every 6 hours
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .month:
                         if day % 3 == 0 { // Only show every 3rd day
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .total:
                         // Only show every other month
                         if day == 1 && Calendar.current.component(.month, from: date) % 2 == 1 {
-                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     default:
-                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedInterval), centered: true)
                             .font(.footnote)
                         AxisGridLine()
                     }
@@ -242,13 +256,13 @@ struct MealStatsView: View {
         .chartScrollPosition(x: $scrollPosition)
         .chartScrollTargetBehavior(
             .valueAligned(
-                matching: selectedDuration == .day ?
+                matching: selectedInterval == .day ?
                     DateComponents(minute: 0) :
                     DateComponents(hour: 0),
-                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedDuration))
+                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedInterval))
             )
         )
-        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedDuration))
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedInterval))
         .frame(height: 250)
     }
 }
@@ -266,12 +280,12 @@ private struct MealSelectionPopover: View {
     // The meal statistics to display
     let meal: MealStats
     // The selected duration in the time picker
-    let selectedDuration: Stat.StateModel.StatsTimeInterval
+    let selectedInterval: Stat.StateModel.StatsTimeInterval
     // Setting controlling whether to display fat and protein
     let isFpuEnabled: Bool
 
     private var timeText: String {
-        if selectedDuration == .day {
+        if selectedInterval == .day {
             let hour = Calendar.current.component(.hour, from: date)
             return "\(hour):00-\(hour + 1):00"
         } else {