GlucoseChartView.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import Charts
  2. import SwiftDate
  3. import SwiftUI
  4. extension DateFormatter: AxisValueFormatter {
  5. public func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
  6. timeStyle = .short
  7. return string(from: Date(timeIntervalSince1970: value))
  8. }
  9. }
  10. extension NumberFormatter: AxisValueFormatter {
  11. public func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
  12. numberStyle = .decimal
  13. maximumFractionDigits = 1
  14. return string(from: value as NSNumber)!
  15. }
  16. }
  17. struct GlucoseChartView: UIViewRepresentable {
  18. @Binding var glucose: [BloodGlucose]
  19. @Binding var suggestion: Suggestion?
  20. let units: GlucoseUnits
  21. func makeUIView(context _: Context) -> LineChartView {
  22. let view = LineChartView()
  23. makeDataPointsFor(view: view)
  24. view.xAxis.valueFormatter = DateFormatter()
  25. view.leftAxis.valueFormatter = NumberFormatter()
  26. view.xAxis.labelPosition = .top
  27. view.rightAxis.drawLabelsEnabled = false
  28. view.drawBordersEnabled = true
  29. view.setScaleEnabled(false)
  30. view.setVisibleXRangeMaximum(6.hours.timeInterval)
  31. view.xAxis.granularityEnabled = true
  32. view.xAxis.granularity = 1.hours.timeInterval
  33. return view
  34. }
  35. func updateUIView(_ view: LineChartView, context _: Context) {
  36. makeDataPointsFor(view: view)
  37. view.moveViewToX(glucose.last?.dateString.timeIntervalSince1970 ?? 0)
  38. }
  39. private func makeDataPointsFor(view: LineChartView) {
  40. guard !glucose.isEmpty else {
  41. return
  42. }
  43. let dataPoints = glucose.map {
  44. ChartDataEntry(
  45. x: $0.dateString.timeIntervalSince1970,
  46. y: Double($0.sgv ?? 0) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
  47. )
  48. }
  49. let data = MyLineChartDataSet(entries: dataPoints, label: "BG")
  50. data.drawCirclesEnabled = true
  51. data.circleRadius = 2
  52. data.setCircleColor(UIColor(named: "LoopGreen")!)
  53. data.setColor(UIColor(named: "LoopGreen")!)
  54. data.lineWidth = 0
  55. data.drawValuesEnabled = false
  56. var series = [data]
  57. let lastDate = suggestion?.deliverAt ?? Date()
  58. if let iob = suggestion?.predictions?.iob {
  59. let dataPoints = iob.enumerated().map {
  60. ChartDataEntry(
  61. x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
  62. y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
  63. )
  64. }
  65. let data = MyLineChartDataSet(entries: dataPoints, label: "IOB")
  66. data.drawCirclesEnabled = true
  67. data.circleRadius = 2
  68. data.setCircleColor(.blue)
  69. data.setColor(.blue)
  70. data.lineWidth = 0
  71. data.drawValuesEnabled = false
  72. series.append(data)
  73. }
  74. if let zt = suggestion?.predictions?.zt {
  75. let dataPoints = zt.enumerated().map {
  76. ChartDataEntry(
  77. x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
  78. y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
  79. )
  80. }
  81. let data = MyLineChartDataSet(entries: dataPoints, label: "ZT")
  82. data.drawCirclesEnabled = true
  83. data.circleRadius = 2
  84. data.setCircleColor(.cyan)
  85. data.setColor(.cyan)
  86. data.lineWidth = 0
  87. data.drawValuesEnabled = false
  88. series.append(data)
  89. }
  90. if let cob = suggestion?.predictions?.cob {
  91. let dataPoints = cob.enumerated().map {
  92. ChartDataEntry(
  93. x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
  94. y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
  95. )
  96. }
  97. let data = MyLineChartDataSet(entries: dataPoints, label: "COB")
  98. data.drawCirclesEnabled = true
  99. data.circleRadius = 2
  100. data.setCircleColor(.orange)
  101. data.setColor(.orange)
  102. data.lineWidth = 0
  103. data.drawValuesEnabled = false
  104. series.append(data)
  105. }
  106. if let uam = suggestion?.predictions?.uam {
  107. let dataPoints = uam.enumerated().map {
  108. ChartDataEntry(
  109. x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
  110. y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
  111. )
  112. }
  113. let data = MyLineChartDataSet(entries: dataPoints, label: "UAM")
  114. data.drawCirclesEnabled = true
  115. data.circleRadius = 2
  116. data.setCircleColor(.yellow)
  117. data.setColor(.yellow)
  118. data.lineWidth = 0
  119. data.drawValuesEnabled = false
  120. series.append(data)
  121. }
  122. view.data = LineChartData(dataSets: series)
  123. }
  124. }
  125. class MyLineChartDataSet: LineChartDataSet {
  126. override func entryIndex(x xValue: Double, closestToY yValue: Double, rounding: ChartDataSetRounding) -> Int {
  127. var closest = partitioningIndex { $0.x >= xValue }
  128. if closest >= endIndex {
  129. closest = endIndex - 1
  130. }
  131. let closestXValue = self[closest].x
  132. switch rounding {
  133. case .up:
  134. // If rounding up, and found x-value is lower than specified x, and we can go upper...
  135. if closestXValue < xValue, closest < index(before: endIndex)
  136. {
  137. formIndex(after: &closest)
  138. }
  139. case .down:
  140. // If rounding down, and found x-value is upper than specified x, and we can go lower...
  141. if closestXValue > xValue, closest > startIndex
  142. {
  143. formIndex(before: &closest)
  144. }
  145. case .closest:
  146. break
  147. }
  148. // Search by closest to y-value
  149. if !yValue.isNaN
  150. {
  151. while closest > startIndex, self[index(before: closest)].x == closestXValue
  152. {
  153. formIndex(before: &closest)
  154. }
  155. var closestYValue = self[closest].y
  156. var closestYIndex = closest
  157. while closest < index(before: endIndex)
  158. {
  159. formIndex(after: &closest)
  160. let value = self[closest]
  161. if value.x != closestXValue { break }
  162. if abs(value.y - yValue) <= abs(closestYValue - yValue)
  163. {
  164. closestYValue = yValue
  165. closestYIndex = closest
  166. }
  167. }
  168. closest = closestYIndex
  169. }
  170. return closest
  171. }
  172. }