StatsView.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. //
  2. // FilteredLoopsView.swift
  3. // FreeAPS
  4. //
  5. // Created by Jon Mårtensson on 2023-05-29.
  6. //
  7. import CoreData
  8. import SwiftDate
  9. import SwiftUI
  10. struct StatsView: View {
  11. @FetchRequest var fetchRequest: FetchedResults<LoopStatRecord>
  12. @FetchRequest var fetchRequestReadings: FetchedResults<Readings>
  13. @State var headline: Color = .secondary
  14. @Binding var highLimit: Decimal
  15. @Binding var lowLimit: Decimal
  16. @Binding var units: GlucoseUnits
  17. @Binding var overrideUnit: Bool
  18. private let conversionFactor = 0.0555
  19. var body: some View {
  20. VStack(spacing: 10) {
  21. loops
  22. Divider()
  23. hba1c
  24. Divider()
  25. bloodGlucose
  26. }
  27. }
  28. init(
  29. filter: NSDate,
  30. _ highLimit: Binding<Decimal>,
  31. _ lowLimit: Binding<Decimal>,
  32. _ units: Binding<GlucoseUnits>,
  33. _ overrideUnit: Binding<Bool>
  34. ) {
  35. _fetchRequest = FetchRequest<LoopStatRecord>(
  36. sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)],
  37. predicate: NSPredicate(format: "interval > 0 AND start > %@", filter)
  38. )
  39. _fetchRequestReadings = FetchRequest<Readings>(
  40. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
  41. predicate: NSPredicate(format: "glucose > 0 AND date > %@", filter)
  42. )
  43. _highLimit = highLimit
  44. _lowLimit = lowLimit
  45. _units = units
  46. _overrideUnit = overrideUnit
  47. }
  48. var loops: some View {
  49. VStack(spacing: 10) {
  50. let loops = fetchRequest
  51. if !loops.isEmpty {
  52. // First date
  53. let previous = loops.last?.end ?? Date()
  54. // Last date (recent)
  55. let current = loops.first?.start ?? Date()
  56. // Total time in days
  57. let totalTime = (current - previous).timeInterval / 8.64E4
  58. let durationArray = loops.compactMap({ each in each.duration })
  59. let durationArrayCount = durationArray.count
  60. // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
  61. let medianDuration = medianCalculationDouble(array: durationArray)
  62. let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") })
  63. .count
  64. let errorNR = durationArrayCount - successsNR
  65. let successRate: Double? = (Double(successsNR) / Double(successsNR + errorNR)) * 100
  66. let loopNr = totalTime <= 1 ? Double(successsNR + errorNR) : round(Double(successsNR + errorNR) / totalTime)
  67. let intervalArray = loops.compactMap({ each in each.interval as Double })
  68. let intervalAverage = intervalArray.reduce(0, +) / Double(intervalArray.count)
  69. // let maximumInterval = intervalArray.max()
  70. // let minimumInterval = intervalArray.min()
  71. HStack(spacing: 35) {
  72. VStack(spacing: 5) {
  73. Text("Loops").font(.subheadline).foregroundColor(headline)
  74. Text(loopNr.formatted())
  75. }
  76. VStack(spacing: 5) {
  77. Text("Interval").font(.subheadline).foregroundColor(headline)
  78. Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " min")
  79. }
  80. VStack(spacing: 5) {
  81. Text("Duration").font(.subheadline).foregroundColor(headline)
  82. Text(
  83. (medianDuration * 60)
  84. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " s"
  85. )
  86. }
  87. VStack(spacing: 5) {
  88. Text("Sucess").font(.subheadline).foregroundColor(headline)
  89. Text(
  90. ((successRate ?? 100) / 100)
  91. .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
  92. )
  93. }
  94. }
  95. }
  96. }
  97. }
  98. private func medianCalculation(array: [Int]) -> Double {
  99. guard !array.isEmpty else {
  100. return 0
  101. }
  102. let sorted = array.sorted()
  103. let length = array.count
  104. if length % 2 == 0 {
  105. return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
  106. }
  107. return Double(sorted[length / 2])
  108. }
  109. private func medianCalculationDouble(array: [Double]) -> Double {
  110. guard !array.isEmpty else {
  111. return 0
  112. }
  113. let sorted = array.sorted()
  114. let length = array.count
  115. if length % 2 == 0 {
  116. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  117. }
  118. return sorted[length / 2]
  119. }
  120. var hba1c: some View {
  121. HStack(spacing: 50) {
  122. let useUnit: GlucoseUnits = (units == .mmolL && overrideUnit) ? .mgdL :
  123. (units == .mgdL && overrideUnit || units == .mmolL) ? .mmolL : .mgdL
  124. let hba1cs = glucoseStats()
  125. let glucose = fetchRequestReadings
  126. // First date
  127. let previous = glucose.last?.date ?? Date()
  128. // Last date (recent)
  129. let current = glucose.first?.date ?? Date()
  130. // Total time in days
  131. let numberOfDays = (current - previous).timeInterval / 8.64E4
  132. let hba1cString = (
  133. useUnit == .mmolL ? hba1cs.ifcc
  134. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
  135. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
  136. + " %"
  137. )
  138. VStack(spacing: 5) {
  139. Text("HbA1C").font(.subheadline).foregroundColor(headline)
  140. Text(hba1cString)
  141. }
  142. VStack(spacing: 5) {
  143. Text("SD").font(.subheadline).foregroundColor(.secondary)
  144. Text(
  145. hba1cs.sd
  146. .formatted(
  147. .number.grouping(.never).rounded()
  148. .precision(.fractionLength(units == .mmolL ? 1 : 0))
  149. )
  150. )
  151. }
  152. VStack(spacing: 5) {
  153. Text("CV").font(.subheadline).foregroundColor(.secondary)
  154. Text(hba1cs.cv.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
  155. }
  156. VStack(spacing: 5) {
  157. Text("Days").font(.subheadline).foregroundColor(.secondary)
  158. Text(numberOfDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))))
  159. }
  160. }
  161. }
  162. var bloodGlucose: some View {
  163. HStack(spacing: 30) {
  164. let bgs = glucoseStats()
  165. let glucose = fetchRequestReadings
  166. // First date
  167. let previous = glucose.last?.date ?? Date()
  168. // Last date (recent)
  169. let current = glucose.first?.date ?? Date()
  170. // Total time in days
  171. let numberOfDays = (current - previous).timeInterval / 8.64E4
  172. VStack(spacing: 5) {
  173. Text(numberOfDays < 1 ? "Readings today" : "Readings / 24h").font(.subheadline)
  174. .foregroundColor(.secondary)
  175. Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
  176. }
  177. VStack(spacing: 5) {
  178. Text("Average").font(.subheadline).foregroundColor(headline)
  179. Text(
  180. bgs.average
  181. .formatted(
  182. .number.grouping(.never).rounded()
  183. .precision(.fractionLength(units == .mmolL ? 1 : 0))
  184. )
  185. )
  186. }
  187. VStack(spacing: 5) {
  188. Text("Median").font(.subheadline).foregroundColor(.secondary)
  189. Text(
  190. bgs.median
  191. .formatted(
  192. .number.grouping(.never).rounded()
  193. .precision(.fractionLength(units == .mmolL ? 1 : 0))
  194. )
  195. )
  196. }
  197. }
  198. }
  199. private func glucoseStats()
  200. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  201. {
  202. let glucose = fetchRequestReadings
  203. // First date
  204. let previous = glucose.last?.date ?? Date()
  205. // Last date (recent)
  206. let current = glucose.first?.date ?? Date()
  207. // Total time in days
  208. let numberOfDays = (current - previous).timeInterval / 8.64E4
  209. let denominator = numberOfDays < 1 ? 1 : numberOfDays
  210. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  211. let sumReadings = justGlucoseArray.reduce(0, +)
  212. let countReadings = justGlucoseArray.count
  213. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  214. let medianGlucose = medianCalculation(array: justGlucoseArray)
  215. var NGSPa1CStatisticValue = 0.0
  216. var IFCCa1CStatisticValue = 0.0
  217. if numberOfDays > 0 {
  218. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
  219. IFCCa1CStatisticValue = 10.929 *
  220. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  221. }
  222. var sumOfSquares = 0.0
  223. for array in justGlucoseArray {
  224. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  225. }
  226. var sd = 0.0
  227. var cv = 0.0
  228. // Avoid division by zero
  229. if glucoseAverage > 0 {
  230. sd = sqrt(sumOfSquares / Double(countReadings))
  231. cv = sd / Double(glucoseAverage) * 100
  232. }
  233. var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  234. output = (
  235. ifcc: IFCCa1CStatisticValue,
  236. ngsp: NGSPa1CStatisticValue,
  237. average: glucoseAverage * (units == .mmolL ? conversionFactor : 1),
  238. median: medianGlucose * (units == .mmolL ? conversionFactor : 1),
  239. sd: sd * (units == .mmolL ? conversionFactor : 1), cv: cv,
  240. readings: Double(countReadings) / denominator
  241. )
  242. return output
  243. }
  244. private func tir() -> [(decimal: Decimal, string: String)] {
  245. let glucose = fetchRequestReadings
  246. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  247. let totalReadings = justGlucoseArray.count
  248. let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
  249. let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
  250. let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
  251. let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
  252. let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
  253. let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
  254. let tir = 100 - (hypoPercentage + hyperPercentage)
  255. var array: [(decimal: Decimal, string: String)] = []
  256. array.append((decimal: Decimal(hypoPercentage), string: "Low"))
  257. array.append((decimal: Decimal(tir), string: "NormaL"))
  258. array.append((decimal: Decimal(hyperPercentage), string: "High"))
  259. return array
  260. }
  261. }