StatRootView.swift 33 KB

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