DoseChart.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. //
  2. // DoseChart.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. fileprivate struct DosePointsCache {
  12. let basal: [ChartPoint]
  13. let basalFill: [ChartPoint]
  14. let bolus: [ChartPoint]
  15. let highlight: [ChartPoint]
  16. }
  17. public class DoseChart: ChartProviding {
  18. public init() {
  19. doseEntries = []
  20. }
  21. public var doseEntries: [DoseEntry] {
  22. didSet {
  23. pointsCache = nil
  24. }
  25. }
  26. private var pointsCache: DosePointsCache? {
  27. didSet {
  28. if let pointsCache = pointsCache {
  29. if let lastDate = pointsCache.highlight.last?.x as? ChartAxisValueDate {
  30. endDate = lastDate.date
  31. }
  32. }
  33. }
  34. }
  35. /// The minimum range to display for insulin values.
  36. private let doseDisplayRangePoints: [ChartPoint] = [0, 1].map {
  37. return ChartPoint(
  38. x: ChartAxisValue(scalar: 0),
  39. y: ChartAxisValueInt($0)
  40. )
  41. }
  42. public private(set) var endDate: Date?
  43. private var doseChartCache: ChartPointsTouchHighlightLayerViewCache?
  44. }
  45. public extension DoseChart {
  46. func didReceiveMemoryWarning() {
  47. pointsCache = nil
  48. doseChartCache = nil
  49. }
  50. func generate(withFrame frame: CGRect, xAxisModel: ChartAxisModel, xAxisValues: [ChartAxisValue], axisLabelSettings: ChartLabelSettings, guideLinesLayerSettings: ChartGuideLinesLayerSettings, colors: ChartColorPalette, chartSettings: ChartSettings, labelsWidthY: CGFloat, gestureRecognizer: UIGestureRecognizer?, traitCollection: UITraitCollection) -> Chart
  51. {
  52. let integerFormatter = NumberFormatter.integer
  53. let startDate = ChartAxisValueDate.dateFromScalar(xAxisValues.first!.scalar)
  54. let points = generateDosePoints(startDate: startDate)
  55. let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesUsingLinearSegmentStep(
  56. chartPoints: points.basal + points.bolus + doseDisplayRangePoints,
  57. minSegmentCount: 2,
  58. maxSegmentCount: 3,
  59. multiple: log(2) / 2,
  60. axisValueGenerator: { ChartAxisValueDoubleLog(screenLocDouble: $0, formatter: integerFormatter, labelSettings: axisLabelSettings) },
  61. addPaddingSegmentIfEdge: true)
  62. let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY))
  63. let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel)
  64. let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)
  65. // The dose area
  66. let lineModel = ChartLineModel(chartPoints: points.basal, lineColor: colors.insulinTint, lineWidth: 2, animDuration: 0, animDelay: 0)
  67. let doseLine = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel])
  68. let doseArea = ChartPointsFillsLayer(
  69. xAxis: xAxisLayer.axis,
  70. yAxis: yAxisLayer.axis,
  71. fills: [ChartPointsFill(
  72. chartPoints: points.basalFill,
  73. fillColor: colors.insulinTint.withAlphaComponent(0.5),
  74. createContainerPoints: false
  75. )]
  76. )
  77. // bolus points
  78. let bolusPointSize: Double = 12
  79. let bolusLayer: ChartPointsScatterDownTrianglesLayer<ChartPoint>?
  80. if points.bolus.count > 0 {
  81. bolusLayer = ChartPointsScatterDownTrianglesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: points.bolus, displayDelay: 0, itemSize: CGSize(width: bolusPointSize, height: bolusPointSize), itemFillColor: colors.insulinTint)
  82. } else {
  83. bolusLayer = nil
  84. }
  85. // Grid lines
  86. let gridLayer = ChartGuideLinesForValuesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, settings: guideLinesLayerSettings, axisValuesX: Array(xAxisValues.dropFirst().dropLast()), axisValuesY: yAxisValues)
  87. // 0-line
  88. let dummyZeroChartPoint = ChartPoint(x: ChartAxisValueDouble(0), y: ChartAxisValueDouble(0))
  89. let zeroGuidelineLayer = ChartPointsViewsLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: [dummyZeroChartPoint], viewGenerator: {(chartPointModel, layer, chart) -> UIView? in
  90. let width: CGFloat = 1
  91. let viewFrame = CGRect(x: chart.contentView.bounds.minX, y: chartPointModel.screenLoc.y - width / 2, width: chart.contentView.bounds.size.width, height: width)
  92. let v = UIView(frame: viewFrame)
  93. v.layer.backgroundColor = colors.insulinTint.cgColor
  94. return v
  95. })
  96. if gestureRecognizer != nil {
  97. doseChartCache = ChartPointsTouchHighlightLayerViewCache(
  98. xAxisLayer: xAxisLayer,
  99. yAxisLayer: yAxisLayer,
  100. axisLabelSettings: axisLabelSettings,
  101. chartPoints: points.highlight,
  102. tintColor: colors.insulinTint,
  103. gestureRecognizer: gestureRecognizer
  104. )
  105. }
  106. let layers: [ChartLayer?] = [
  107. gridLayer,
  108. xAxisLayer,
  109. yAxisLayer,
  110. zeroGuidelineLayer,
  111. doseChartCache?.highlightLayer,
  112. doseArea,
  113. doseLine,
  114. bolusLayer
  115. ]
  116. let chart = Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.compactMap { $0 })
  117. // the bolus points are drawn in the chart's drawersContentView. Update the drawersContentView frame to allow the bolus points to be drawn without clipping
  118. var frame = chart.drawersContentView.frame
  119. frame.size.height = frame.height+CGFloat(bolusPointSize/2)
  120. chart.drawersContentView.frame = frame.offsetBy(dx: 0, dy: -CGFloat(bolusPointSize/2))
  121. return chart
  122. }
  123. private func generateDosePoints(startDate: Date) -> DosePointsCache {
  124. guard pointsCache == nil else {
  125. return pointsCache!
  126. }
  127. let dateFormatter = DateFormatter(timeStyle: .short)
  128. let doseFormatter = NumberFormatter.dose
  129. var basalPoints = [ChartPoint]()
  130. var basalFillPoints = [ChartPoint]()
  131. var bolusPoints = [ChartPoint]()
  132. var highlightPoints = [ChartPoint]()
  133. for entry in doseEntries {
  134. let time = entry.endDate.timeIntervalSince(entry.startDate)
  135. if entry.type == .bolus && entry.netBasalUnits > 0 {
  136. let x = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter)
  137. let y = ChartAxisValueDoubleLog(actualDouble: entry.unitsInDeliverableIncrements, unitString: "U", formatter: doseFormatter)
  138. let point = ChartPoint(x: x, y: y)
  139. bolusPoints.append(point)
  140. highlightPoints.append(point)
  141. } else if time > 0 {
  142. // TODO: Display the DateInterval
  143. let startX = ChartAxisValueDate(date: max(startDate, entry.startDate), formatter: dateFormatter)
  144. let endX = ChartAxisValueDate(date: entry.endDate, formatter: dateFormatter)
  145. let zero = ChartAxisValueInt(0)
  146. let rate = entry.netBasalUnitsPerHour
  147. let value = ChartAxisValueDoubleLog(actualDouble: rate, unitString: "U/hour", formatter: doseFormatter)
  148. let valuePoints: [ChartPoint]
  149. if abs(rate) > .ulpOfOne {
  150. valuePoints = [
  151. ChartPoint(x: startX, y: value),
  152. ChartPoint(x: endX, y: value)
  153. ]
  154. } else {
  155. valuePoints = []
  156. }
  157. basalFillPoints += [ChartPoint(x: startX, y: zero)] + valuePoints + [ChartPoint(x: endX, y: zero)]
  158. if entry.startDate > startDate {
  159. basalPoints += [ChartPoint(x: startX, y: zero)]
  160. }
  161. basalPoints += valuePoints + [ChartPoint(x: endX, y: zero)]
  162. highlightPoints += valuePoints
  163. }
  164. }
  165. let pointsCache = DosePointsCache(basal: basalPoints, basalFill: basalFillPoints, bolus: bolusPoints, highlight: highlightPoints)
  166. self.pointsCache = pointsCache
  167. return pointsCache
  168. }
  169. }