GlucoseAreaChart.swift 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import Charts
  2. import SwiftUI
  3. struct GlucoseAreaChart: View {
  4. let glucose: [GlucoseStored]
  5. let highLimit: Decimal
  6. let lowLimit: Decimal
  7. let isTodayOrLast24h: Bool
  8. let units: GlucoseUnits
  9. let hourlyStats: [HourlyStats]
  10. var body: some View {
  11. VStack(alignment: .leading, spacing: 8) {
  12. Chart {
  13. if isTodayOrLast24h {
  14. // Single day line chart
  15. ForEach(glucose.sorted(by: { ($0.date ?? Date()) < ($1.date ?? Date()) }), id: \.id) { reading in
  16. LineMark(
  17. x: .value("Time", reading.date ?? Date()),
  18. y: .value("Glucose", Double(reading.glucose))
  19. )
  20. .lineStyle(StrokeStyle(lineWidth: 2))
  21. .foregroundStyle(.blue)
  22. }
  23. } else {
  24. // Statistical view for longer periods
  25. // 10-90 percentile area
  26. ForEach(hourlyStats, id: \.hour) { stats in
  27. AreaMark(
  28. x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
  29. yStart: .value("10th Percentile", stats.percentile10),
  30. yEnd: .value("90th Percentile", stats.percentile90),
  31. series: .value("10-90", "10-90")
  32. )
  33. .foregroundStyle(.blue.opacity(0.2))
  34. }
  35. // 25-75 percentile area
  36. ForEach(hourlyStats, id: \.hour) { stats in
  37. AreaMark(
  38. x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
  39. yStart: .value("25th Percentile", stats.percentile25),
  40. yEnd: .value("75th Percentile", stats.percentile75),
  41. series: .value("25-75", "25-75")
  42. )
  43. .foregroundStyle(.blue.opacity(0.3))
  44. }
  45. // Median line
  46. ForEach(hourlyStats, id: \.hour) { stats in
  47. LineMark(
  48. x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
  49. y: .value("Median", stats.median),
  50. series: .value("Median", "Median")
  51. )
  52. .lineStyle(StrokeStyle(lineWidth: 2))
  53. .foregroundStyle(.blue)
  54. }
  55. }
  56. // High/Low limit lines
  57. RuleMark(y: .value("High Limit", Double(highLimit)))
  58. .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
  59. .foregroundStyle(.orange)
  60. RuleMark(y: .value("Low Limit", Double(lowLimit)))
  61. .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
  62. .foregroundStyle(.red)
  63. }
  64. .chartYScale(domain: 40 ... 400)
  65. .chartYAxis {
  66. AxisMarks(position: .leading)
  67. }
  68. .chartYAxisLabel(alignment: .leading) {
  69. Text("\(units.rawValue)")
  70. .foregroundStyle(.primary)
  71. .font(.caption)
  72. .padding(.vertical, 3)
  73. }
  74. .chartXAxis {
  75. AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
  76. AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
  77. AxisGridLine()
  78. }
  79. }
  80. .frame(height: 200)
  81. // Legend
  82. if !isTodayOrLast24h {
  83. legend
  84. }
  85. }
  86. }
  87. private var legend: some View {
  88. HStack(spacing: 20) {
  89. VStack {
  90. // 10-90 Percentile
  91. HStack(spacing: 8) {
  92. Rectangle()
  93. .frame(width: 20, height: 8)
  94. .foregroundStyle(.blue.opacity(0.2))
  95. Text("10% - 90%")
  96. .font(.caption)
  97. .foregroundStyle(.secondary)
  98. }
  99. // 25-75 Percentile
  100. HStack(spacing: 8) {
  101. Rectangle()
  102. .frame(width: 20, height: 8)
  103. .foregroundStyle(.blue.opacity(0.3))
  104. Text("25% - 75%")
  105. .font(.caption)
  106. .foregroundStyle(.secondary)
  107. }
  108. }
  109. // Median
  110. HStack(spacing: 8) {
  111. Rectangle()
  112. .frame(width: 20, height: 2)
  113. .foregroundStyle(.blue)
  114. Text("Median")
  115. .font(.caption)
  116. .foregroundStyle(.secondary)
  117. }
  118. VStack {
  119. // High Limit
  120. HStack(spacing: 8) {
  121. Rectangle()
  122. .frame(width: 20, height: 1)
  123. .foregroundStyle(.orange)
  124. Text("High Limit")
  125. .font(.caption)
  126. .foregroundStyle(.secondary)
  127. }
  128. // Low Limit
  129. HStack(spacing: 8) {
  130. Rectangle()
  131. .frame(width: 20, height: 1)
  132. .foregroundStyle(.red)
  133. Text("Low Limit")
  134. .font(.caption)
  135. .foregroundStyle(.secondary)
  136. }
  137. }
  138. }
  139. .padding(.horizontal)
  140. }
  141. }
  142. private extension Calendar {
  143. func startOfHour(for date: Date) -> Date {
  144. let components = dateComponents([.year, .month, .day, .hour], from: date)
  145. return self.date(from: components) ?? date
  146. }
  147. }