StatRootView.swift 32 KB

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