StatsView.swift 11 KB

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