StatRootView.swift 34 KB

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