瀏覽代碼

meal chart

polscm32 aka Marvout 1 年之前
父節點
當前提交
3845b4e092

+ 106 - 65
FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift

@@ -15,33 +15,35 @@ struct MealStats: Identifiable {
 }
 }
 
 
 extension Stat.StateModel {
 extension Stat.StateModel {
-    /// Initializes and fetches meal statistics
+    /// Sets up meal statistics by fetching and processing meal data
     ///
     ///
     /// This function:
     /// This function:
-    /// 1. Fetches carbohydrate records from CoreData
-    /// 2. Groups and processes the records into meal statistics
-    /// 3. Updates the mealStats array on the main thread
+    /// 1. Fetches hourly and daily meal statistics asynchronously
+    /// 2. Updates the state model with the fetched statistics on the main actor
+    /// 3. Calculates and caches initial daily averages
     func setupMealStats() {
     func setupMealStats() {
         Task {
         Task {
-            let stats = await fetchMealStats()
+            let (hourly, daily) = await fetchMealStats()
+
             await MainActor.run {
             await MainActor.run {
-                self.mealStats = stats
+                self.hourlyMealStats = hourly
+                self.dailyMealStats = daily
             }
             }
+
+            // Initially calculate and cache daily averages
+            await calculateAndCacheDailyAverages()
         }
         }
     }
     }
 
 
-    /// Fetches and processes meal statistics for a specific duration
-    /// - Returns: Array of MealStats containing daily meal statistics, sorted by date
+    /// Fetches and processes meal statistics from Core Data
+    /// - Returns: A tuple containing hourly and daily meal statistics arrays
     ///
     ///
     /// This function:
     /// This function:
-    /// 1. Fetches carbohydrate entries from CoreData
-    /// 2. Groups entries by day or hour based on selected duration
+    /// 1. Fetches carbohydrate entries from Core Data
+    /// 2. Groups entries by hour and day
     /// 3. Calculates total macronutrients for each time period
     /// 3. Calculates total macronutrients for each time period
-    ///
-    /// The grouping logic:
-    /// - For Day view: Groups by hour to show meal distribution
-    /// - For other views: Groups by day to show daily totals
-    private func fetchMealStats() async -> [MealStats] {
+    /// 4. Returns the processed statistics as (hourly: [MealStats], daily: [MealStats])
+    private func fetchMealStats() async -> (hourly: [MealStats], daily: [MealStats]) {
         // Fetch CarbEntryStored entries from Core Data
         // Fetch CarbEntryStored entries from Core Data
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             ofType: CarbEntryStored.self,
@@ -53,74 +55,113 @@ extension Stat.StateModel {
         )
         )
 
 
         return await mealTaskContext.perform {
         return await mealTaskContext.perform {
-            // Safely unwrap the fetched results, return empty array if nil
-            guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+            // Safely unwrap the fetched results, return empty arrays if nil
+            guard let fetchedResults = results as? [CarbEntryStored] else { return ([], []) }
 
 
             let calendar = Calendar.current
             let calendar = Calendar.current
 
 
-            // Group entries by day or hour depending on selected duration
-            let groupedEntries = Dictionary(grouping: fetchedResults) { entry in
-                if self.selectedDurationForMealStats == .Day {
-                    // For Day view, group by hour
-                    let components = calendar.dateComponents([.year, .month, .day, .hour], from: entry.date ?? Date())
-                    return calendar.date(from: components) ?? Date()
-                } else {
-                    // For other views, group by day
-                    return calendar.startOfDay(for: entry.date ?? Date())
-                }
+            // Group entries by hour for hourly statistics
+            let hourlyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                let components = calendar.dateComponents([.year, .month, .day, .hour], from: entry.date ?? Date())
+                return calendar.date(from: components) ?? Date()
             }
             }
 
 
-            // Get all unique dates/hours from the entries
-            let timePoints = groupedEntries.keys.sorted()
-
-            // Calculate statistics for each time point
-            return timePoints.map { timePoint in
-                let entries = groupedEntries[timePoint, default: []]
+            // Group entries by day for daily statistics
+            let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.date ?? Date())
+            }
 
 
-                let carbsTotal = entries.reduce(0.0) { $0 + $1.carbs }
-                let fatTotal = entries.reduce(0.0) { $0 + $1.fat }
-                let proteinTotal = entries.reduce(0.0) { $0 + $1.protein }
+            // Calculate statistics for each hour
+            let hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
+                let entries = hourlyGrouped[timePoint, default: []]
+                return MealStats(
+                    date: timePoint,
+                    carbs: entries.reduce(0.0) { $0 + $1.carbs },
+                    fat: entries.reduce(0.0) { $0 + $1.fat },
+                    protein: entries.reduce(0.0) { $0 + $1.protein }
+                )
+            }
 
 
+            // Calculate statistics for each day
+            let dailyStats = dailyGrouped.keys.sorted().map { timePoint in
+                let entries = dailyGrouped[timePoint, default: []]
                 return MealStats(
                 return MealStats(
                     date: timePoint,
                     date: timePoint,
-                    carbs: carbsTotal,
-                    fat: fatTotal,
-                    protein: proteinTotal
+                    carbs: entries.reduce(0.0) { $0 + $1.carbs },
+                    fat: entries.reduce(0.0) { $0 + $1.fat },
+                    protein: entries.reduce(0.0) { $0 + $1.protein }
                 )
                 )
             }
             }
+
+            return (hourlyStats, dailyStats)
         }
         }
     }
     }
 
 
