StatRootView.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. import Charts
  2. import CoreData
  3. import SwiftDate
  4. import SwiftUI
  5. import Swinject
  6. extension Stat {
  7. struct RootView: BaseView {
  8. let resolver: Resolver
  9. @StateObject var state = StateModel()
  10. // @Environment(\.managedObjectContext) var moc
  11. @FetchRequest(
  12. entity: Readings.entity(),
  13. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
  14. format: "date >= %@", Calendar.current.startOfDay(for: Date()) as NSDate
  15. )
  16. ) var fetchedGlucoseDay: FetchedResults<Readings>
  17. @FetchRequest(
  18. entity: Readings.entity(),
  19. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
  20. predicate: NSPredicate(format: "date > %@", Date().addingTimeInterval(-24.hours.timeInterval) as NSDate)
  21. ) var fetchedGlucoseTwentyFourHours: FetchedResults<Readings>
  22. @FetchRequest(
  23. entity: Readings.entity(),
  24. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
  25. predicate: NSPredicate(format: "date > %@", Date().addingTimeInterval(-7.days.timeInterval) as NSDate)
  26. ) var fetchedGlucoseWeek: FetchedResults<Readings>
  27. @FetchRequest(
  28. entity: Readings.entity(),
  29. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
  30. format: "date > %@",
  31. Date().addingTimeInterval(-30.days.timeInterval) as NSDate
  32. )
  33. ) var fetchedGlucoseMonth: FetchedResults<Readings>
  34. @FetchRequest(
  35. entity: Readings.entity(),
  36. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)], predicate: NSPredicate(
  37. format: "date > %@",
  38. Date().addingTimeInterval(-90.days.timeInterval) as NSDate
  39. )
  40. ) var fetchedGlucose: FetchedResults<Readings>
  41. @FetchRequest(
  42. entity: TDD.entity(),
  43. sortDescriptors: [NSSortDescriptor(key: "timestamp", ascending: false)]
  44. ) var fetchedTDD: FetchedResults<TDD>
  45. @FetchRequest(
  46. entity: LoopStatRecord.entity(),
  47. sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)], predicate: NSPredicate(
  48. format: "start > %@",
  49. Date().addingTimeInterval(-24.hours.timeInterval) as NSDate
  50. )
  51. ) var fetchedLoopStats: FetchedResults<LoopStatRecord>
  52. @FetchRequest(
  53. entity: InsulinDistribution.entity(),
  54. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
  55. ) var fetchedInsulin: FetchedResults<InsulinDistribution>
  56. enum Duration: String, CaseIterable, Identifiable {
  57. case Today
  58. case Day
  59. case Week
  60. case Month
  61. case Total
  62. var id: Self { self }
  63. }
  64. @State private var selectedDuration: Duration = .Today
  65. @State var paddingAmount: CGFloat? = 10
  66. @State var headline: Color = .secondary
  67. @State var days: Double = 0
  68. @State var pointSize: CGFloat = 3
  69. @State var conversionFactor = 0.0555
  70. @ViewBuilder func stats() -> some View {
  71. bloodGlucose
  72. Divider()
  73. loops
  74. Divider()
  75. hba1c
  76. }
  77. @ViewBuilder func chart() -> some View {
  78. switch selectedDuration {
  79. case .Today:
  80. glucoseChart
  81. case .Day:
  82. glucoseChartTwentyFourHours
  83. case .Week:
  84. glucoseChartWeek
  85. case .Month:
  86. glucoseChartMonth
  87. case .Total:
  88. glucoseChart90
  89. }
  90. tirChart
  91. }
  92. var body: some View {
  93. ZStack {
  94. VStack(alignment: .center) {
  95. chart().padding(.top, 20)
  96. Divider()
  97. stats()
  98. Divider()
  99. Picker("Duration", selection: $selectedDuration) {
  100. ForEach(Duration.allCases) { duration in
  101. Text(duration.rawValue).tag(Optional(duration))
  102. }
  103. }
  104. .pickerStyle(.segmented)
  105. }
  106. }
  107. .onAppear(perform: configureView)
  108. .navigationBarTitle("Statistics")
  109. .navigationBarTitleDisplayMode(.automatic)
  110. .navigationBarItems(leading: Button("Close", action: state.hideModal))
  111. }
  112. var loops: some View {
  113. VStack {
  114. let loops_ = loopStats(fetchedLoopStats)
  115. HStack {
  116. ForEach(0 ..< loops_.count, id: \.self) { index in
  117. VStack {
  118. Text(loops_[index].string).font(.subheadline).foregroundColor(.secondary)
  119. Text(
  120. index == 0 ? loops_[index].double.formatted() : (
  121. index == 2 ? loops_[index].double
  122. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(2))) :
  123. loops_[index]
  124. .double
  125. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
  126. )
  127. )
  128. }.padding(.horizontal, 6)
  129. }
  130. }
  131. }
  132. }
  133. var hba1c: some View {
  134. let useUnit: GlucoseUnits = (state.units == .mmolL && (state.overrideUnit ?? false)) ? .mgdL :
  135. (state.units == .mgdL && (state.overrideUnit ?? false) || state.units == .mmolL) ? .mmolL : .mgdL
  136. return HStack {
  137. let hba1cs = glucoseStats(fetchedGlucose)
  138. let hba1cString = (
  139. useUnit == .mmolL ? hba1cs.ifcc
  140. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
  141. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
  142. + " %"
  143. )
  144. VStack {
  145. Text("HbA1C").font(.subheadline).foregroundColor(headline)
  146. HStack {
  147. VStack {
  148. Text(hba1cString)
  149. }
  150. }
  151. }.padding([.horizontal], 15)
  152. VStack {
  153. Text("SD").font(.subheadline).foregroundColor(.secondary)
  154. HStack {
  155. VStack {
  156. Text(
  157. hba1cs.sd
  158. .formatted(
  159. .number.grouping(.never).rounded()
  160. .precision(.fractionLength(state.units == .mmolL ? 1 : 0))
  161. )
  162. )
  163. }
  164. }
  165. }.padding([.horizontal], 15)
  166. VStack {
  167. Text("CV").font(.subheadline).foregroundColor(.secondary)
  168. HStack {
  169. VStack {
  170. Text(
  171. hba1cs.cv.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
  172. )
  173. }
  174. }
  175. }.padding([.horizontal], 15)
  176. // if selectedDuration == .Total || selectedDuration == .Today {
  177. VStack {
  178. Text("Days").font(.subheadline).foregroundColor(.secondary)
  179. HStack {
  180. VStack {
  181. Text(numberOfDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))))
  182. }
  183. }
  184. }.padding([.horizontal], 15)
  185. // }
  186. }
  187. }
  188. var bloodGlucose: some View {
  189. VStack {
  190. HStack {
  191. let bgs = glucoseStats(fetchedGlucose)
  192. VStack {
  193. HStack {
  194. Text(selectedDuration == .Today ? "Readings today" : "Readings / 24h").font(.subheadline)
  195. .foregroundColor(.secondary)
  196. }
  197. HStack {
  198. VStack {
  199. Text(
  200. bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
  201. )
  202. }
  203. }
  204. }
  205. VStack {
  206. HStack {
  207. Text("Average").font(.subheadline).foregroundColor(headline)
  208. }
  209. HStack {
  210. VStack {
  211. Text(
  212. bgs.average
  213. .formatted(
  214. .number.grouping(.never).rounded()
  215. .precision(.fractionLength(state.units == .mmolL ? 1 : 0))
  216. )
  217. )
  218. }
  219. }
  220. }
  221. VStack {
  222. HStack {
  223. Text("Median").font(.subheadline).foregroundColor(.secondary)
  224. }
  225. HStack {
  226. VStack {
  227. Text(
  228. bgs.median
  229. .formatted(
  230. .number.grouping(.never).rounded()
  231. .precision(.fractionLength(state.units == .mmolL ? 1 : 0))
  232. )
  233. )
  234. }
  235. }
  236. }
  237. }
  238. }
  239. }
  240. var numberOfDays: Double {
  241. let array = selectedDuration == .Today ? fetchedGlucoseDay : selectedDuration == .Day ?
  242. fetchedGlucoseTwentyFourHours :
  243. selectedDuration == .Week ? fetchedGlucoseWeek : selectedDuration == .Month ? fetchedGlucoseMonth :
  244. selectedDuration ==
  245. .Total ? fetchedGlucose : fetchedGlucoseDay
  246. let endIndex = array.count - 1
  247. var days = 0.0
  248. if endIndex > 0 {
  249. let firstElementTime = fetchedGlucose.first?.date ?? Date()
  250. let lastElementTime = fetchedGlucose[endIndex].date ?? Date()
  251. days = (firstElementTime - lastElementTime).timeInterval / 8.64E4
  252. }
  253. return days
  254. }
  255. var tirChart: some View {
  256. let array = selectedDuration == .Today ? fetchedGlucoseDay : selectedDuration == .Day ?
  257. fetchedGlucoseTwentyFourHours :
  258. selectedDuration == .Week ? fetchedGlucoseWeek : selectedDuration == .Month ? fetchedGlucoseMonth :
  259. selectedDuration ==
  260. .Total ? fetchedGlucose : fetchedGlucoseDay
  261. let fetched = tir(array)
  262. let data: [ShapeModel] = [
  263. .init(type: "Low", percent: fetched[0].decimal),
  264. .init(type: "In Range", percent: fetched[1].decimal),
  265. .init(type: "High", percent: fetched[2].decimal)
  266. ]
  267. return Chart(data) { shape in
  268. BarMark(
  269. x: .value("TIR", shape.percent)
  270. )
  271. .foregroundStyle(by: .value("Group", shape.type))
  272. .annotation(position: .overlay, alignment: .center) {
  273. Text(
  274. shape.percent == 0 ? "" : shape
  275. .percent < 12 ? "\(shape.percent, format: .number.precision(.fractionLength(0)))" :
  276. "\(shape.percent, format: .number.precision(.fractionLength(0))) %"
  277. )
  278. }
  279. }
  280. .chartXAxis(.hidden)
  281. .chartForegroundStyleScale(["Low": .red, "In Range": .green, "High": .orange]).frame(maxHeight: 55)
  282. }
  283. var glucoseChart: some View {
  284. let count = fetchedGlucoseDay.count
  285. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  286. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  287. return Chart {
  288. ForEach(fetchedGlucoseDay.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  289. PointMark(
  290. x: .value("Date", item.date ?? Date()),
  291. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  292. )
  293. .foregroundStyle(.orange)
  294. .symbolSize(count < 20 ? 30 : 12)
  295. }
  296. ForEach(
  297. fetchedGlucoseDay
  298. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  299. id: \.date
  300. ) { item in
  301. PointMark(
  302. x: .value("Date", item.date ?? Date()),
  303. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  304. )
  305. .foregroundStyle(.green)
  306. .symbolSize(count < 20 ? 30 : 12)
  307. }
  308. ForEach(fetchedGlucoseDay.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  309. PointMark(
  310. x: .value("Date", item.date ?? Date()),
  311. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  312. )
  313. .foregroundStyle(.red)
  314. .symbolSize(count < 20 ? 30 : 12)
  315. }
  316. }
  317. .chartYAxis {
  318. AxisMarks(
  319. values: [
  320. 0,
  321. lowLimit,
  322. highLimit,
  323. state.units == .mmolL ? 15 : 270
  324. ]
  325. )
  326. }
  327. }
  328. var glucoseChartTwentyFourHours: some View {
  329. let count = fetchedGlucoseTwentyFourHours.count
  330. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  331. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  332. return Chart {
  333. ForEach(fetchedGlucoseTwentyFourHours.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  334. PointMark(
  335. x: .value("Date", item.date ?? Date()),
  336. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  337. )
  338. .foregroundStyle(.orange)
  339. .symbolSize(count < 20 ? 20 : 10)
  340. }
  341. ForEach(
  342. fetchedGlucoseTwentyFourHours
  343. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  344. id: \.date
  345. ) { item in
  346. PointMark(
  347. x: .value("Date", item.date ?? Date()),
  348. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  349. )
  350. .foregroundStyle(.green)
  351. .symbolSize(count < 20 ? 20 : 10)
  352. }
  353. ForEach(fetchedGlucoseTwentyFourHours.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  354. PointMark(
  355. x: .value("Date", item.date ?? Date()),
  356. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  357. )
  358. .foregroundStyle(.red)
  359. .symbolSize(count < 20 ? 20 : 10)
  360. }
  361. }
  362. .chartYAxis {
  363. AxisMarks(
  364. values: [
  365. 0,
  366. lowLimit,
  367. highLimit,
  368. state.units == .mmolL ? 15 : 270
  369. ]
  370. )
  371. } }
  372. var glucoseChartWeek: some View {
  373. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  374. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  375. return Chart {
  376. ForEach(fetchedGlucoseWeek.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  377. PointMark(
  378. x: .value("Date", item.date ?? Date()),
  379. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  380. )
  381. .foregroundStyle(.orange)
  382. .symbolSize(5)
  383. }
  384. ForEach(
  385. fetchedGlucoseWeek
  386. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  387. id: \.date
  388. ) { item in
  389. PointMark(
  390. x: .value("Date", item.date ?? Date()),
  391. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  392. )
  393. .foregroundStyle(.green)
  394. .symbolSize(5)
  395. }
  396. ForEach(fetchedGlucoseWeek.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  397. PointMark(
  398. x: .value("Date", item.date ?? Date()),
  399. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  400. )
  401. .foregroundStyle(.red)
  402. .symbolSize(5)
  403. }
  404. }
  405. .chartYAxis {
  406. AxisMarks(
  407. values: [
  408. 0,
  409. lowLimit,
  410. highLimit,
  411. state.units == .mmolL ? 15 : 270
  412. ]
  413. )
  414. }
  415. }
  416. var glucoseChartMonth: some View {
  417. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  418. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  419. return Chart {
  420. ForEach(fetchedGlucoseMonth.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  421. PointMark(
  422. x: .value("Date", item.date ?? Date()),
  423. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  424. )
  425. .foregroundStyle(.orange)
  426. .symbolSize(2)
  427. }
  428. ForEach(
  429. fetchedGlucoseMonth
  430. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  431. id: \.date
  432. ) { item in
  433. PointMark(
  434. x: .value("Date", item.date ?? Date()),
  435. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  436. )
  437. .foregroundStyle(.green)
  438. .symbolSize(2)
  439. }
  440. ForEach(fetchedGlucoseMonth.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  441. PointMark(
  442. x: .value("Date", item.date ?? Date()),
  443. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  444. )
  445. .foregroundStyle(.red)
  446. .symbolSize(2)
  447. }
  448. }
  449. .chartYAxis {
  450. AxisMarks(
  451. values: [
  452. 0,
  453. lowLimit,
  454. highLimit,
  455. state.units == .mmolL ? 15 : 270
  456. ]
  457. )
  458. }
  459. }
  460. var glucoseChart90: some View {
  461. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  462. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  463. return Chart {
  464. ForEach(fetchedGlucose.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  465. PointMark(
  466. x: .value("Date", item.date ?? Date()),
  467. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  468. )
  469. .foregroundStyle(.orange)
  470. .symbolSize(2)
  471. }
  472. ForEach(
  473. fetchedGlucose
  474. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  475. id: \.date
  476. ) { item in
  477. PointMark(
  478. x: .value("Date", item.date ?? Date()),
  479. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  480. )
  481. .foregroundStyle(.green)
  482. .symbolSize(2)
  483. }
  484. ForEach(fetchedGlucose.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  485. PointMark(
  486. x: .value("Date", item.date ?? Date()),
  487. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  488. )
  489. .foregroundStyle(.red)
  490. .symbolSize(2)
  491. }
  492. }
  493. .chartYAxis {
  494. AxisMarks(
  495. values: [
  496. 0,
  497. lowLimit,
  498. highLimit,
  499. state.units == .mmolL ? 15 : 270
  500. ]
  501. )
  502. }
  503. }
  504. private func loopStats(_ loops: FetchedResults<LoopStatRecord>) -> [(double: Double, string: String)] {
  505. guard (loops.first?.start) != nil else { return [] }
  506. var i = 0.0
  507. var minimumInt = 999.0
  508. var maximumInt = 0.0
  509. var timeIntervalLoops = 0.0
  510. var previousTimeLoop = loops.first?.end ?? Date()
  511. var timeIntervalLoopArray: [Double] = []
  512. let durationArray = loops.compactMap({ each in each.duration })
  513. let durationArrayCount = durationArray.count
  514. // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
  515. let medianDuration = medianCalculationDouble(array: durationArray)
  516. let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
  517. let errorNR = durationArrayCount - successsNR
  518. let successRate: Double? = (Double(successsNR) / Double(successsNR + errorNR)) * 100
  519. for each in loops {
  520. if let loopEnd = each.end {
  521. i += 1
  522. timeIntervalLoops = (previousTimeLoop - (each.start ?? previousTimeLoop)).timeInterval / 60
  523. if timeIntervalLoops > 0.0, i != 1 {
  524. timeIntervalLoopArray.append(timeIntervalLoops)
  525. }
  526. if timeIntervalLoops > maximumInt {
  527. maximumInt = timeIntervalLoops
  528. }
  529. if timeIntervalLoops < minimumInt, i != 1 {
  530. minimumInt = timeIntervalLoops
  531. }
  532. previousTimeLoop = loopEnd
  533. }
  534. }
  535. // Average Loop Interval in minutes
  536. let timeOfFirstIndex = loops.first?.start ?? Date()
  537. let lastIndexWithTimestamp = loops.count - 1
  538. let timeOfLastIndex = loops[lastIndexWithTimestamp].end ?? Date()
  539. let averageInterval = (timeOfFirstIndex - timeOfLastIndex).timeInterval / 60 / Double(errorNR + successsNR)
  540. if minimumInt == 999.0 {
  541. minimumInt = 0.0
  542. }
  543. var array: [(double: Double, string: String)] = []
  544. array.append((double: Double(successsNR + errorNR), string: "Loops"))
  545. array.append((double: averageInterval, string: "Interval"))
  546. array.append((double: medianDuration, string: "Duration"))
  547. array.append((double: successRate ?? 100, string: "%"))
  548. return array
  549. }
  550. private func medianCalculation(array: [Int]) -> Double {
  551. guard !array.isEmpty else {
  552. return 0
  553. }
  554. let sorted = array.sorted()
  555. let length = array.count
  556. if length % 2 == 0 {
  557. return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
  558. }
  559. return Double(sorted[length / 2])
  560. }
  561. private func medianCalculationDouble(array: [Double]) -> Double {
  562. guard !array.isEmpty else {
  563. return 0
  564. }
  565. let sorted = array.sorted()
  566. let length = array.count
  567. if length % 2 == 0 {
  568. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  569. }
  570. return sorted[length / 2]
  571. }
  572. private func glucoseStats(_ glucose_90: FetchedResults<Readings>)
  573. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  574. {
  575. var numberOfDays: Double = 0
  576. let endIndex = glucose_90.count - 1
  577. if endIndex > 0 {
  578. let firstElementTime = glucose_90[0].date ?? Date()
  579. let lastElementTime = glucose_90[endIndex].date ?? Date()
  580. numberOfDays = (firstElementTime - lastElementTime).timeInterval / 8.64E4
  581. }
  582. var duration = 1
  583. var denominator: Double = 1
  584. switch selectedDuration {
  585. case .Today:
  586. let minutesSinceMidnight = Calendar.current.component(.hour, from: Date()) * 60 + Calendar.current
  587. .component(.minute, from: Date())
  588. duration = minutesSinceMidnight
  589. denominator = 1
  590. case .Day:
  591. duration = 1 * 1440
  592. denominator = 1
  593. case .Week:
  594. duration = 7 * 1440
  595. if numberOfDays > 7 { denominator = 7 } else { denominator = numberOfDays }
  596. case .Month:
  597. duration = 30 * 1440
  598. if numberOfDays > 30 { denominator = 30 } else { denominator = numberOfDays }
  599. case .Total:
  600. duration = 90 * 1440
  601. if numberOfDays >= 90 { denominator = 90 } else { denominator = numberOfDays }
  602. }
  603. let timeAgo = Date().addingTimeInterval(-duration.minutes.timeInterval)
  604. let glucose = glucose_90.filter({ ($0.date ?? Date()) >= timeAgo })
  605. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  606. let sumReadings = justGlucoseArray.reduce(0, +)
  607. let countReadings = justGlucoseArray.count
  608. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  609. let medianGlucose = medianCalculation(array: justGlucoseArray)
  610. var NGSPa1CStatisticValue = 0.0
  611. var IFCCa1CStatisticValue = 0.0
  612. if numberOfDays > 0 {
  613. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
  614. IFCCa1CStatisticValue = 10.929 *
  615. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  616. }
  617. var sumOfSquares = 0.0
  618. for array in justGlucoseArray {
  619. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  620. }
  621. var sd = 0.0
  622. var cv = 0.0
  623. // Avoid division by zero
  624. if glucoseAverage > 0 {
  625. sd = sqrt(sumOfSquares / Double(countReadings))
  626. cv = sd / Double(glucoseAverage) * 100
  627. }
  628. var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  629. output = (
  630. ifcc: IFCCa1CStatisticValue,
  631. ngsp: NGSPa1CStatisticValue,
  632. average: glucoseAverage * (state.units == .mmolL ? conversionFactor : 1),
  633. median: medianGlucose * (state.units == .mmolL ? conversionFactor : 1),
  634. sd: sd * (state.units == .mmolL ? conversionFactor : 1), cv: cv,
  635. readings: Double(countReadings) / denominator
  636. )
  637. return output
  638. }
  639. private func tir(_ glucose_90: FetchedResults<Readings>) -> [(decimal: Decimal, string: String)] {
  640. var duration = 1
  641. switch selectedDuration {
  642. case .Today:
  643. let minutesSinceMidnight = Calendar.current.component(.hour, from: Date()) * 60 + Calendar.current
  644. .component(.minute, from: Date())
  645. duration = minutesSinceMidnight
  646. case .Day:
  647. duration = 1 * 1440
  648. case .Week:
  649. duration = 7 * 1440
  650. case .Month:
  651. duration = 30 * 1440
  652. case .Total:
  653. duration = 90 * 1440
  654. }
  655. let hypoLimit = Int(state.lowLimit ?? 70)
  656. let hyperLimit = Int(state.highLimit ?? 145)
  657. let timeAgo = Date().addingTimeInterval(-duration.minutes.timeInterval)
  658. let glucose = glucose_90.filter({ ($0.date ?? Date()) >= timeAgo })
  659. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  660. let totalReadings = justGlucoseArray.count
  661. let hyperArray = glucose.filter({ $0.glucose >= hyperLimit })
  662. let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
  663. let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
  664. let hypoArray = glucose.filter({ $0.glucose <= hypoLimit })
  665. let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
  666. let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
  667. let tir = 100 - (hypoPercentage + hyperPercentage)
  668. var array: [(decimal: Decimal, string: String)] = []
  669. array.append((decimal: Decimal(hypoPercentage), string: "Low"))
  670. array.append((decimal: Decimal(tir), string: "NormaL"))
  671. array.append((decimal: Decimal(hyperPercentage), string: "High"))
  672. return array
  673. }
  674. private func colorOfGlucose(_ index: Int) -> Color {
  675. let whichIndex = index
  676. switch whichIndex {
  677. case 0:
  678. return .red
  679. case 1:
  680. return .green
  681. case 2:
  682. return .orange
  683. default:
  684. return .primary
  685. }
  686. }
  687. }
  688. }