GlucoseAreaChart.swift 6.0 KB

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