|
|
@@ -4,30 +4,31 @@ import SwiftUI
|
|
|
struct BolusStatsView: View {
|
|
|
@Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
|
|
|
let bolusStats: [BolusStats]
|
|
|
- let calculateAverages: @Sendable(Date, Date) async -> (manual: Double, smb: Double, external: 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 currentAverages: (manual: Double, smb: Double, external: Double) = (0, 0, 0)
|
|
|
@State private var updateTimer = Stat.UpdateTimer()
|
|
|
- @State private var isScrolling = false
|
|
|
|
|
|
+ /// Returns the time interval length for the visible domain based on selected duration
|
|
|
private var visibleDomainLength: TimeInterval {
|
|
|
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
|
|
|
+ case .Total: return 90 * 24 * 3600 // Three months in seconds
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Calculates the visible date range based on scroll position and domain length
|
|
|
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)
|
|
|
}
|
|
|
|
|
|
+ /// Returns the appropriate date format style based on the selected time interval
|
|
|
private var dateFormat: Date.FormatStyle {
|
|
|
switch selectedDuration {
|
|
|
case .Day:
|
|
|
@@ -41,86 +42,115 @@ struct BolusStatsView: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Returns DateComponents for aligning dates based on the selected duration
|
|
|
private var alignmentComponents: DateComponents {
|
|
|
switch selectedDuration {
|
|
|
case .Day:
|
|
|
- return DateComponents(hour: 0) // Align to start of day
|
|
|
+ return DateComponents(hour: 0) // Align to midnight
|
|
|
case .Week:
|
|
|
- return DateComponents(weekday: 2) // 2 = Monday in Calendar
|
|
|
+ return DateComponents(weekday: 2) // Monday is weekday 2
|
|
|
case .Month,
|
|
|
.Total:
|
|
|
- return DateComponents(day: 1) // Align to first day of month
|
|
|
+ return DateComponents(day: 1) // First day of month
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Returns bolus statistics for a specific date
|
|
|
private func getBolusForDate(_ date: Date) -> BolusStats? {
|
|
|
bolusStats.first { stat in
|
|
|
Calendar.current.isDate(stat.date, inSameDayAs: date)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Updates the current averages for bolus insulin based on the visible date range
|
|
|
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
|
|
|
- }
|
|
|
- }
|
|
|
+ currentAverages = state.getCachedBolusAverages(for: visibleDateRange)
|
|
|
}
|
|
|
|
|
|
- private func formatVisibleDateRange(showTimeRange: Bool = false) -> String {
|
|
|
+ /// Formats the visible date range into a human-readable string
|
|
|
+ private func formatVisibleDateRange() -> String {
|
|
|
let start = visibleDateRange.start
|
|
|
let end = visibleDateRange.end
|
|
|
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 {
|
|
|
- return "\(start.formatted(.dateTime.month().day())), \(timeRange)"
|
|
|
+ endDateText = end.formatted(.dateTime.day().month())
|
|
|
}
|
|
|
|
|
|
- default:
|
|
|
- return "\(start.formatted(.dateTime.month().day())) - \(end.formatted(.dateTime.month().day()))"
|
|
|
+ // 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()))"
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
+ private func getInitialScrollPosition() -> Date {
|
|
|
+ let calendar = Calendar.current
|
|
|
+ let now = Date()
|
|
|
+
|
|
|
+ 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 {
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
statsView
|
|
|
-
|
|
|
chartsView
|
|
|
}
|
|
|
-
|
|
|
.onAppear {
|
|
|
+ scrollPosition = getInitialScrollPosition()
|
|
|
updateAverages()
|
|
|
}
|
|
|
.onChange(of: scrollPosition) {
|
|
|
- isScrolling = true
|
|
|
updateTimer.scheduleUpdate {
|
|
|
updateAverages()
|
|
|
- isScrolling = false
|
|
|
}
|
|
|
}
|
|
|
.onChange(of: selectedDuration) {
|
|
|
- updateAverages()
|
|
|
- scrollPosition = Date()
|
|
|
+ Task {
|
|
|
+ scrollPosition = getInitialScrollPosition()
|
|
|
+ updateAverages()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -167,36 +197,37 @@ struct BolusStatsView: View {
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
- Text(formatVisibleDateRange(showTimeRange: isScrolling))
|
|
|
+ Text(formatVisibleDateRange())
|
|
|
.font(.subheadline)
|
|
|
.foregroundStyle(.secondary)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private var chartsView: some View {
|
|
|
- Chart {
|
|
|
- ForEach(bolusStats) { stat in
|
|
|
- // Manual Bolus (Bottom)
|
|
|
- BarMark(
|
|
|
- x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
|
|
|
- y: .value("Amount", stat.manualBolus)
|
|
|
- )
|
|
|
- .foregroundStyle(by: .value("Type", "Manual"))
|
|
|
+ Chart(bolusStats) { stat in
|
|
|
+ // Total Bolus Bar
|
|
|
+ BarMark(
|
|
|
+ x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
|
|
|
+ y: .value("Amount", stat.manualBolus)
|
|
|
+ )
|
|
|
+ .foregroundStyle(by: .value("Type", "Manual"))
|
|
|
+ .position(by: .value("Type", "Manual"))
|
|
|
|
|
|
- // SMB (Middle)
|
|
|
- BarMark(
|
|
|
- x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
|
|
|
- y: .value("Amount", stat.smb)
|
|
|
- )
|
|
|
- .foregroundStyle(by: .value("Type", "SMB"))
|
|
|
+ // Carb Bolus Bar
|
|
|
+ BarMark(
|
|
|
+ x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
|
|
|
+ y: .value("Amount", stat.smb)
|
|
|
+ )
|
|
|
+ .foregroundStyle(by: .value("Type", "SMB"))
|
|
|
+ .position(by: .value("Type", "SMB"))
|
|
|
|
|
|
- // External (Top)
|
|
|
- BarMark(
|
|
|
- x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
|
|
|
- y: .value("Amount", stat.external)
|
|
|
- )
|
|
|
- .foregroundStyle(by: .value("Type", "External"))
|
|
|
- }
|
|
|
+ // Correction Bolus Bar
|
|
|
+ BarMark(
|
|
|
+ x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
|
|
|
+ y: .value("Amount", stat.external)
|
|
|
+ )
|
|
|
+ .foregroundStyle(by: .value("Type", "External"))
|
|
|
+ .position(by: .value("Type", "External"))
|
|
|
|
|
|
if let selectedDate,
|
|
|
let selectedBolus = getBolusForDate(selectedDate)
|
|
|
@@ -238,7 +269,7 @@ struct BolusStatsView: View {
|
|
|
|
|
|
switch selectedDuration {
|
|
|
case .Day:
|
|
|
- if hour % 6 == 0 { // Show only every 6 hours (0, 6, 12, 18)
|
|
|
+ if hour % 6 == 0 { // Show only every 6 hours
|
|
|
AxisValueLabel(format: dateFormat, centered: true)
|
|
|
AxisGridLine()
|
|
|
}
|
|
|
@@ -248,6 +279,7 @@ struct BolusStatsView: View {
|
|
|
AxisGridLine()
|
|
|
}
|
|
|
case .Total:
|
|
|
+ // Only show January, April, July, October
|
|
|
if day == 1 && Calendar.current.component(.month, from: date) % 3 == 1 {
|
|
|
AxisValueLabel(format: dateFormat, centered: true)
|
|
|
AxisGridLine()
|
|
|
@@ -267,7 +299,9 @@ struct BolusStatsView: View {
|
|
|
matching: selectedDuration == .Day ?
|
|
|
DateComponents(minute: 0) : // Align to next hour for Day view
|
|
|
DateComponents(hour: 0), // Align to start of day for other views
|
|
|
- majorAlignment: .matching(alignmentComponents)
|
|
|
+ majorAlignment: .matching(
|
|
|
+ alignmentComponents
|
|
|
+ )
|
|
|
)
|
|
|
)
|
|
|
.chartXVisibleDomain(length: visibleDomainLength)
|