StackedChartSetup.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import CoreData
  2. import Foundation
  3. extension Stat {
  4. /// Represents a single data point in the glucose range distribution
  5. /// - hour: The hour of the day (0-23)
  6. /// - count: The percentage of readings in this range for the given hour (0-100)
  7. struct GlucoseRangeValue {
  8. let hour: Int
  9. let count: Double
  10. }
  11. /// Represents the distribution of glucose values within specific ranges for each hour
  12. ///
  13. /// This struct is used to visualize how glucose values are distributed across different
  14. /// ranges (e.g., low, target, high) throughout the day. Each range has a name and
  15. /// corresponding hourly values showing the percentage of readings in that range.
  16. ///
  17. /// Example ranges and their meanings:
  18. /// - "<54": Urgent low
  19. /// - "54-70": Low
  20. /// - "70-140": Target range
  21. /// - "140-180": High
  22. /// - "180-200": Very high
  23. /// - "200-220": Very high+
  24. /// - ">220": Urgent high
  25. struct GlucoseRangeStats: Identifiable {
  26. let id = UUID()
  27. /// The name of the glucose range (e.g., "70-140", "<54")
  28. let name: String
  29. /// Array of hourly values containing percentages for this range
  30. let values: [GlucoseRangeValue]
  31. }
  32. }
  33. extension Stat.StateModel {
  34. /// Calculates range statistics for grouped glucose values
  35. /// - Parameter groupedValues: Dictionary with dates as keys and arrays of glucose readings as values
  36. /// - Returns: Dictionary with dates as keys and arrays of range statistics as values
  37. ///
  38. /// This function processes glucose readings grouped by date to calculate the distribution
  39. /// of values across different ranges for each hour of the day.
  40. func calculateRangeStats(
  41. for groupedValues: [Date: [GlucoseStored]]
  42. ) -> [Date: [Stat.GlucoseRangeStats]] {
  43. groupedValues.mapValues { values in
  44. calculateGlucoseRangeStats(from: values.map(\.objectID))
  45. }
  46. }
  47. /// Calculates the distribution of glucose values across different ranges for each hour
  48. /// - Parameter ids: Array of NSManagedObjectIDs for glucose readings
  49. /// - Returns: Array of GlucoseRangeStats containing percentage distributions
  50. ///
  51. /// The calculation process:
  52. /// 1. Groups readings by hour of day
  53. /// 2. Defines glucose ranges and their conditions
  54. /// 3. For each range and hour:
  55. /// - Counts readings that fall within the range
  56. /// - Calculates percentage of total readings in that range
  57. ///
  58. /// The results are used to create stacked area charts showing:
  59. /// - Distribution of glucose values across ranges
  60. /// - Patterns in glucose control throughout the day
  61. /// - Time spent in different ranges for each hour
  62. func calculateGlucoseRangeStats(from ids: [NSManagedObjectID]) -> [Stat.GlucoseRangeStats] {
  63. let calendar = Calendar.current
  64. // Group glucose values by hour
  65. let hourlyGroups = Dictionary(
  66. grouping: fetchGlucoseValues(from: ids),
  67. by: { calendar.component(.hour, from: $0.date ?? Date()) }
  68. )
  69. // Prepare hourly values for processing
  70. let hourlyValues = (0 ... 23).map { hour -> (hour: Int, values: [Double]) in
  71. let values = hourlyGroups[hour]?.compactMap { Double($0.glucose) } ?? []
  72. return (hour, values)
  73. }
  74. // Define glucose ranges and their conditions
  75. let ranges: [(name: String, filter: (Double) -> Bool)] = [
  76. ("<54", { [self] in $0 < Double(self.lowLimit - 20) }),
  77. ("54-70", { [self] in $0 >= Double(self.lowLimit - 20) && $0 < Double(self.lowLimit) }),
  78. ("70-140", { [self] in $0 >= Double(self.lowLimit) && $0 <= 140 }),
  79. ("140-180", { [self] in $0 > 140 && $0 <= Double(self.highLimit) }),
  80. ("180-200", { [self] in $0 > Double(self.highLimit) && $0 <= 200 }),
  81. ("200-220", { $0 > 200 && $0 <= 220 }),
  82. (">220", { $0 > 220 })
  83. ]
  84. // Calculate percentage distribution for each range
  85. return ranges.map { range in
  86. Stat.GlucoseRangeStats(
  87. name: range.name,
  88. values: hourlyValues.map { hour, values in
  89. let total = Double(values.count)
  90. let count = values.filter(range.filter).count
  91. return Stat.GlucoseRangeValue(
  92. hour: hour,
  93. count: total > 0 ? Double(count) / total : 0
  94. )
  95. }
  96. )
  97. }
  98. }
  99. }