ChartsView.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. //
  2. // FilteredLoopsView.swift
  3. // FreeAPS
  4. //
  5. // Created by Jon Mårtensson on 2023-05-29.
  6. //
  7. import Charts
  8. import CoreData
  9. import SwiftDate
  10. import SwiftUI
  11. struct ChartsView: View {
  12. @FetchRequest var fetchRequest: FetchedResults<Readings>
  13. @Binding var highLimit: Decimal
  14. @Binding var lowLimit: Decimal
  15. @Binding var units: GlucoseUnits
  16. @Binding var overrideUnit: Bool
  17. @Binding var standing: Bool
  18. @State var headline: Color = .secondary
  19. private let conversionFactor = 0.0555
  20. var body: some View {
  21. glucoseChart
  22. Rectangle().fill(.cyan.opacity(0.2)).frame(maxHeight: 3)
  23. if standing { tirChart } else { standingTIRchart }
  24. }
  25. init(
  26. filter: NSDate,
  27. _ highLimit: Binding<Decimal>,
  28. _ lowLimit: Binding<Decimal>,
  29. _ units: Binding<GlucoseUnits>,
  30. _ overrideUnit: Binding<Bool>,
  31. _ standing: Binding<Bool>
  32. ) { _fetchRequest = FetchRequest<Readings>(
  33. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
  34. predicate: NSPredicate(format: "glucose > 0 AND date > %@", filter)
  35. )
  36. _highLimit = highLimit
  37. _lowLimit = lowLimit
  38. _units = units
  39. _overrideUnit = overrideUnit
  40. _standing = standing
  41. }
  42. var glucoseChart: some View {
  43. // Be aware of the low/lowLimit difference. lowLimit/highLimit is always in mg/dl, whereas low/high is configurable in settings
  44. let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
  45. let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
  46. let readings = fetchRequest
  47. let count = readings.count
  48. // The symbol size when fewer readings are larger
  49. let sizeOfDataPoints: CGFloat = count < 20 ? 50 : count < 50 ? 35 : count > 2000 ? 5 : 15
  50. return Chart {
  51. ForEach(readings.filter({ $0.glucose > Int(highLimit) }), id: \.date) { item in
  52. PointMark(
  53. x: .value("Date", item.date ?? Date()),
  54. y: .value("High", Double(item.glucose) * (units == .mmolL ? self.conversionFactor : 1))
  55. )
  56. .foregroundStyle(.orange)
  57. .symbolSize(sizeOfDataPoints)
  58. }
  59. ForEach(
  60. readings
  61. .filter({
  62. $0.glucose >= Int(lowLimit) && $0
  63. .glucose <= Int(highLimit) }),
  64. id: \.date
  65. ) { item in
  66. PointMark(
  67. x: .value("Date", item.date ?? Date()),
  68. y: .value("In Range", Double(item.glucose) * (units == .mmolL ? conversionFactor : 1))
  69. )
  70. .foregroundStyle(.green)
  71. .symbolSize(sizeOfDataPoints)
  72. }
  73. ForEach(readings.filter({ $0.glucose < Int(lowLimit) }), id: \.date) { item in
  74. PointMark(
  75. x: .value("Date", item.date ?? Date()),
  76. y: .value("Low", Double(item.glucose) * (units == .mmolL ? conversionFactor : 1))
  77. )
  78. .foregroundStyle(.red)
  79. .symbolSize(sizeOfDataPoints)
  80. }
  81. }
  82. .chartYAxis {
  83. AxisMarks(
  84. values: [
  85. 0,
  86. low,
  87. high,
  88. units == .mmolL ? 15 : 270
  89. ]
  90. )
  91. } // .background(.gray.opacity(0.05))
  92. }
  93. var tirChart: some View {
  94. let fetched = tir()
  95. let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
  96. let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
  97. let data: [ShapeModel] = [
  98. .init(
  99. type: NSLocalizedString(
  100. "Low",
  101. comment: ""
  102. ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
  103. percent: fetched[0].decimal
  104. ),
  105. .init(type: NSLocalizedString("In Range", comment: ""), percent: fetched[1].decimal),
  106. .init(
  107. type: NSLocalizedString(
  108. "High",
  109. comment: ""
  110. ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
  111. percent: fetched[2].decimal
  112. )
  113. ]
  114. return Chart(data) { shape in
  115. BarMark(
  116. x: .value("TIR", shape.percent)
  117. )
  118. .foregroundStyle(by: .value("Group", shape.type))
  119. .annotation(position: .top, alignment: .center) {
  120. Text(
  121. "\(shape.percent, format: .number.precision(.fractionLength(0))) %"
  122. ).font(.footnote).foregroundColor(.secondary)
  123. }
  124. }
  125. .chartXAxis(.hidden)
  126. .chartForegroundStyleScale([
  127. NSLocalizedString(
  128. "Low",
  129. comment: ""
  130. ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .red,
  131. NSLocalizedString("In Range", comment: ""): .green,
  132. NSLocalizedString(
  133. "High",
  134. comment: ""
  135. ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .orange
  136. ]).frame(maxHeight: 25)
  137. }
  138. var standingTIRchart: some View {
  139. let fetched = tir()
  140. let low = lowLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
  141. let high = highLimit * (units == .mmolL ? Decimal(conversionFactor) : 1)
  142. let data: [ShapeModel] = [
  143. .init(
  144. type: NSLocalizedString(
  145. "Low",
  146. comment: ""
  147. ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
  148. percent: fetched[0].decimal
  149. ),
  150. .init(type: NSLocalizedString("In Range", comment: ""), percent: fetched[1].decimal),
  151. .init(
  152. type: NSLocalizedString(
  153. "High",
  154. comment: ""
  155. ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))",
  156. percent: fetched[2].decimal
  157. )
  158. ]
  159. return VStack(alignment: .center) {
  160. Chart(data) { shape in
  161. BarMark(
  162. x: .value("Shape", shape.type),
  163. y: .value("Percentage", shape.percent)
  164. )
  165. .foregroundStyle(by: .value("Group", shape.type))
  166. .annotation(position: shape.percent <= 9 ? .top : .overlay, alignment: .center) {
  167. Text(shape.percent == 0 ? "" : "\(shape.percent, format: .number.precision(.fractionLength(0))) %")
  168. }
  169. }
  170. .chartYAxis(.hidden)
  171. .chartLegend(.hidden)
  172. .chartForegroundStyleScale([
  173. NSLocalizedString(
  174. "Low",
  175. comment: ""
  176. ) + " (\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .red,
  177. NSLocalizedString("In Range", comment: ""): .green,
  178. NSLocalizedString(
  179. "High",
  180. comment: ""
  181. ) + " (\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))))": .orange
  182. ])
  183. }
  184. }
  185. private func tir() -> [(decimal: Decimal, string: String)] {
  186. let hypoLimit = Int(lowLimit)
  187. let hyperLimit = Int(highLimit)
  188. let glucose = fetchRequest
  189. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  190. let totalReadings = justGlucoseArray.count
  191. let hyperArray = glucose.filter({ $0.glucose >= hyperLimit })
  192. let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
  193. let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
  194. let hypoArray = glucose.filter({ $0.glucose <= hypoLimit })
  195. let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
  196. let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
  197. let tir = 100 - (hypoPercentage + hyperPercentage)
  198. var array: [(decimal: Decimal, string: String)] = []
  199. array.append((decimal: Decimal(hypoPercentage), string: "Low"))
  200. array.append((decimal: Decimal(tir), string: "NormaL"))
  201. array.append((decimal: Decimal(hyperPercentage), string: "High"))
  202. return array
  203. }
  204. }