-    /// Calculates average meal statistics for a specified date range
-    /// - Parameters:
-    ///   - startDate: Start date of the range
-    ///   - endDate: End date of the range
-    /// - Returns: Tuple containing average values for carbs, fat, and protein
+    /// Calculates and caches the daily averages of macronutrients
     ///
     ///
-    /// The calculation process:
-    /// 1. Filters meal records within the date range
-    /// 2. Calculates total values for each macronutrient
-    /// 3. Divides totals by number of records to get averages
-    /// 4. Returns (0,0,0) if no records are found
-    func calculateAverageMealStats(
-        from startDate: Date,
-        to endDate: Date
-    ) async -> (carbs: Double, fat: Double, protein: Double) {
-        let filteredStats = mealStats.filter { stat in
-            stat.date >= startDate && stat.date <= endDate
+    /// This function:
+    /// 1. Groups meal statistics by day
+    /// 2. Calculates average carbs, fat and protein for each day
+    /// 3. Caches the results for later use
+    ///
+    /// This only needs to be called once during subscribe.
+    private func calculateAndCacheDailyAverages() async {
+        let calendar = Calendar.current
+
+        // Calculate averages in context
+        let dailyAverages = await mealTaskContext.perform { [dailyMealStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyMealStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate averages for each day
+            var averages: [Date: (Double, Double, Double)] = [:]
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce((0.0, 0.0, 0.0)) { acc, stat in
+                    (acc.0 + stat.carbs, acc.1 + stat.fat, acc.2 + stat.protein)
+                }
+                let count = Double(stats.count)
+                averages[day] = (total.0 / count, total.1 / count, total.2 / count)
+            }
+            return averages
         }
         }
 
 
-        guard !filteredStats.isEmpty else { return (0, 0, 0) }
+        // Update cache on main thread
+        await MainActor.run {
+            self.dailyAveragesCache = dailyAverages
+        }
+    }
 
 
-        let totalCarbs = filteredStats.reduce(0.0) { $0 + $1.carbs }
-        let totalFat = filteredStats.reduce(0.0) { $0 + $1.fat }
-        let totalProtein = filteredStats.reduce(0.0) { $0 + $1.protein }
-        let count = Double(filteredStats.count)
+    /// Returns the average macronutrient values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: A tuple containing the average carbs, fat and protein values for the date range
+    func getCachedMealAverages(for range: (start: Date, end: Date)) -> (carbs: Double, fat: Double, protein: Double) {
+        return calculateAveragesForDateRange(from: range.start, to: range.end)
+    }
 
 
-        return (
-            carbs: totalCarbs / count,
-            fat: totalFat / count,
-            protein: totalProtein / count
-        )
+    /// Calculates the average macronutrient 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 tuple containing the average carbs, fat and protein values for the date range
+    func calculateAveragesForDateRange(from startDate: Date, to endDate: Date) -> (carbs: Double, fat: Double, protein: Double) {
+        // Filter cached values to only include those within the date range
+        let relevantStats = dailyAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return (0, 0, 0) }
+
+        // Calculate total macronutrients across all days
+        let total = relevantStats.values.reduce((0.0, 0.0, 0.0)) { acc, avg in
+            (acc.0 + avg.0, acc.1 + avg.1, acc.2 + avg.2)
+        }
+
+        // Calculate averages by dividing totals by number of days
+        let count = Double(relevantStats.count)
+
+        return (total.0 / count, total.1 / count, total.2 / count)
     }
     }
 }
 }

