AreaChartSetup.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import CoreData
  2. import Foundation
  3. /// Represents statistical values for glucose readings grouped by hour of the day.
  4. ///
  5. /// This struct contains various percentile calculations that help visualize
  6. /// glucose distribution patterns throughout the day:
  7. ///
  8. /// - The median (50th percentile) shows the central tendency
  9. /// - The 25th and 75th percentiles form the interquartile range (IQR)
  10. /// - The 10th and 90th percentiles show the wider range of values
  11. ///
  12. /// The data is used to create area charts with:
  13. /// - A dark blue area for the interquartile range (25th-75th percentile)
  14. /// - A light blue area for the wider range (10th-90th percentile)
  15. /// - A solid blue line for the median
  16. ///
  17. /// Example usage:
  18. /// ```swift
  19. /// let stats = HourlyStats(
  20. /// hour: 14, // 2 PM
  21. /// median: 120, // Center line
  22. /// percentile25: 100, // Lower bound of dark band
  23. /// percentile75: 140, // Upper bound of dark band
  24. /// percentile10: 80, // Lower bound of light band
  25. /// percentile90: 160 // Upper bound of light band
  26. /// )
  27. /// ```
  28. ///
  29. /// This data structure is used to create area charts showing glucose
  30. /// variability patterns across different times of day.
  31. public struct HourlyStats: Equatable {
  32. /// The hour of day (0-23) these statistics represent
  33. let hour: Int
  34. /// The median (50th percentile) glucose value for this hour
  35. let median: Double
  36. /// The 25th percentile glucose value (lower quartile)
  37. let percentile25: Double
  38. /// The 75th percentile glucose value (upper quartile)
  39. let percentile75: Double
  40. /// The 10th percentile glucose value (lower whisker)
  41. let percentile10: Double
  42. /// The 90th percentile glucose value (upper whisker)
  43. let percentile90: Double
  44. }
  45. extension Double {
  46. /// Helper property to check if a number is even
  47. var isEven: Bool {
  48. truncatingRemainder(dividingBy: 2) == 0
  49. }
  50. }
  51. extension Stat.StateModel {
  52. /// Calculates hourly statistics for grouped glucose values
  53. /// - Parameter groupedValues: Dictionary with dates as keys and arrays of glucose readings as values
  54. /// - Returns: Dictionary with dates as keys and arrays of hourly statistics as values
  55. ///
  56. /// This function processes glucose readings grouped by date to calculate hourly statistics
  57. /// for each group. The statistics include median and various percentiles that show
  58. /// the distribution of glucose values throughout the day.
  59. func calculateStats(
  60. for groupedValues: [Date: [GlucoseStored]]
  61. ) -> [Date: [HourlyStats]] {
  62. groupedValues.mapValues { values in
  63. calculateHourlyStats(from: values.map(\.objectID))
  64. }
  65. }
  66. /// Calculates detailed hourly statistics for a set of glucose readings
  67. /// - Parameter ids: Array of NSManagedObjectIDs for glucose readings
  68. /// - Returns: Array of HourlyStats containing percentile calculations for each hour
  69. ///
  70. /// The calculation process:
  71. /// 1. Groups readings by hour of day (0-23)
  72. /// 2. For each hour:
  73. /// - Sorts glucose values
  74. /// - Calculates median (50th percentile)
  75. /// - Calculates 10th, 25th, 75th, and 90th percentiles
  76. ///
  77. /// These statistics are used to show:
  78. /// - The typical glucose range for each hour
  79. /// - The variability of glucose values
  80. /// - Patterns in glucose behavior throughout the day
  81. func calculateHourlyStats(from ids: [NSManagedObjectID]) -> [HourlyStats] {
  82. let calendar = Calendar.current
  83. // Fetch glucose values and group them by hour
  84. let hourlyGroups = Dictionary(
  85. grouping: fetchGlucoseValues(from: ids),
  86. by: { calendar.component(.hour, from: $0.date ?? Date()) }
  87. )
  88. // Calculate stats for each hour (0-23)
  89. return (0 ... 23).map { hour in
  90. let values = hourlyGroups[hour]?.compactMap { Double($0.glucose) }.sorted() ?? []
  91. guard !values.isEmpty else {
  92. return HourlyStats(hour: hour, median: 0, percentile25: 0, percentile75: 0, percentile10: 0, percentile90: 0)
  93. }
  94. // Calculate percentiles using array indices
  95. let count = values.count
  96. return HourlyStats(
  97. hour: hour,
  98. median: values[count * 50 / 100],
  99. percentile25: values[count * 25 / 100],
  100. percentile75: values[count * 75 / 100],
  101. percentile10: values[count * 10 / 100],
  102. percentile90: values[count * 90 / 100]
  103. )
  104. }
  105. }
  106. }