MainChartHelper.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import Charts
  2. import CoreData
  3. import Foundation
  4. import SwiftUICore
  5. enum MainChartHelper {
  6. // Calculates the glucose value thats the nearest to parameter 'time'
  7. /// -Returns: A NSManagedObject of GlucoseStored
  8. /// it is thread safe as everything is executed on the main thread
  9. static func timeToNearestGlucose(glucoseValues: [GlucoseStored], time: TimeInterval) -> GlucoseStored? {
  10. guard !glucoseValues.isEmpty else {
  11. return nil
  12. }
  13. var low = 0
  14. var high = glucoseValues.count - 1
  15. var closestGlucose: GlucoseStored?
  16. // binary search to find next glucose
  17. while low <= high {
  18. let mid = low + (high - low) / 2
  19. let midTime = glucoseValues[mid].date?.timeIntervalSince1970 ?? 0
  20. if midTime == time {
  21. return glucoseValues[mid]
  22. } else if midTime < time {
  23. low = mid + 1
  24. } else {
  25. high = mid - 1
  26. }
  27. // update if necessary
  28. if closestGlucose == nil || abs(midTime - time) < abs(closestGlucose!.date?.timeIntervalSince1970 ?? 0 - time) {
  29. closestGlucose = glucoseValues[mid]
  30. }
  31. }
  32. return closestGlucose
  33. }
  34. enum Config {
  35. static let bolusSize: CGFloat = 5
  36. static let bolusScale: CGFloat = 1.8
  37. static let carbsSize: CGFloat = 5
  38. static let maxCarbSize: CGFloat = 30
  39. static let carbsScale: CGFloat = 0.3
  40. static let fpuSize: CGFloat = 10
  41. static let maxGlucose = 270
  42. static let minGlucose = 45
  43. }
  44. static func bolusOffset(units: GlucoseUnits) -> Decimal {
  45. units == .mgdL ? 30 : 1.66
  46. }
  47. static func calculateDuration(
  48. objectID: NSManagedObjectID,
  49. attribute: String,
  50. context: NSManagedObjectContext
  51. ) -> TimeInterval? {
  52. do {
  53. let object = try context.existingObject(with: objectID)
  54. if let attributeValue = object.value(forKey: attribute) as? NSDecimalNumber {
  55. let doubleValue = attributeValue.doubleValue
  56. if doubleValue != 0 {
  57. return TimeInterval(doubleValue * 60) // return seconds
  58. }
  59. } else {
  60. debugPrint("Attribute \(attribute) not found or not of type NSDecimalNumber")
  61. }
  62. } catch {
  63. debugPrint(
  64. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate duration for object with error: \(error.localizedDescription)"
  65. )
  66. }
  67. return nil
  68. }
  69. static func calculateTarget(objectID: NSManagedObjectID, attribute: String, context: NSManagedObjectContext) -> Decimal? {
  70. do {
  71. let object = try context.existingObject(with: objectID)
  72. if let attributeValue = object.value(forKey: attribute) as? NSDecimalNumber, attributeValue != 0 {
  73. return attributeValue.decimalValue
  74. }
  75. } catch {
  76. debugPrint(
  77. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate target for object with error: \(error.localizedDescription)"
  78. )
  79. }
  80. return nil
  81. }
  82. }
  83. // MARK: - Rule Marks and Charts configurations
  84. extension MainChartView {
  85. func drawCurrentTimeMarker() -> some ChartContent {
  86. RuleMark(
  87. x: .value(
  88. "",
  89. Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
  90. unit: .second
  91. )
  92. ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
  93. }
  94. func drawStartRuleMark() -> some ChartContent {
  95. RuleMark(
  96. x: .value(
  97. "",
  98. startMarker,
  99. unit: .second
  100. )
  101. ).foregroundStyle(Color.clear)
  102. }
  103. func drawEndRuleMark() -> some ChartContent {
  104. RuleMark(
  105. x: .value(
  106. "",
  107. endMarker,
  108. unit: .second
  109. )
  110. ).foregroundStyle(Color.clear)
  111. }
  112. func basalChartPlotStyle(_ plotContent: ChartPlotContent) -> some View {
  113. plotContent
  114. .rotationEffect(.degrees(180))
  115. .scaleEffect(x: -1, y: 1)
  116. }
  117. var mainChartXAxis: some AxisContent {
  118. AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
  119. if displayXgridLines {
  120. AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
  121. } else {
  122. AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
  123. }
  124. }
  125. }
  126. var basalChartXAxis: some AxisContent {
  127. AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
  128. if displayXgridLines {
  129. AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
  130. } else {
  131. AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
  132. }
  133. AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
  134. .font(.footnote).foregroundStyle(Color.primary)
  135. }
  136. }
  137. var mainChartYAxis: some AxisContent {
  138. AxisMarks(position: .trailing) { value in
  139. if displayYgridLines {
  140. AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
  141. } else {
  142. AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
  143. }
  144. if let glucoseValue = value.as(Double.self), glucoseValue > 0 {
  145. /// fix offset between the two charts...
  146. if units == .mmolL {
  147. AxisTick(length: 7, stroke: .init(lineWidth: 7)).foregroundStyle(Color.clear)
  148. }
  149. AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
  150. }
  151. }
  152. }
  153. var cobIobChartYAxis: some AxisContent {
  154. AxisMarks(position: .trailing) { _ in
  155. if displayYgridLines {
  156. AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
  157. } else {
  158. AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
  159. }
  160. }
  161. }
  162. }
  163. // MARK: - Calculations and formatting
  164. extension MainChartView {
  165. func fullWidth(viewWidth: CGFloat) -> CGFloat {
  166. viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
  167. }
  168. // Update start and end marker to fix scroll update problem with x axis
  169. func updateStartEndMarkers() {
  170. startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
  171. let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
  172. // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
  173. let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
  174. Int(1.5) * 5 * state
  175. .minCount * 60
  176. ))
  177. endMarker = state
  178. .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
  179. dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
  180. }
  181. }