MainChartView.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import Charts
  2. import CoreData
  3. import SwiftUI
  4. let screenSize: CGRect = UIScreen.main.bounds
  5. let calendar = Calendar.current
  6. struct MainChartView: View {
  7. var geo: GeometryProxy
  8. var safeAreaSize: CGFloat
  9. var units: GlucoseUnits
  10. var hours: Int
  11. var highGlucose: Decimal
  12. var lowGlucose: Decimal
  13. var currentGlucoseTarget: Decimal
  14. var glucoseColorScheme: GlucoseColorScheme
  15. var screenHours: Int16
  16. var displayXgridLines: Bool
  17. var displayYgridLines: Bool
  18. var thresholdLines: Bool
  19. var state: Home.StateModel
  20. @State var basalProfiles: [BasalProfile] = []
  21. @State var preparedTempBasals: [(start: Date, end: Date, rate: Double)] = []
  22. @State var selection: Date? = nil
  23. @State var mainChartHasInitialized = false
  24. let now = Date.now
  25. private let context = CoreDataStack.shared.persistentContainer.viewContext
  26. @Environment(\.colorScheme) var colorScheme
  27. @Environment(\.calendar) var calendar
  28. var upperLimit: Decimal {
  29. units == .mgdL ? 400 : 22.2
  30. }
  31. private var selectedGlucose: GlucoseStored? {
  32. guard let selection = selection else { return nil }
  33. let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
  34. return state.glucoseFromPersistence.first { $0.date.map(range.contains) ?? false }
  35. }
  36. private func findDetermination(in range: ClosedRange<Date>) -> OrefDetermination? {
  37. state.enactedAndNonEnactedDeterminations.first {
  38. $0.deliverAt ?? now >= range.lowerBound && $0.deliverAt ?? now <= range.upperBound
  39. }
  40. }
  41. var selectedCOBValue: OrefDetermination? {
  42. guard let selection = selection else { return nil }
  43. let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
  44. return findDetermination(in: range)
  45. }
  46. var selectedIOBValue: OrefDetermination? {
  47. guard let selection = selection else { return nil }
  48. let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
  49. return findDetermination(in: range)
  50. }
  51. var body: some View {
  52. VStack {
  53. ZStack {
  54. VStack(spacing: 5) {
  55. dummyBasalChart
  56. staticYAxisChart
  57. Spacer()
  58. dummyCobChart
  59. }
  60. ScrollViewReader { scroller in
  61. ScrollView(.horizontal, showsIndicators: false) {
  62. VStack(spacing: 5) {
  63. basalChart
  64. mainChart
  65. Spacer()
  66. cobIobChart
  67. }.onChange(of: screenHours) {
  68. scroller.scrollTo("MainChart", anchor: .trailing)
  69. }
  70. .onChange(of: state.glucoseFromPersistence.last?.glucose) {
  71. scroller.scrollTo("MainChart", anchor: .trailing)
  72. state.updateStartEndMarkers()
  73. }
  74. .onChange(of: state.enactedAndNonEnactedDeterminations.first?.deliverAt) {
  75. scroller.scrollTo("MainChart", anchor: .trailing)
  76. }
  77. .onChange(of: units) {
  78. // TODO: - Refactor this to only update the Y Axis Scale
  79. state.setupGlucoseArray()
  80. }
  81. .onAppear {
  82. if !mainChartHasInitialized {
  83. scroller.scrollTo("MainChart", anchor: .trailing)
  84. state.updateStartEndMarkers()
  85. calculateTempBasalsInBackground()
  86. mainChartHasInitialized = true
  87. }
  88. }
  89. }
  90. }
  91. }
  92. }
  93. }
  94. }
  95. // MARK: - Main Chart with selection Popover
  96. extension MainChartView {
  97. private var mainChart: some View {
  98. VStack {
  99. Chart {
  100. drawStartRuleMark()
  101. drawEndRuleMark()
  102. drawCurrentTimeMarker()
  103. GlucoseTargetsView(
  104. targetProfiles: state.targetProfiles
  105. )
  106. OverrideView(
  107. state: state,
  108. overrides: state.overrides,
  109. overrideRunStored: state.overrideRunStored,
  110. units: state.units,
  111. viewContext: context
  112. )
  113. TempTargetView(
  114. tempTargetStored: state.tempTargetStored,
  115. tempTargetRunStored: state.tempTargetRunStored,
  116. units: state.units,
  117. viewContext: context
  118. )
  119. GlucoseChartView(
  120. glucoseData: state.glucoseFromPersistence,
  121. units: state.units,
  122. highGlucose: state.highGlucose,
  123. lowGlucose: state.lowGlucose,
  124. currentGlucoseTarget: state.currentGlucoseTarget,
  125. isSmoothingEnabled: state.isSmoothingEnabled,
  126. glucoseColorScheme: state.glucoseColorScheme
  127. )
  128. InsulinView(
  129. glucoseData: state.glucoseFromPersistence,
  130. insulinData: state.insulinFromPersistence,
  131. units: state.units
  132. )
  133. CarbView(
  134. glucoseData: state.glucoseFromPersistence,
  135. units: state.units,
  136. carbData: state.carbsFromPersistence,
  137. fpuData: state.fpusFromPersistence,
  138. minValue: units == .mgdL ? state.minYAxisValue : state.minYAxisValue
  139. .asMmolL
  140. )
  141. ForecastView(
  142. preprocessedData: state.preprocessedData,
  143. minForecast: state.minForecast,
  144. maxForecast: state.maxForecast,
  145. units: state.units,
  146. maxValue: state.maxYAxisValue,
  147. forecastDisplayType: state.forecastDisplayType
  148. )
  149. /// show glucose value when hovering over it
  150. if let selectedGlucose {
  151. SelectionPopoverView(
  152. selectedGlucose: selectedGlucose,
  153. selectedIOBValue: selectedIOBValue,
  154. selectedCOBValue: selectedCOBValue,
  155. units: units,
  156. highGlucose: highGlucose,
  157. lowGlucose: lowGlucose,
  158. currentGlucoseTarget: currentGlucoseTarget,
  159. glucoseColorScheme: glucoseColorScheme
  160. )
  161. }
  162. }
  163. .id("MainChart")
  164. .onChange(of: state.insulinFromPersistence) {
  165. state.roundedTotalBolus = state.calculateTINS()
  166. }
  167. .frame(
  168. minHeight: geo.size.height * (0.28 - safeAreaSize)
  169. )
  170. .frame(width: fullWidth(viewWidth: screenSize.width))
  171. .chartXScale(domain: state.startMarker ... state.endMarker)
  172. .chartXAxis { mainChartXAxis }
  173. .chartYAxis { mainChartYAxis }
  174. .chartYAxis(.hidden)
  175. .chartXSelection(value: $selection)
  176. .chartYScale(
  177. domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue
  178. .asMmolL ... state.maxYAxisValue.asMmolL
  179. )
  180. .chartLegend(.hidden)
  181. }
  182. }
  183. }