StatRootView.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  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. // Spacer()
  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 {
  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 endIndex = fetchedGlucose.count - 1
  242. var days = 0.0
  243. if endIndex > 0 {
  244. let firstElementTime = fetchedGlucose.first?.date ?? Date()
  245. let lastElementTime = fetchedGlucose[endIndex].date ?? Date()
  246. days = (firstElementTime - lastElementTime).timeInterval / 8.64E4
  247. }
  248. return days
  249. }
  250. var tirChart: some View {
  251. let array = selectedDuration == .Today ? fetchedGlucoseDay : selectedDuration == .Day ?
  252. fetchedGlucoseTwentyFourHours :
  253. selectedDuration == .Week ? fetchedGlucoseWeek : selectedDuration == .Month ? fetchedGlucoseMonth :
  254. selectedDuration ==
  255. .Total ? fetchedGlucose : fetchedGlucoseDay
  256. let fetched = tir(array)
  257. let data: [ShapeModel] = [
  258. .init(type: "Low", percent: fetched[0].decimal),
  259. .init(type: "In Range", percent: fetched[1].decimal),
  260. .init(type: "High", percent: fetched[2].decimal)
  261. ]
  262. return Chart(data) { shape in
  263. BarMark(
  264. x: .value("TIR", shape.percent)
  265. )
  266. .foregroundStyle(by: .value("Group", shape.type))
  267. .annotation(position: .overlay, alignment: .center) {
  268. Text(
  269. shape.percent == 0 ? "" : shape
  270. .percent < 12 ? "\(shape.percent, format: .number.precision(.fractionLength(0)))" :
  271. "\(shape.percent, format: .number.precision(.fractionLength(0))) %"
  272. )
  273. }
  274. }
  275. .chartXAxis(.hidden)
  276. .chartForegroundStyleScale(["Low": .red, "In Range": .green, "High": .orange]).frame(maxHeight: 45)
  277. }
  278. var glucoseChart: some View {
  279. let count = fetchedGlucoseDay.count
  280. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  281. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  282. return Chart {
  283. ForEach(fetchedGlucoseDay.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  284. PointMark(
  285. x: .value("Date", item.date ?? Date()),
  286. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  287. )
  288. .foregroundStyle(.orange)
  289. .symbolSize(count < 20 ? 30 : 12)
  290. }
  291. ForEach(
  292. fetchedGlucoseDay
  293. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  294. id: \.date
  295. ) { item in
  296. PointMark(
  297. x: .value("Date", item.date ?? Date()),
  298. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  299. )
  300. .foregroundStyle(.green)
  301. .symbolSize(count < 20 ? 30 : 12)
  302. }
  303. ForEach(fetchedGlucoseDay.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  304. PointMark(
  305. x: .value("Date", item.date ?? Date()),
  306. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  307. )
  308. .foregroundStyle(.red)
  309. .symbolSize(count < 20 ? 30 : 12)
  310. }
  311. }
  312. .chartYAxis {
  313. AxisMarks(
  314. values: [
  315. 0,
  316. lowLimit,
  317. highLimit,
  318. state.units == .mmolL ? 15 : 270
  319. ]
  320. )
  321. }
  322. }
  323. var glucoseChartTwentyFourHours: some View {
  324. let count = fetchedGlucoseTwentyFourHours.count
  325. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  326. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  327. return Chart {
  328. ForEach(fetchedGlucoseTwentyFourHours.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  329. PointMark(
  330. x: .value("Date", item.date ?? Date()),
  331. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  332. )
  333. .foregroundStyle(.orange)
  334. .symbolSize(count < 20 ? 20 : 10)
  335. }
  336. ForEach(
  337. fetchedGlucoseTwentyFourHours
  338. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  339. id: \.date
  340. ) { item in
  341. PointMark(
  342. x: .value("Date", item.date ?? Date()),
  343. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  344. )
  345. .foregroundStyle(.green)
  346. .symbolSize(count < 20 ? 20 : 10)
  347. }
  348. ForEach(fetchedGlucoseTwentyFourHours.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  349. PointMark(
  350. x: .value("Date", item.date ?? Date()),
  351. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  352. )
  353. .foregroundStyle(.red)
  354. .symbolSize(count < 20 ? 20 : 10)
  355. }
  356. }
  357. .chartYAxis {
  358. AxisMarks(
  359. values: [
  360. 0,
  361. lowLimit,
  362. highLimit,
  363. state.units == .mmolL ? 15 : 270
  364. ]
  365. )
  366. } }
  367. var glucoseChartWeek: some View {
  368. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  369. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  370. return Chart {
  371. ForEach(fetchedGlucoseWeek.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  372. PointMark(
  373. x: .value("Date", item.date ?? Date()),
  374. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  375. )
  376. .foregroundStyle(.orange)
  377. .symbolSize(5)
  378. }
  379. ForEach(
  380. fetchedGlucoseWeek
  381. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  382. id: \.date
  383. ) { item in
  384. PointMark(
  385. x: .value("Date", item.date ?? Date()),
  386. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  387. )
  388. .foregroundStyle(.green)
  389. .symbolSize(5)
  390. }
  391. ForEach(fetchedGlucoseWeek.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  392. PointMark(
  393. x: .value("Date", item.date ?? Date()),
  394. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  395. )
  396. .foregroundStyle(.red)
  397. .symbolSize(5)
  398. }
  399. }
  400. .chartYAxis {
  401. AxisMarks(
  402. values: [
  403. 0,
  404. lowLimit,
  405. highLimit,
  406. state.units == .mmolL ? 15 : 270
  407. ]
  408. )
  409. }
  410. }
  411. var glucoseChartMonth: some View {
  412. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  413. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  414. return Chart {
  415. ForEach(fetchedGlucoseMonth.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  416. PointMark(
  417. x: .value("Date", item.date ?? Date()),
  418. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  419. )
  420. .foregroundStyle(.orange)
  421. .symbolSize(2)
  422. }
  423. ForEach(
  424. fetchedGlucoseMonth
  425. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  426. id: \.date
  427. ) { item in
  428. PointMark(
  429. x: .value("Date", item.date ?? Date()),
  430. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  431. )
  432. .foregroundStyle(.green)
  433. .symbolSize(2)
  434. }
  435. ForEach(fetchedGlucoseMonth.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  436. PointMark(
  437. x: .value("Date", item.date ?? Date()),
  438. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  439. )
  440. .foregroundStyle(.red)
  441. .symbolSize(2)
  442. }
  443. }
  444. .chartYAxis {
  445. AxisMarks(
  446. values: [
  447. 0,
  448. lowLimit,
  449. highLimit,
  450. state.units == .mmolL ? 15 : 270
  451. ]
  452. )
  453. }
  454. }
  455. var glucoseChart90: some View {
  456. let lowLimit = (state.lowLimit ?? (4 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  457. let highLimit = (state.highLimit ?? (10 * 0.0555)) * (state.units == .mmolL ? Decimal(conversionFactor) : 1)
  458. return Chart {
  459. ForEach(fetchedGlucose.filter({ $0.glucose > Int(state.highLimit ?? 145) }), id: \.date) { item in
  460. PointMark(
  461. x: .value("Date", item.date ?? Date()),
  462. y: .value("Low", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  463. )
  464. .foregroundStyle(.orange)
  465. .symbolSize(2)
  466. }
  467. ForEach(
  468. fetchedGlucose
  469. .filter({ $0.glucose >= Int(state.lowLimit ?? 70) && $0.glucose <= Int(state.highLimit ?? 145) }),
  470. id: \.date
  471. ) { item in
  472. PointMark(
  473. x: .value("Date", item.date ?? Date()),
  474. y: .value("In Range", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  475. )
  476. .foregroundStyle(.green)
  477. .symbolSize(2)
  478. }
  479. ForEach(fetchedGlucose.filter({ $0.glucose < Int(state.lowLimit ?? 70) }), id: \.date) { item in
  480. PointMark(
  481. x: .value("Date", item.date ?? Date()),
  482. y: .value("High", Double(item.glucose) * (state.units == .mmolL ? conversionFactor : 1))
  483. )
  484. .foregroundStyle(.red)
  485. .symbolSize(2)
  486. }
  487. }
  488. .chartYAxis {
  489. AxisMarks(
  490. values: [
  491. 0,
  492. lowLimit,
  493. highLimit,
  494. state.units == .mmolL ? 15 : 270
  495. ]
  496. )
  497. }
  498. }
  499. private func loopStats(_ loops: FetchedResults<LoopStatRecord>) -> [(double: Double, string: String)] {
  500. guard (loops.first?.start) != nil else { return [] }
  501. var i = 0.0
  502. var minimumInt = 999.0
  503. var maximumInt = 0.0
  504. var timeIntervalLoops = 0.0
  505. var previousTimeLoop = loops.first?.end ?? Date()
  506. var timeIntervalLoopArray: [Double] = []
  507. let durationArray = loops.compactMap({ each in each.duration })
  508. let durationArrayCount = durationArray.count
  509. // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
  510. let medianDuration = medianCalculationDouble(array: durationArray)
  511. let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
  512. let errorNR = durationArrayCount - successsNR
  513. let successRate: Double? = (Double(successsNR) / Double(successsNR + errorNR)) * 100
  514. for each in loops {
  515. if let loopEnd = each.end {
  516. i += 1
  517. timeIntervalLoops = (previousTimeLoop - (each.start ?? previousTimeLoop)).timeInterval / 60
  518. if timeIntervalLoops > 0.0, i != 1 {
  519. timeIntervalLoopArray.append(timeIntervalLoops)
  520. }
  521. if timeIntervalLoops > maximumInt {
  522. maximumInt = timeIntervalLoops
  523. }
  524. if timeIntervalLoops < minimumInt, i != 1 {
  525. minimumInt = timeIntervalLoops
  526. }
  527. previousTimeLoop = loopEnd
  528. }
  529. }
  530. // Average Loop Interval in minutes
  531. let timeOfFirstIndex = loops.first?.start ?? Date()
  532. let lastIndexWithTimestamp = loops.count - 1
  533. let timeOfLastIndex = loops[lastIndexWithTimestamp].end ?? Date()
  534. let averageInterval = (timeOfFirstIndex - timeOfLastIndex).timeInterval / 60 / Double(errorNR + successsNR)
  535. if minimumInt == 999.0 {
  536. minimumInt = 0.0
  537. }
  538. var array: [(double: Double, string: String)] = []
  539. array.append((double: Double(successsNR + errorNR), string: "Loops"))
  540. array.append((double: averageInterval, string: "Interval"))
  541. array.append((double: medianDuration, string: "Duration"))
  542. array.append((double: successRate ?? 100, string: "%"))
  543. return array
  544. }
  545. private func medianCalculation(array: [Int]) -> Double {
  546. guard !array.isEmpty else {
  547. return 0
  548. }
  549. let sorted = array.sorted()
  550. let length = array.count
  551. if length % 2 == 0 {
  552. return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
  553. }
  554. return Double(sorted[length / 2])
  555. }
  556. private func medianCalculationDouble(array: [Double]) -> Double {
  557. guard !array.isEmpty else {
  558. return 0
  559. }
  560. let sorted = array.sorted()
  561. let length = array.count
  562. if length % 2 == 0 {
  563. return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
  564. }
  565. return sorted[length / 2]
  566. }
  567. private func glucoseStats(_ glucose_90: FetchedResults<Readings>)
  568. -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  569. {
  570. var numberOfDays: Double = 0
  571. let endIndex = glucose_90.count - 1
  572. if endIndex > 0 {
  573. let firstElementTime = glucose_90[0].date ?? Date()
  574. let lastElementTime = glucose_90[endIndex].date ?? Date()
  575. numberOfDays = (firstElementTime - lastElementTime).timeInterval / 8.64E4
  576. }
  577. var duration = 1
  578. var denominator: Double = 1
  579. switch selectedDuration {
  580. case .Today:
  581. let minutesSinceMidnight = Calendar.current.component(.hour, from: Date()) * 60 + Calendar.current
  582. .component(.minute, from: Date())
  583. duration = minutesSinceMidnight
  584. denominator = 1
  585. case .Day:
  586. duration = 1 * 1440
  587. denominator = 1
  588. case .Week:
  589. duration = 7 * 1440
  590. if numberOfDays > 7 { denominator = 7 } else { denominator = numberOfDays }
  591. case .Month:
  592. duration = 30 * 1440
  593. if numberOfDays > 30 { denominator = 30 } else { denominator = numberOfDays }
  594. case .Total:
  595. duration = 90 * 1440
  596. if numberOfDays >= 90 { denominator = 90 } else { denominator = numberOfDays }
  597. }
  598. let timeAgo = Date().addingTimeInterval(-duration.minutes.timeInterval)
  599. let glucose = glucose_90.filter({ ($0.date ?? Date()) >= timeAgo })
  600. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  601. let sumReadings = justGlucoseArray.reduce(0, +)
  602. let countReadings = justGlucoseArray.count
  603. let glucoseAverage = Double(sumReadings) / Double(countReadings)
  604. let medianGlucose = medianCalculation(array: justGlucoseArray)
  605. var NGSPa1CStatisticValue = 0.0
  606. var IFCCa1CStatisticValue = 0.0
  607. if numberOfDays > 0 {
  608. NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
  609. IFCCa1CStatisticValue = 10.929 *
  610. (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol) A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
  611. }
  612. var sumOfSquares = 0.0
  613. for array in justGlucoseArray {
  614. sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
  615. }
  616. var sd = 0.0
  617. var cv = 0.0
  618. // Avoid division by zero
  619. if glucoseAverage > 0 {
  620. sd = sqrt(sumOfSquares / Double(countReadings))
  621. cv = sd / Double(glucoseAverage) * 100
  622. }
  623. var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
  624. output = (
  625. ifcc: IFCCa1CStatisticValue,
  626. ngsp: NGSPa1CStatisticValue,
  627. average: glucoseAverage * (state.units == .mmolL ? conversionFactor : 1),
  628. median: medianGlucose * (state.units == .mmolL ? conversionFactor : 1),
  629. sd: sd * (state.units == .mmolL ? conversionFactor : 1), cv: cv,
  630. readings: Double(countReadings) / denominator
  631. )
  632. return output
  633. }
  634. private func tir(_ glucose_90: FetchedResults<Readings>) -> [(decimal: Decimal, string: String)] {
  635. var duration = 1
  636. switch selectedDuration {
  637. case .Today:
  638. let minutesSinceMidnight = Calendar.current.component(.hour, from: Date()) * 60 + Calendar.current
  639. .component(.minute, from: Date())
  640. duration = minutesSinceMidnight
  641. case .Day:
  642. duration = 1 * 1440
  643. case .Week:
  644. duration = 7 * 1440
  645. case .Month:
  646. duration = 30 * 1440
  647. case .Total:
  648. duration = 90 * 1440
  649. }
  650. let hypoLimit = Int(state.lowLimit ?? 70)
  651. let hyperLimit = Int(state.highLimit ?? 145)
  652. let timeAgo = Date().addingTimeInterval(-duration.minutes.timeInterval)
  653. let glucose = glucose_90.filter({ ($0.date ?? Date()) >= timeAgo })
  654. let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
  655. let totalReadings = justGlucoseArray.count
  656. let hyperArray = glucose.filter({ $0.glucose >= hyperLimit })
  657. let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
  658. let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
  659. let hypoArray = glucose.filter({ $0.glucose <= hypoLimit })
  660. let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
  661. let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
  662. let tir = 100 - (hypoPercentage + hyperPercentage)
  663. var array: [(decimal: Decimal, string: String)] = []
  664. array.append((decimal: Decimal(hypoPercentage), string: "Low"))
  665. array.append((decimal: Decimal(tir), string: "NormaL"))
  666. array.append((decimal: Decimal(hyperPercentage), string: "High"))
  667. return array
  668. }
  669. private func colorOfGlucose(_ index: Int) -> Color {
  670. let whichIndex = index
  671. switch whichIndex {
  672. case 0:
  673. return .red
  674. case 1:
  675. return .green
  676. case 2:
  677. return .orange
  678. default:
  679. return .primary
  680. }
  681. }
  682. }
  683. }