+ 5 - 1
FreeAPS/Sources/Modules/Stat/StatStateModel.swift

@@ -49,7 +49,6 @@ extension Stat {
         var glucoseFromPersistence: [GlucoseStored] = []
         var glucoseFromPersistence: [GlucoseStored] = []
         var loopStatRecords: [LoopStatRecord] = []
         var loopStatRecords: [LoopStatRecord] = []
         var groupedLoopStats: [LoopStatsByPeriod] = []
         var groupedLoopStats: [LoopStatsByPeriod] = []
-        var mealStats: [MealStats] = []
         var tddStats: [TDD] = []
         var tddStats: [TDD] = []
         var bolusStats: [BolusStats] = []
         var bolusStats: [BolusStats] = []
         var hourlyStats: [HourlyStats] = []
         var hourlyStats: [HourlyStats] = []
@@ -71,6 +70,11 @@ extension Stat {
         private var monthlyRangeStatsCache: [Date: [GlucoseRangeStats]] = [:]
         private var monthlyRangeStatsCache: [Date: [GlucoseRangeStats]] = [:]
         private var totalRangeStatsCache: [GlucoseRangeStats] = []
         private var totalRangeStatsCache: [GlucoseRangeStats] = []
 
 
+        // Cache for Meal Stats
+        var hourlyMealStats: [MealStats] = []
+        var dailyMealStats: [MealStats] = []
+        var dailyAveragesCache: [Date: (carbs: Double, fat: Double, protein: Double)] = [:]
+
         // Selected Duration for Glucose Stats
         // Selected Duration for Glucose Stats
         var selectedDurationForGlucoseStats: StatsTimeInterval = .Day {
         var selectedDurationForGlucoseStats: StatsTimeInterval = .Day {
             didSet {
             didSet {

+ 7 - 6
FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

@@ -315,11 +315,13 @@ extension Stat {
             StatCard {
             StatCard {
                 switch state.selectedMealChartType {
                 switch state.selectedMealChartType {
                 case .totalMeals:
                 case .totalMeals:
+                    // TODO: -
                     var hasMealData: Bool {
                     var hasMealData: Bool {
-                        state.mealStats.contains { $0.carbs > 0 || $0.fat > 0 || $0.protein > 0 }
+                        state.dailyMealStats.contains { $0.carbs > 0 || $0.fat > 0 || $0.protein > 0 }
                     }
                     }
 
 
-                    if state.mealStats.isEmpty || !hasMealData {
+                    // TODO: -
+                    if state.dailyMealStats.isEmpty || !hasMealData {
                         ContentUnavailableView(
                         ContentUnavailableView(
                             "No Meal Data",
                             "No Meal Data",
                             systemImage: "fork.knife",
                             systemImage: "fork.knife",
@@ -328,10 +330,9 @@ extension Stat {
                     } else {
                     } else {
                         MealStatsView(
                         MealStatsView(
                             selectedDuration: $state.selectedDurationForMealStats,
                             selectedDuration: $state.selectedDurationForMealStats,
-                            mealStats: state.mealStats,
-                            calculateAverages: { start, end in
-                                await state.calculateAverageMealStats(from: start, to: end)
-                            }
+                            mealStats: state.selectedDurationForMealStats == .Day ?
+                                state.hourlyMealStats : state.dailyMealStats,
+                            state: state
                         )
                         )
                     }
                     }
                 case .mealToHypoHyperDistribution:
                 case .mealToHypoHyperDistribution:

+ 143 - 45
FreeAPS/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift

@@ -4,30 +4,49 @@ import SwiftUI
 struct MealStatsView: View {
 struct MealStatsView: View {
     @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
     @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
     let mealStats: [MealStats]
     let mealStats: [MealStats]
-    let calculateAverages: @Sendable(Date, Date) async -> (carbs: Double, fat: Double, protein: Double)
+    let state: Stat.StateModel
 
 
-    @State private var scrollPosition = Date()
+    @State private var scrollPosition = Date() // gets updated in onAppear block
     @State private var selectedDate: Date?
     @State private var selectedDate: Date?
     @State private var currentAverages: (carbs: Double, fat: Double, protein: Double) = (0, 0, 0)
     @State private var currentAverages: (carbs: Double, fat: Double, protein: Double) = (0, 0, 0)
     @State private var updateTimer = Stat.UpdateTimer()
     @State private var updateTimer = Stat.UpdateTimer()
-    @State private var isScrolling = false
 
 
+    /// Returns the time interval length for the visible domain based on selected duration
+    /// - Returns: TimeInterval representing the visible time range in seconds
+    ///
+    /// Time intervals:
+    /// - Day: 24 hours (86400 seconds)
+    /// - Week: 7 days (604800 seconds)
+    /// - Month: 30 days (2592000 seconds)
+    /// - Total: 90 days (7776000 seconds)
     private var visibleDomainLength: TimeInterval {
     private var visibleDomainLength: TimeInterval {
         switch selectedDuration {
         switch selectedDuration {
-        case .Day: return 24 * 3600 // 1 day
-        case .Week: return 7 * 24 * 3600 // 1 week
-        case .Month: return 30 * 24 * 3600 // 1 month
-        case .Total: return 90 * 24 * 3600 // 3 months
+        case .Day: return 24 * 3600 // One day in seconds
+        case .Week: return 7 * 24 * 3600 // One week in seconds
+        case .Month: return 30 * 24 * 3600 // One month in seconds (approximated)
+        case .Total: return 90 * 24 * 3600 // Three months in seconds
         }
         }
     }
     }
 
 
+    /// Calculates the visible date range based on scroll position and domain length
+    /// - Returns: Tuple containing start and end dates of the visible range
+    ///
+    /// The start date is determined by the current scroll position, while the end date
+    /// is calculated by adding the visible domain length to the start date
     private var visibleDateRange: (start: Date, end: Date) {
     private var visibleDateRange: (start: Date, end: Date) {
-        let halfDomain = visibleDomainLength / 2
-        let start = scrollPosition.addingTimeInterval(-halfDomain)
-        let end = scrollPosition.addingTimeInterval(halfDomain)
+        let start = scrollPosition // Current scroll position marks the start
+        let end = start.addingTimeInterval(visibleDomainLength)
         return (start, end)
         return (start, end)
     }
     }
 
 
+    /// Returns the appropriate date format style based on the selected time interval
+    /// - Returns: A Date.FormatStyle configured for the current time interval
+    ///
+    /// Format styles:
+    /// - Day: Shows hour only (e.g. "13")
+    /// - Week: Shows abbreviated weekday (e.g. "Mon")
+    /// - Month: Shows day of month (e.g. "15")
+    /// - Total: Shows abbreviated month (e.g. "Jan")
     private var dateFormat: Date.FormatStyle {
     private var dateFormat: Date.FormatStyle {
         switch selectedDuration {
         switch selectedDuration {
         case .Day:
         case .Day:
@@ -41,86 +60,150 @@ struct MealStatsView: View {
         }
         }
     }
     }
 
 
+    /// Returns DateComponents for aligning dates based on the selected duration
+    /// - Returns: DateComponents configured for the appropriate alignment
+    ///
+    /// This property provides date components for aligning dates in the chart:
+    /// - For Day view: Aligns to start of day (midnight)
+    /// - For Week view: Aligns to Monday (weekday 2)
+    /// - For Month/Total view: Aligns to first day of month
     private var alignmentComponents: DateComponents {
     private var alignmentComponents: DateComponents {
         switch selectedDuration {
         switch selectedDuration {
         case .Day:
         case .Day:
-            return DateComponents(hour: 0) // Align to start of day
+            return DateComponents(hour: 0) // Align to midnight
         case .Week:
         case .Week:
-            return DateComponents(weekday: 2) // 2 = Monday in Calendar
+            return DateComponents(weekday: 2) // Monday is weekday 2 in Calendar
         case .Month,
         case .Month,
              .Total:
              .Total:
-            return DateComponents(day: 1) // Align to first day of month
+            return DateComponents(day: 1) // First day of month
         }
         }
     }
     }
 
 
+    /// Returns meal statistics for a specific date
+    /// - Parameter date: The date to find meal statistics for
+    /// - Returns: MealStats object if found for the given date, nil otherwise
+    ///
+    /// This function searches through the meal statistics array to find the first entry
+    /// that matches the provided date (comparing only the day component, not time).
     private func getMealForDate(_ date: Date) -> MealStats? {
     private func getMealForDate(_ date: Date) -> MealStats? {
         mealStats.first { stat in
         mealStats.first { stat in
             Calendar.current.isDate(stat.date, inSameDayAs: date)
             Calendar.current.isDate(stat.date, inSameDayAs: date)
         }
         }
     }
     }
 
 
+    /// Updates the current averages for macronutrients based on the visible date range
+    ///
+    /// This function:
+    /// - Gets the cached meal averages for the currently visible date range from the state
+    /// - Updates the currentAverages property with the retrieved values (carbs, fat, protein)
     private func updateAverages() {
     private func updateAverages() {
-        Task.detached(priority: .userInitiated) {
-            let dateRange = await MainActor.run { visibleDateRange }
-            let averages = await calculateAverages(dateRange.start, dateRange.end)
-
-            await MainActor.run {
-                currentAverages = averages
-            }
-        }
+        // Get cached averages for visible time window
+        currentAverages = state.getCachedMealAverages(for: visibleDateRange)
     }
     }
 
 
-    private func formatVisibleDateRange(showTimeRange: Bool = false) -> String {
+    /// Formats the visible date range into a human-readable string
+    /// - Returns: A formatted string representing the visible date range
+    ///
+    /// For Day view:
+    /// - Uses relative terms like "Today", "Yesterday", "Tomorrow" when applicable
+    /// - Shows time range in hours and minutes
+    /// - Combines dates if start and end are on the same day
+    ///
+    /// For other views:
+    /// - Uses standard date formatting
+    private func formatVisibleDateRange() -> String {
         let start = visibleDateRange.start
         let start = visibleDateRange.start
         let end = visibleDateRange.end
         let end = visibleDateRange.end
         let calendar = Calendar.current
         let calendar = Calendar.current
+        let today = Date()
 
 
-        switch selectedDuration {
-        case .Day:
-            let today = Date()
-            let isToday = calendar.isDate(start, inSameDayAs: today)
-            let isYesterday = calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!)
+        let timeFormat = start.formatted(.dateTime.hour().minute())
 
 
-            if isToday || isYesterday, !showTimeRange {
-                return isToday ? "Today" : "Yesterday"
-            }
+        // Special handling for Day view with relative dates
+        if selectedDuration == .Day {
+            let startDateText: String
+            let endDateText: String
 
 
-            let timeRange =
-                "\(start.formatted(.dateTime.hour(.twoDigits(amPM: .wide)))) - \(end.formatted(.dateTime.hour(.twoDigits(amPM: .wide))))"
+            // Format start date
+            if calendar.isDate(start, inSameDayAs: today) {
+                startDateText = "Today"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                startDateText = "Yesterday"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                startDateText = "Tomorrow"
+            } else {
+                startDateText = start.formatted(.dateTime.day().month())
+            }
 
 
-            if isToday {
-                return "Today, \(timeRange)"
-            } else if isYesterday {
-                return "Yesterday, \(timeRange)"
+            // Format end date
+            if calendar.isDate(end, inSameDayAs: today) {
+                endDateText = "Today"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                endDateText = "Yesterday"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                endDateText = "Tomorrow"
             } else {
             } else {
-                return "\(start.formatted(.dateTime.month().day())), \(timeRange)"
+                endDateText = end.formatted(.dateTime.day().month())
+            }
+
+            // If start and end are on the same day, show date only once
+            if calendar.isDate(start, inSameDayAs: end) {
+                return "\(startDateText), \(timeFormat) - \(end.formatted(.dateTime.hour().minute()))"
             }
             }
 
 
-        default:
-            return "\(start.formatted(.dateTime.month().day())) - \(end.formatted(.dateTime.month().day()))"
+            return "\(startDateText), \(timeFormat) - \(endDateText), \(end.formatted(.dateTime.hour().minute()))"
+        }
+
+        // Standard format for other views
+        return "\(start.formatted()) - \(end.formatted())"
+    }
+
+    /// Returns the initial scroll position date based on the selected duration
+    /// - Returns: A Date representing where the chart should initially scroll to
+    ///
+    /// This function calculates an appropriate starting scroll position by subtracting
+    /// a time interval from the current date based on the selected duration:
+    /// - For Day view: 1 day before now
+    /// - For Week view: 7 days before now
+    /// - For Month view: 1 month before now
+    /// - For Total view: 3 months before now
+    private func getInitialScrollPosition() -> Date {
+        let calendar = Calendar.current
+        let now = Date()
+
+        // Calculate scroll position based on selected time interval
+        switch selectedDuration {
+        case .Day:
+            return calendar.date(byAdding: .day, value: -1, to: 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)!
         }
         }
     }
     }
 
 
     var body: some View {
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
         VStack(alignment: .leading, spacing: 8) {
             statsView
             statsView
-
             chartsView
             chartsView
         }
         }
 
 
         .onAppear {
         .onAppear {
+            scrollPosition = getInitialScrollPosition()
             updateAverages()
             updateAverages()
         }
         }
         .onChange(of: scrollPosition) {
         .onChange(of: scrollPosition) {
-            isScrolling = true
             updateTimer.scheduleUpdate {
             updateTimer.scheduleUpdate {
                 updateAverages()
                 updateAverages()
-                isScrolling = false
             }
             }
         }
         }
         .onChange(of: selectedDuration) {
         .onChange(of: selectedDuration) {
-            updateAverages()
-            scrollPosition = Date()
+            Task {
+                scrollPosition = getInitialScrollPosition()
+                updateAverages()
+            }
         }
         }
     }
     }
 
 
@@ -167,7 +250,7 @@ struct MealStatsView: View {
 
 
             Spacer()
             Spacer()
 
 
-            Text(formatVisibleDateRange(showTimeRange: isScrolling))
+            Text(formatVisibleDateRange())
                 .font(.subheadline)
                 .font(.subheadline)
                 .foregroundStyle(.secondary)
                 .foregroundStyle(.secondary)
         }
         }
@@ -281,29 +364,43 @@ struct MealStatsView: View {
     }
     }
 }
 }
 
 
+/// A view that displays detailed meal information in a popover
+///
+/// This view shows a formatted display of meal macronutrients including:
+/// - Date of the meal
+/// - Carbohydrates in grams
+/// - Fat in grams
+/// - Protein in grams
 private struct MealSelectionPopover: View {
 private struct MealSelectionPopover: View {
+    // The date when the meal was logged
     let date: Date
     let date: Date
+    // The meal statistics to display
     let meal: MealStats
     let meal: MealStats
 
 
     var body: some View {
     var body: some View {
         VStack(alignment: .leading, spacing: 4) {
         VStack(alignment: .leading, spacing: 4) {
+            // Display formatted date header
             Text(date.formatted(.dateTime.month().day()))
             Text(date.formatted(.dateTime.month().day()))
                 .font(.caption)
                 .font(.caption)
                 .foregroundStyle(.secondary)
                 .foregroundStyle(.secondary)
 
 
+            // Grid layout for macronutrient values
             Grid(alignment: .leading) {
             Grid(alignment: .leading) {
+                // Carbohydrates row
                 GridRow {
                 GridRow {
                     Text("Carbs:")
                     Text("Carbs:")
                     Text(meal.carbs.formatted(.number.precision(.fractionLength(1))))
                     Text(meal.carbs.formatted(.number.precision(.fractionLength(1))))
                         .gridColumnAlignment(.trailing)
                         .gridColumnAlignment(.trailing)
                     Text("g")
                     Text("g")
                 }
                 }
+                // Fat row
                 GridRow {
                 GridRow {
                     Text("Fat:")
                     Text("Fat:")
                     Text(meal.fat.formatted(.number.precision(.fractionLength(1))))
                     Text(meal.fat.formatted(.number.precision(.fractionLength(1))))
                         .gridColumnAlignment(.trailing)
                         .gridColumnAlignment(.trailing)
                     Text("g")
                     Text("g")
                 }
                 }
+                // Protein row
                 GridRow {
                 GridRow {
                     Text("Protein:")
                     Text("Protein:")
                     Text(meal.protein.formatted(.number.precision(.fractionLength(1))))
                     Text(meal.protein.formatted(.number.precision(.fractionLength(1))))
@@ -315,6 +412,7 @@ private struct MealSelectionPopover: View {
         }
         }
         .padding(8)
         .padding(8)
         .background(
         .background(
+            // Add background styling with shadow
             RoundedRectangle(cornerRadius: 8)
             RoundedRectangle(cornerRadius: 8)
                 .fill(Color(.systemBackground))
                 .fill(Color(.systemBackground))
                 .shadow(radius: 2)
                 .shadow(radius: 2)