GlucoseDistributionChart.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import Charts
  2. import SwiftUI
  3. struct GlucoseDistributionChart: View {
  4. @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
  5. let state: Stat.StateModel
  6. let glucose: [GlucoseStored]
  7. let highLimit: Decimal
  8. let lowLimit: Decimal
  9. let units: GlucoseUnits
  10. let glucoseRangeStats: [Stat.GlucoseRangeStats]
  11. @State private var scrollPosition = Date()
  12. @State private var selection: Date?
  13. private var visibleDateRange: (start: Date, end: Date) {
  14. let calendar = Calendar.current
  15. return (
  16. calendar.startOfDay(for: scrollPosition),
  17. calendar.startOfDay(for: scrollPosition).addingTimeInterval(24 * 3600)
  18. )
  19. }
  20. private func formatVisibleDateRange() -> String {
  21. let calendar = Calendar.current
  22. let today = Date()
  23. switch selectedDuration {
  24. case .Day:
  25. let isToday = calendar.isDate(scrollPosition, inSameDayAs: today)
  26. let isYesterday = calendar.isDate(
  27. scrollPosition,
  28. inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!
  29. )
  30. return if isToday {
  31. "Today"
  32. } else if isYesterday {
  33. "Yesterday"
  34. } else {
  35. scrollPosition.formatted(date: .numeric, time: .omitted)
  36. }
  37. case .Week:
  38. let weekStart = calendar
  39. .date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: scrollPosition))!
  40. let weekEnd = calendar.date(byAdding: .day, value: 6, to: weekStart)!
  41. return "\(weekStart.formatted(date: .numeric, time: .omitted)) - \(weekEnd.formatted(date: .numeric, time: .omitted))"
  42. case .Month:
  43. let monthStart = calendar.date(
  44. from: calendar.dateComponents([.year, .month], from: scrollPosition)
  45. )!
  46. let monthEnd = calendar.date(byAdding: .month, value: 1, to: monthStart)!
  47. let lastDayOfMonth = calendar.date(byAdding: .day, value: -1, to: monthEnd)!
  48. return "\(monthStart.formatted(date: .numeric, time: .omitted)) - \(lastDayOfMonth.formatted(date: .numeric, time: .omitted))"
  49. case .Total:
  50. let endDate = scrollPosition
  51. let startDate = calendar.date(byAdding: .month, value: -3, to: endDate)!
  52. return "\(startDate.formatted(date: .numeric, time: .omitted)) - \(endDate.formatted(date: .numeric, time: .omitted))"
  53. }
  54. }
  55. var body: some View {
  56. VStack(alignment: .leading, spacing: 8) {
  57. HStack(alignment: .top) {
  58. Text("Glucose Distribution")
  59. .font(.headline)
  60. Spacer()
  61. Text(formatVisibleDateRange())
  62. .font(.subheadline)
  63. .foregroundStyle(.secondary)
  64. }
  65. Chart(glucoseRangeStats) { range in
  66. ForEach(range.values, id: \.hour) { value in
  67. AreaMark(
  68. x: .value("Hour", Calendar.current.dateForChartHour(value.hour)),
  69. y: .value("Count", value.count),
  70. stacking: .normalized
  71. )
  72. .foregroundStyle(by: .value("Range", range.name))
  73. }
  74. }
  75. .chartForegroundStyleScale([
  76. "<54": .purple.opacity(0.7),
  77. "54-70": .red.opacity(0.7),
  78. "70-140": .green,
  79. "140-180": .green.opacity(0.7),
  80. "180-200": .yellow.opacity(0.7),
  81. "200-220": .orange.opacity(0.7),
  82. ">220": .orange.opacity(0.8)
  83. ])
  84. .chartYAxis {
  85. AxisMarks(position: .trailing)
  86. }
  87. .chartXAxis {
  88. AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
  89. AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
  90. AxisGridLine()
  91. }
  92. }
  93. .chartScrollableAxes(.horizontal)
  94. .chartScrollPosition(x: $scrollPosition)
  95. .chartXSelection(value: $selection)
  96. .chartXVisibleDomain(length: 24 * 3600)
  97. .chartScrollTargetBehavior(
  98. .valueAligned(
  99. matching: DateComponents(minute: 0),
  100. majorAlignment: .matching(DateComponents(hour: 0))
  101. )
  102. )
  103. .frame(height: 200)
  104. }
  105. .onChange(of: scrollPosition) {
  106. state.glucoseScrollPosition = scrollPosition
  107. state.updateDisplayedStats(for: .distribution)
  108. }
  109. }
  110. }