CarbEffectChart.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. //
  2. // CarbEffectChart.swift
  3. // LoopUI
  4. //
  5. // Copyright © 2019 LoopKit Authors. All rights reserved.
  6. //
  7. import Foundation
  8. import LoopKit
  9. import SwiftCharts
  10. import UIKit
  11. public class CarbEffectChart: GlucoseChart, ChartProviding {
  12. /// The chart points for expected carb effect velocity
  13. public private(set) var carbEffectPoints: [ChartPoint] = [] {
  14. didSet {
  15. // don't extend the end date for carb effects
  16. }
  17. }
  18. /// The chart points for observed insulin counteraction effect velocity
  19. public private(set) var insulinCounteractionEffectPoints: [ChartPoint] = [] {
  20. didSet {
  21. // Extend 1 hour past the seen effect to ensure some future prediction is displayed
  22. if let lastDate = insulinCounteractionEffectPoints.last?.x as? ChartAxisValueDate {
  23. endDate = lastDate.date.addingTimeInterval(.hours(1))
  24. }
  25. }
  26. }
  27. /// The chart points used for selection in the carb effect chart
  28. public private(set) var allCarbEffectPoints: [ChartPoint] = []
  29. public private(set) var endDate: Date?
  30. private lazy var dateFormatter = DateFormatter(timeStyle: .short)
  31. private lazy var decimalFormatter = NumberFormatter.dose
  32. private var carbEffectChartCache: ChartPointsTouchHighlightLayerViewCache?
  33. }
  34. extension CarbEffectChart {
  35. public func didReceiveMemoryWarning() {
  36. carbEffectPoints = []
  37. insulinCounteractionEffectPoints = []
  38. allCarbEffectPoints = []
  39. carbEffectChartCache = nil
  40. }
  41. 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
  42. {
  43. /// The minimum range to display for carb effect values.
  44. let carbEffectDisplayRangePoints: [ChartPoint] = [0, glucoseUnit.chartableIncrement].map {
  45. return ChartPoint(
  46. x: ChartAxisValue(scalar: 0),
  47. y: ChartAxisValueDouble($0)
  48. )
  49. }
  50. let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(carbEffectPoints + allCarbEffectPoints + carbEffectDisplayRangePoints,
  51. minSegmentCount: 2,
  52. maxSegmentCount: 4,
  53. multiple: glucoseUnit.chartableIncrement / 2,
  54. axisValueGenerator: {
  55. ChartAxisValueDouble($0, labelSettings: axisLabelSettings)
  56. },
  57. addPaddingSegmentIfEdge: false
  58. )
  59. let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY))
  60. let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel)
  61. let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)
  62. let carbFillColor = colors.carbTint.withAlphaComponent(0.5)
  63. let carbBlendMode: CGBlendMode
  64. switch traitCollection.userInterfaceStyle {
  65. case .dark:
  66. carbBlendMode = .plusLighter
  67. case .light, .unspecified:
  68. carbBlendMode = .plusDarker
  69. @unknown default:
  70. carbBlendMode = .plusDarker
  71. }
  72. // Carb effect
  73. let effectsLayer = ChartPointsFillsLayer(
  74. xAxis: xAxisLayer.axis,
  75. yAxis: yAxisLayer.axis,
  76. fills: [
  77. ChartPointsFill(chartPoints: carbEffectPoints, fillColor: UIColor.secondaryLabel.withAlphaComponent(0.5)),
  78. ChartPointsFill(chartPoints: insulinCounteractionEffectPoints, fillColor: carbFillColor, blendMode: carbBlendMode)
  79. ]
  80. )
  81. // Grid lines
  82. let gridLayer = ChartGuideLinesForValuesLayer(
  83. xAxis: xAxisLayer.axis,
  84. yAxis: yAxisLayer.axis,
  85. settings: guideLinesLayerSettings,
  86. axisValuesX: Array(xAxisValues.dropFirst().dropLast()),
  87. axisValuesY: yAxisValues
  88. )
  89. // 0-line
  90. let dummyZeroChartPoint = ChartPoint(x: ChartAxisValueDouble(0), y: ChartAxisValueDouble(0))
  91. let zeroGuidelineLayer = ChartPointsViewsLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: [dummyZeroChartPoint], viewGenerator: {(chartPointModel, layer, chart) -> UIView? in
  92. let width: CGFloat = 1
  93. let viewFrame = CGRect(x: chart.contentView.bounds.minX, y: chartPointModel.screenLoc.y - width / 2, width: chart.contentView.bounds.size.width, height: width)
  94. let v = UIView(frame: viewFrame)
  95. v.layer.backgroundColor = carbFillColor.cgColor
  96. return v
  97. })
  98. if gestureRecognizer != nil {
  99. carbEffectChartCache = ChartPointsTouchHighlightLayerViewCache(
  100. xAxisLayer: xAxisLayer,
  101. yAxisLayer: yAxisLayer,
  102. axisLabelSettings: axisLabelSettings,
  103. chartPoints: allCarbEffectPoints,
  104. tintColor: colors.carbTint,
  105. gestureRecognizer: gestureRecognizer
  106. )
  107. }
  108. let layers: [ChartLayer?] = [
  109. gridLayer,
  110. xAxisLayer,
  111. yAxisLayer,
  112. zeroGuidelineLayer,
  113. carbEffectChartCache?.highlightLayer,
  114. effectsLayer
  115. ]
  116. return Chart(
  117. frame: frame,
  118. innerFrame: innerFrame,
  119. settings: chartSettings,
  120. layers: layers.compactMap { $0 }
  121. )
  122. }
  123. }
  124. extension CarbEffectChart {
  125. /// Convert an array of GlucoseEffects (as glucose values) into glucose effect velocity (glucose/min) for charting
  126. ///
  127. /// - Parameter effects: A timeline of glucose values representing glucose change
  128. public func setCarbEffects(_ effects: [GlucoseEffect]) {
  129. let unit = glucoseUnit.unitDivided(by: .minute())
  130. let unitString = unit.unitString
  131. var lastDate = effects.first?.endDate
  132. var lastValue = effects.first?.quantity.doubleValue(for: glucoseUnit)
  133. let minuteInterval = 5.0
  134. var carbEffectPoints = [ChartPoint]()
  135. let zero = ChartAxisValueInt(0)
  136. for effect in effects.dropFirst() {
  137. let value = effect.quantity.doubleValue(for: glucoseUnit)
  138. let valuePerMinute = (value - lastValue!) / minuteInterval
  139. lastValue = value
  140. let startX = ChartAxisValueDate(date: lastDate!, formatter: dateFormatter)
  141. let endX = ChartAxisValueDate(date: effect.endDate, formatter: dateFormatter)
  142. lastDate = effect.endDate
  143. let valueY = ChartAxisValueDoubleUnit(valuePerMinute, unitString: unitString, formatter: decimalFormatter)
  144. carbEffectPoints += [
  145. ChartPoint(x: startX, y: zero),
  146. ChartPoint(x: startX, y: valueY),
  147. ChartPoint(x: endX, y: valueY),
  148. ChartPoint(x: endX, y: zero)
  149. ]
  150. }
  151. self.carbEffectPoints = carbEffectPoints
  152. }
  153. /// Charts glucose effect velocity
  154. ///
  155. /// - Parameter effects: A timeline of glucose velocity values
  156. public func setInsulinCounteractionEffects(_ effects: [GlucoseEffectVelocity]) {
  157. let unit = glucoseUnit.unitDivided(by: .minute())
  158. let unitString = String(format: NSLocalizedString("%1$@/min", comment: "Format string describing glucose units per minute (1: glucose unit string)"), glucoseUnit.shortLocalizedUnitString())
  159. var insulinCounteractionEffectPoints: [ChartPoint] = []
  160. var allCarbEffectPoints: [ChartPoint] = []
  161. let zero = ChartAxisValueInt(0)
  162. for effect in effects {
  163. let startX = ChartAxisValueDate(date: effect.startDate, formatter: dateFormatter)
  164. let endX = ChartAxisValueDate(date: effect.endDate, formatter: dateFormatter)
  165. let value = ChartAxisValueDoubleUnit(effect.quantity.doubleValue(for: unit), unitString: unitString, formatter: decimalFormatter)
  166. guard value.scalar != 0 else {
  167. continue
  168. }
  169. let valuePoint = ChartPoint(x: endX, y: value)
  170. insulinCounteractionEffectPoints += [
  171. ChartPoint(x: startX, y: zero),
  172. ChartPoint(x: startX, y: value),
  173. valuePoint,
  174. ChartPoint(x: endX, y: zero)
  175. ]
  176. allCarbEffectPoints.append(valuePoint)
  177. }
  178. self.insulinCounteractionEffectPoints = insulinCounteractionEffectPoints
  179. self.allCarbEffectPoints = allCarbEffectPoints
  180. }
  181. }