GlucoseTargetsView.swift 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import Charts
  2. import Foundation
  3. import SwiftUI
  4. struct GlucoseTargetsView: ChartContent {
  5. let startMarker: Date
  6. let units: GlucoseUnits
  7. let bgTargets: BGTargets
  8. var body: some ChartContent {
  9. drawGlucoseTargets()
  10. }
  11. /**
  12. Draws glucose target ranges on the chart
  13. - Returns: A ChartContent containing line marks representing target glucose ranges
  14. The function:
  15. - Creates target profiles for two consecutive days
  16. - Converts values between mg/dL and mmol/L based on user settings
  17. - Draws green lines to visualize the target ranges
  18. */
  19. private func drawGlucoseTargets() -> some ChartContent {
  20. // Array to store target profiles for visualization
  21. let targetProfiles: [TargetProfile] = processFetchedTargets(bgTargets)
  22. // Draw target lines for each profile
  23. return ForEach(targetProfiles, id: \.self) { profile in
  24. LineMark(
  25. x: .value("Time", Date(timeIntervalSinceReferenceDate: profile.startTime)),
  26. y: .value("Target", profile.value)
  27. )
  28. .lineStyle(.init(lineWidth: 1))
  29. .foregroundStyle(Color.green.gradient)
  30. LineMark(
  31. x: .value("Time", Date(timeIntervalSinceReferenceDate: profile.endTime)),
  32. y: .value("Target", profile.value)
  33. )
  34. .lineStyle(.init(lineWidth: 1))
  35. .foregroundStyle(Color.green.gradient)
  36. }
  37. }
  38. /**
  39. Processes raw glucose target data into a list of target profiles for visualization.
  40. - Parameter rawTargets: The raw glucose target data containing offset and glucose values.
  41. - Returns: An array of `TargetProfile` objects, each representing a glucose target range for today and tomorrow.
  42. The function:
  43. - Converts glucose targets into profiles covering two consecutive days (today and tomorrow).
  44. - Calculates start and end times for each target based on the offsets provided.
  45. - Handles conversions between mg/dL and mmol/L as per user settings.
  46. - Ensures targets span across midnight to avoid data cutoff.
  47. Example:
  48. For a target at offset 0 (midnight) with low glucose value 70 mg/dL, the function generates two profiles:
  49. - One for today from midnight to the next target offset or end of the day.
  50. - Another for tomorrow covering the same time range.
  51. */
  52. private func processFetchedTargets(_ rawTargets: BGTargets) -> [TargetProfile] {
  53. var targetProfiles: [TargetProfile] = []
  54. // Ensure there are targets to process
  55. guard !rawTargets.targets.isEmpty else {
  56. print("Warning: No targets to process in rawTargets.")
  57. return []
  58. }
  59. let targets = rawTargets.targets
  60. // Base date is the start of the day for the startMarker
  61. let baseDate = Calendar.current.startOfDay(for: startMarker)
  62. // Process each target twice: once for today and once for tomorrow
  63. for index in 0 ..< (targets.count * 2) {
  64. // Calculate the day offset (0 for today, 1 for tomorrow)
  65. let dayOffset = index / targets.count
  66. let targetIndex = index % targets.count
  67. // Validate target index to ensure safety
  68. guard targetIndex < targets.count else {
  69. print("Error: Invalid target index \(targetIndex).")
  70. continue
  71. }
  72. // Fetch the target for the current iteration
  73. let target = targets[targetIndex]
  74. // Calculate the time offset for the current day
  75. let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60)
  76. // Calculate the start time for the current target
  77. let startTime = baseDate
  78. .addingTimeInterval(dayTimeOffset)
  79. .addingTimeInterval(TimeInterval(target.offset * 60))
  80. // Calculate the end time for the current target
  81. let endTime: Date = {
  82. if targetIndex + 1 < targets.count {
  83. // End time is the start time of the next target within the same day
  84. return baseDate
  85. .addingTimeInterval(dayTimeOffset)
  86. .addingTimeInterval(TimeInterval(targets[targetIndex + 1].offset * 60))
  87. } else {
  88. // End time is the end of the day (midnight of the next day)
  89. return baseDate.addingTimeInterval(dayTimeOffset + 24 * 60 * 60)
  90. }
  91. }()
  92. // Convert glucose value based on user unit preference (mg/dL or mmol/L)
  93. let targetValue = units == .mgdL ? target.low : target.low.asMmolL
  94. // Append the processed target profile to the list
  95. targetProfiles.append(
  96. TargetProfile(
  97. value: targetValue,
  98. startTime: startTime.timeIntervalSinceReferenceDate,
  99. endTime: endTime.timeIntervalSinceReferenceDate
  100. )
  101. )
  102. }
  103. return targetProfiles
  104. }
  105. }
  106. struct TargetProfile: Hashable {
  107. let value: Decimal
  108. let startTime: TimeInterval
  109. let endTime: TimeInterval
  110. }
  111. private extension Date {
  112. var startOfDay: Date {
  113. Calendar.current.startOfDay(for: self)
  114. }
  115. }