StatRootView.swift 32 KB

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