PredictedGlucoseChart.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. //
  2. // PredictedGlucoseChart.swift
  3. // LoopUI
  4. //
  5. // Copyright © 2019 LoopKit Authors. All rights reserved.
  6. //
  7. import Foundation
  8. import LoopKit
  9. import SwiftCharts
  10. import HealthKit
  11. import UIKit
  12. public class PredictedGlucoseChart: GlucoseChart, ChartProviding {
  13. public private(set) var glucosePoints: [ChartPoint] = [] {
  14. didSet {
  15. if let lastDate = glucosePoints.last?.x as? ChartAxisValueDate {
  16. updateEndDate(lastDate.date)
  17. }
  18. }
  19. }
  20. /// The chart points for predicted glucose
  21. public private(set) var predictedGlucosePoints: [ChartPoint] = [] {
  22. didSet {
  23. if let lastDate = predictedGlucosePoints.last?.x as? ChartAxisValueDate {
  24. updateEndDate(lastDate.date)
  25. }
  26. }
  27. }
  28. /// The chart points for alternate predicted glucose
  29. public private(set) var alternatePredictedGlucosePoints: [ChartPoint]?
  30. public var targetGlucoseSchedule: GlucoseRangeSchedule? {
  31. didSet {
  32. targetGlucosePoints = []
  33. }
  34. }
  35. public var preMealOverride: TemporaryScheduleOverride? {
  36. didSet {
  37. preMealOverrideDurationPoints = []
  38. }
  39. }
  40. public var scheduleOverride: TemporaryScheduleOverride? {
  41. didSet {
  42. targetOverrideDurationPoints = []
  43. }
  44. }
  45. private var targetGlucosePoints = [TargetChartBar]()
  46. private var preMealOverrideDurationPoints: [ChartPoint] = []
  47. private var targetOverrideDurationPoints: [ChartPoint] = []
  48. private var glucoseChartCache: ChartPointsTouchHighlightLayerViewCache?
  49. public private(set) var endDate: Date?
  50. private var predictedGlucoseSoftBounds: PredictedGlucoseBounds?
  51. private let yAxisStepSizeMGDLOverride: Double?
  52. private var maxYAxisSegmentCount: Double {
  53. // when a glucose value is below the predicted glucose minimum soft bound, allow for more y-axis segments
  54. return glucoseValueBelowSoftBoundsMinimum() ? 5 : 4
  55. }
  56. private func updateEndDate(_ date: Date) {
  57. if endDate == nil || date > endDate! {
  58. self.endDate = date
  59. }
  60. }
  61. public init(predictedGlucoseBounds: PredictedGlucoseBounds? = nil,
  62. yAxisStepSizeMGDLOverride: Double? = nil) {
  63. self.predictedGlucoseSoftBounds = predictedGlucoseBounds
  64. self.yAxisStepSizeMGDLOverride = yAxisStepSizeMGDLOverride
  65. super.init()
  66. }
  67. }
  68. extension PredictedGlucoseChart {
  69. public func didReceiveMemoryWarning() {
  70. glucosePoints = []
  71. predictedGlucosePoints = []
  72. alternatePredictedGlucosePoints = nil
  73. targetGlucosePoints = [TargetChartBar]()
  74. targetOverrideDurationPoints = []
  75. glucoseChartCache = nil
  76. }
  77. public func generate(withFrame frame: CGRect, xAxisModel: ChartAxisModel, xAxisValues: [ChartAxisValue], axisLabelSettings: ChartLabelSettings, guideLinesLayerSettings: ChartGuideLinesLayerSettings, colors: ChartColorPalette, chartSettings: ChartSettings, labelsWidthY: CGFloat, gestureRecognizer: UIGestureRecognizer?, traitCollection: UITraitCollection) -> Chart
  78. {
  79. if targetGlucosePoints.isEmpty, xAxisValues.count > 1, let schedule = targetGlucoseSchedule {
  80. // TODO: This only considers one override: pre-meal or an active override. ChartPoint.barsForGlucoseRangeSchedule needs to accept list of overridden ranges.
  81. let potentialOverride = (preMealOverride?.isActive() ?? false) ? preMealOverride : (scheduleOverride?.isActive() ?? false) ? scheduleOverride : nil
  82. targetGlucosePoints = ChartPoint.barsForGlucoseRangeSchedule(schedule, unit: glucoseUnit, xAxisValues: xAxisValues, considering: potentialOverride)
  83. var displayedScheduleOverride = scheduleOverride
  84. if let preMealOverride = preMealOverride, preMealOverride.isActive() {
  85. preMealOverrideDurationPoints = ChartPoint.pointsForGlucoseRangeScheduleOverride(preMealOverride, unit: glucoseUnit, xAxisValues: xAxisValues)
  86. if displayedScheduleOverride != nil {
  87. if displayedScheduleOverride!.scheduledEndDate > preMealOverride.scheduledEndDate {
  88. let start = max(displayedScheduleOverride!.startDate, preMealOverride.scheduledEndDate)
  89. displayedScheduleOverride!.scheduledInterval = DateInterval(start: start, end: displayedScheduleOverride!.scheduledEndDate)
  90. } else {
  91. displayedScheduleOverride = nil
  92. }
  93. }
  94. } else {
  95. preMealOverrideDurationPoints = []
  96. }
  97. if let override = displayedScheduleOverride, override.isActive() || override.startDate > Date() {
  98. targetOverrideDurationPoints = ChartPoint.pointsForGlucoseRangeScheduleOverride(override, unit: glucoseUnit, xAxisValues: xAxisValues)
  99. } else {
  100. targetOverrideDurationPoints = []
  101. }
  102. }
  103. let yAxisValues = determineYAxisValues(axisLabelSettings: axisLabelSettings)
  104. let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY))
  105. let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel)
  106. let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)
  107. // The glucose targets
  108. let targetFill = colors.glucoseTint.withAlphaComponent(0.2)
  109. let overrideFill: UIColor = colors.glucoseTint.withAlphaComponent(0.45)
  110. let fills =
  111. targetGlucosePoints.map {
  112. if $0.isOverride {
  113. return ChartPointsFill(
  114. chartPoints: $0.points,
  115. fillColor: overrideFill,
  116. createContainerPoints: false)
  117. } else {
  118. return ChartPointsFill(
  119. chartPoints: $0.points,
  120. fillColor: targetFill,
  121. createContainerPoints: false)
  122. }
  123. } + [
  124. ChartPointsFill(
  125. chartPoints: preMealOverrideDurationPoints,
  126. fillColor: overrideFill,
  127. createContainerPoints: false
  128. ),
  129. ChartPointsFill(
  130. chartPoints: targetOverrideDurationPoints,
  131. fillColor: overrideFill,
  132. createContainerPoints: false
  133. )]
  134. let targetsLayer = ChartPointsFillsLayer(
  135. xAxis: xAxisLayer.axis,
  136. yAxis: yAxisLayer.axis,
  137. fills: fills
  138. )
  139. // Grid lines
  140. let gridLayer = ChartGuideLinesForValuesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, settings: guideLinesLayerSettings, axisValuesX: Array(xAxisValues.dropFirst().dropLast()), axisValuesY: yAxisValues)
  141. let circles = ChartPointsScatterCirclesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: glucosePoints, displayDelay: 0, itemSize: CGSize(width: 4, height: 4), itemFillColor: colors.glucoseTint, optimized: true)
  142. var alternatePrediction: ChartLayer?
  143. if let altPoints = alternatePredictedGlucosePoints, altPoints.count > 1 {
  144. let lineModel = ChartLineModel.predictionLine(points: altPoints, color: colors.glucoseTint, width: 2)
  145. alternatePrediction = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel])
  146. }
  147. var prediction: ChartLayer?
  148. if predictedGlucosePoints.count > 1 {
  149. let lineColor = (alternatePrediction == nil) ? colors.glucoseTint : UIColor.secondaryLabel
  150. let lineModel = ChartLineModel.predictionLine(
  151. points: predictedGlucosePoints,
  152. color: lineColor,
  153. width: 1
  154. )
  155. prediction = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel])
  156. }
  157. if gestureRecognizer != nil {
  158. glucoseChartCache = ChartPointsTouchHighlightLayerViewCache(
  159. xAxisLayer: xAxisLayer,
  160. yAxisLayer: yAxisLayer,
  161. axisLabelSettings: axisLabelSettings,
  162. chartPoints: glucosePoints + (alternatePredictedGlucosePoints ?? predictedGlucosePoints),
  163. tintColor: colors.glucoseTint,
  164. gestureRecognizer: gestureRecognizer
  165. )
  166. }
  167. let layers: [ChartLayer?] = [
  168. gridLayer,
  169. targetsLayer,
  170. xAxisLayer,
  171. yAxisLayer,
  172. glucoseChartCache?.highlightLayer,
  173. prediction,
  174. alternatePrediction,
  175. circles
  176. ]
  177. return Chart(
  178. frame: frame,
  179. innerFrame: innerFrame,
  180. settings: chartSettings,
  181. layers: layers.compactMap { $0 }
  182. )
  183. }
  184. private func determineYAxisValues(axisLabelSettings: ChartLabelSettings? = nil) -> [ChartAxisValue] {
  185. let points = [
  186. glucosePoints, predictedGlucosePoints,
  187. preMealOverrideDurationPoints, targetOverrideDurationPoints,
  188. targetGlucosePoints.flatMap { $0.points },
  189. glucoseDisplayRangePoints
  190. ].flatMap { $0 }
  191. let axisValueGenerator: ChartAxisValueStaticGenerator
  192. if let axisLabelSettings = axisLabelSettings {
  193. axisValueGenerator = { ChartAxisValueDouble($0, labelSettings: axisLabelSettings) }
  194. } else {
  195. axisValueGenerator = { ChartAxisValueDouble($0) }
  196. }
  197. let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesUsingLinearSegmentStep(chartPoints: points,
  198. minSegmentCount: 2,
  199. maxSegmentCount: maxYAxisSegmentCount,
  200. multiple: glucoseUnit == .milligramsPerDeciliter ? (yAxisStepSizeMGDLOverride ?? 25) : 1,
  201. axisValueGenerator: axisValueGenerator,
  202. addPaddingSegmentIfEdge: false
  203. )
  204. return yAxisValues
  205. }
  206. }
  207. extension PredictedGlucoseChart {
  208. public func setGlucoseValues(_ glucoseValues: [GlucoseValue]) {
  209. glucosePoints = glucosePointsFromValues(glucoseValues)
  210. }
  211. public func setPredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) {
  212. let clampedPredicatedGlucoseValues = clampPredictedGlucoseValues(glucoseValues)
  213. predictedGlucosePoints = glucosePointsFromValues(clampedPredicatedGlucoseValues)
  214. }
  215. public func setAlternatePredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) {
  216. alternatePredictedGlucosePoints = glucosePointsFromValues(glucoseValues)
  217. }
  218. }
  219. // MARK: - Clamping the predicted glucose values
  220. extension PredictedGlucoseChart {
  221. var chartMaximumValue: HKQuantity? {
  222. guard let glucosePointMaximum = glucosePoints.max(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
  223. return nil
  224. }
  225. let yAxisValues = determineYAxisValues()
  226. if let maxYAxisValue = yAxisValues.last,
  227. maxYAxisValue.scalar > glucosePointMaximum.y.scalar
  228. {
  229. return HKQuantity(unit: glucoseUnit, doubleValue: maxYAxisValue.scalar)
  230. }
  231. return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMaximum.y.scalar)
  232. }
  233. var chartMinimumValue: HKQuantity? {
  234. guard let glucosePointMinimum = glucosePoints.min(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
  235. return nil
  236. }
  237. let yAxisValues = determineYAxisValues()
  238. if let minYAxisValue = yAxisValues.first,
  239. minYAxisValue.scalar < glucosePointMinimum.y.scalar
  240. {
  241. return HKQuantity(unit: glucoseUnit, doubleValue: minYAxisValue.scalar)
  242. }
  243. return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMinimum.y.scalar)
  244. }
  245. func clampPredictedGlucoseValues(_ glucoseValues: [GlucoseValue]) -> [GlucoseValue] {
  246. guard let predictedGlucoseBounds = predictedGlucoseSoftBounds else {
  247. return glucoseValues
  248. }
  249. let predictedGlucoseValueMaximum = chartMaximumValue != nil ? max(predictedGlucoseBounds.maximum, chartMaximumValue!) : predictedGlucoseBounds.maximum
  250. let predictedGlucoseValueMinimum = chartMinimumValue != nil ? min(predictedGlucoseBounds.minimum, chartMinimumValue!) : predictedGlucoseBounds.minimum
  251. return glucoseValues.map {
  252. if $0.quantity > predictedGlucoseValueMaximum {
  253. return PredictedGlucoseValue(startDate: $0.startDate, quantity: predictedGlucoseValueMaximum)
  254. } else if $0.quantity < predictedGlucoseValueMinimum {
  255. return PredictedGlucoseValue(startDate: $0.startDate, quantity: predictedGlucoseValueMinimum)
  256. } else {
  257. return $0
  258. }
  259. }
  260. }
  261. var chartedGlucoseValueMinimum: HKQuantity? {
  262. guard let glucosePointMinimum = glucosePoints.min(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
  263. return nil
  264. }
  265. return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMinimum.y.scalar)
  266. }
  267. func glucoseValueBelowSoftBoundsMinimum() -> Bool {
  268. guard let predictedGlucoseSoftBounds = predictedGlucoseSoftBounds,
  269. let chartedGlucoseValueMinimum = chartedGlucoseValueMinimum else
  270. {
  271. return false
  272. }
  273. return chartedGlucoseValueMinimum < predictedGlucoseSoftBounds.minimum
  274. }
  275. public struct PredictedGlucoseBounds {
  276. var minimum: HKQuantity
  277. var maximum: HKQuantity
  278. public static var `default`: PredictedGlucoseBounds {
  279. return PredictedGlucoseBounds(minimum: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 40),
  280. maximum: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 400))
  281. }
  282. }
  283. }