From 6a74435e74e67f8cd47b4cd142ce087328f027ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 9 Oct 2024 15:01:54 +0200 Subject: [PATCH 1/5] More loop statistics. Add description pop-up of succes rate in statistics View. Display nr of errors and most frequent cause of loop error in pop-up. Save errors with CoreData. Add localization (new strings to translate). --- .../Core_Data.xcdatamodel/contents | 5 +- FreeAPS/Sources/APS/APSManager.swift | 8 ++- .../Main/en.lproj/Localizable.strings | 9 +++ .../Main/sv.lproj/Localizable.strings | 9 +++ .../Modules/Stat/View/StatRootView.swift | 2 +- .../Sources/Modules/Stat/View/StatsView.swift | 66 ++++++++++++++++++- 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents index 8a0bf22421..edcb016e13 100644 --- a/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents +++ b/Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -57,6 +57,7 @@ + @@ -199,4 +200,4 @@ - + \ No newline at end of file diff --git a/FreeAPS/Sources/APS/APSManager.swift b/FreeAPS/Sources/APS/APSManager.swift index 9427ae2e03..0bf653a45b 100644 --- a/FreeAPS/Sources/APS/APSManager.swift +++ b/FreeAPS/Sources/APS/APSManager.swift @@ -292,7 +292,7 @@ final class BaseAPSManager: APSManager, Injectable { lastError.send(nil) } - loopStats(loopStatRecord: loopStatRecord) + loopStats(loopStatRecord: loopStatRecord, error: error) if settings.closedLoop { reportEnacted(received: error == nil) @@ -1304,7 +1304,7 @@ final class BaseAPSManager: APSManager, Injectable { return branch } - private func loopStats(loopStatRecord: LoopStats) { + private func loopStats(loopStatRecord: LoopStats, error: Error?) { coredataContext.perform { let nLS = LoopStatRecord(context: self.coredataContext) @@ -1314,6 +1314,10 @@ final class BaseAPSManager: APSManager, Injectable { nLS.duration = loopStatRecord.duration ?? 0.0 nLS.interval = loopStatRecord.interval ?? 0.0 + if let error = error { + nLS.error = error.localizedDescription.string + } + try? self.coredataContext.save() } } diff --git a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings index 053eef85b4..24c874025f 100644 --- a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings @@ -1880,6 +1880,15 @@ Enact a temp Basal or a temp target */ /* Loop Errors in statPanel */ "Errors" = "Errors"; +/* Loop Statistics pop-up description */ +"Success = Started / Completed (Loops)" = "Success = Started / Completed (Loops)"; + +/* Loop Statistics pop-up */ +"Most Frequent Error:" = "Most Frequent Error:"; + +/* Loop Statistics pop-up */ +"Non-completed Loops" = "Non-completed Loops"; + /* Average loop interval */ "Interval" = "Interval"; diff --git a/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings index e56c800341..300d1bffd1 100644 --- a/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings @@ -1877,6 +1877,15 @@ Enact a temp Basal or a temp target */ /* Loop Errors in statPanel */ "Errors" = "Fel"; +/* Loop Statistics pop-up description */ +"Success = Started / Completed (Loops)" = "Lyckades = Påbörjade / Avslutade (loopar)"; + +/* Loop Statistics pop-up */ +"Most Frequent Error:" = "Vanligaste fel:"; + +/* Loop Statistics pop-up */ +"Non-completed Loops" = "ej avslutade loopar"; + /* Average loop interval */ "Interval" = "Intervall"; diff --git a/FreeAPS/Sources/Modules/Stat/View/StatRootView.swift b/FreeAPS/Sources/Modules/Stat/View/StatRootView.swift index 3b1a9ae3a3..45d6158839 100644 --- a/FreeAPS/Sources/Modules/Stat/View/StatRootView.swift +++ b/FreeAPS/Sources/Modules/Stat/View/StatRootView.swift @@ -146,7 +146,7 @@ extension Stat { .pickerStyle(.segmented).background(.cyan.opacity(0.2)) stats() } - .dynamicTypeSize(...DynamicTypeSize.xxLarge) + .dynamicTypeSize(...DynamicTypeSize.xLarge) .onAppear(perform: configureView) .navigationBarTitle("Statistics") .navigationBarTitleDisplayMode(.inline) diff --git a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift index 078e919816..1468793578 100644 --- a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift +++ b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift @@ -7,6 +7,7 @@ struct StatsView: View { @FetchRequest var fetchRequestReadings: FetchedResults @State var headline: Color = .secondary + @State var errorReasons: Bool = false @Binding var highLimit: Decimal @Binding var lowLimit: Decimal @@ -83,11 +84,26 @@ struct StatsView: View { ) } VStack(spacing: 5) { - Text("Success").font(.subheadline).foregroundColor(headline) + let succesPercentage = successRate ?? 100 + HStack { + Text("Success").foregroundColor(headline) + if succesPercentage != 100 { + Image(systemName: "info.circle").foregroundStyle(.blue) + } + }.font(.subheadline) Text( - ((successRate ?? 100) / 100) + (succesPercentage / 100) .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1))) ) + }.onTapGesture { + errorReasons.toggle() + } + } + .overlay { + VStack { + if errorReasons { + errors(loopCount - successsNR) + } } } } @@ -119,6 +135,43 @@ struct StatsView: View { return sorted[length / 2] } + private func errors(_ nonCompleted: Int) -> some View { + ZStack { + if nonCompleted > 0 { + let errors = fetchRequest.compactMap(\.error) + if errors.isNotEmpty { + let mostFrequent = errors.mostFrequent()?.description ?? "" + RoundedRectangle(cornerRadius: 6) + .fill(Color(.systemGray2)) + .frame(width: 380, height: 250) + .shadow(radius: 5) + .overlay { + VStack(alignment: .leading) { + Text("Success = Started / Completed (Loops)") + .padding(.horizontal, 5) + .padding(.top, 5) + .padding(.bottom, 3) + HStack { + Text("\(nonCompleted)").bold() + Text("Non-completed Loops") + } + .padding(.horizontal, 8).padding(.bottom, 10) + Text("Most Frequent Error:") + .padding(.horizontal, 5) + .padding(.vertical, 3) + .bold() + Text(mostFrequent) + .padding(.horizontal, 5) + .padding(.bottom, 5) + } + } + } + } + } + .offset(y: -160) + .onTapGesture { errorReasons.toggle() } + } + var hba1c: some View { HStack(spacing: 50) { let useUnit: GlucoseUnits = (units == .mmolL && overrideUnit) ? .mgdL : @@ -276,3 +329,12 @@ struct StatsView: View { return array } } + +extension Collection { + /** + Returns the most frequent element in the collection. + */ + func mostFrequent() -> Element? where Element: Hashable { + reduce(into: [:]) { $0[$1, default: 0] += 1 }.max(by: { $0.1 < $1.1 })?.key + } +} From b6b3b36b7abe4e0cdcf1382002e441105bad0a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Wed, 9 Oct 2024 15:15:22 +0200 Subject: [PATCH 2/5] typo --- FreeAPS/Sources/Modules/Stat/View/StatsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift index 1468793578..36efb4bdcd 100644 --- a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift +++ b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift @@ -155,7 +155,7 @@ struct StatsView: View { Text("\(nonCompleted)").bold() Text("Non-completed Loops") } - .padding(.horizontal, 8).padding(.bottom, 10) + .padding(.horizontal, 5).padding(.bottom, 10) Text("Most Frequent Error:") .padding(.horizontal, 5) .padding(.vertical, 3) From d7583b6e689dd3ea566e1ac55acda959c19d5796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Thu, 10 Oct 2024 02:28:42 +0200 Subject: [PATCH 3/5] Animate LoopsView and PreviewChart while opening the full statistics View. --- .../Modules/Home/View/HomeRootView.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift index 5f20e7b475..1b68766372 100644 --- a/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift +++ b/FreeAPS/Sources/Modules/Home/View/HomeRootView.swift @@ -16,6 +16,9 @@ extension Home { @State var triggerUpdate = false @State var display = false @State var displayGlucose = false + @State var animateLoop = Date.distantPast + @State var animateTIR = Date.distantPast + let buttonFont = Font.custom("TimeButtonFont", size: 14) let viewPadding: CGFloat = 5 @@ -478,9 +481,16 @@ extension Home { .clipShape(RoundedRectangle(cornerRadius: 15)) .addShadows() .padding(.horizontal, 10) + .blur(radius: animateTIRView ? 2 : 0) .onTapGesture { + timeIsNowTIR() state.showModal(for: .statistics) } + .overlay { + if animateTIRView { + animation.asAny() + } + } } var infoPanelView: some View { @@ -536,9 +546,16 @@ extension Home { .clipShape(RoundedRectangle(cornerRadius: 15)) .addShadows() .padding(.horizontal, 10) + .blur(radius: animateLoopView ? 2.5 : 0) .onTapGesture { + timeIsNowLoop() state.showModal(for: .statistics) } + .overlay { + if animateLoopView { + animation.asAny() + } + } } var profileView: some View { @@ -702,6 +719,26 @@ extension Home { .background(TimeEllipse(characters: string.count)) } + private var animateLoopView: Bool { + -1 * animateLoop.timeIntervalSinceNow < 1.5 + } + + private var animateTIRView: Bool { + -1 * animateTIR.timeIntervalSinceNow < 1.5 + } + + private func timeIsNowLoop() { + animateLoop = Date.now + } + + private func timeIsNowTIR() { + animateTIR = Date.now + } + + private var animation: any View { + ActivityIndicator(isAnimating: .constant(true), style: .large) + } + var body: some View { GeometryReader { geo in VStack(spacing: 0) { From bcd4c5625425142f85a2fad219da48e77af58076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Thu, 10 Oct 2024 15:15:30 +0200 Subject: [PATCH 4/5] Display count of the most frequent loop error. --- FreeAPS/Sources/Modules/Stat/View/StatsView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift index 36efb4bdcd..410591d46e 100644 --- a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift +++ b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift @@ -141,6 +141,7 @@ struct StatsView: View { let errors = fetchRequest.compactMap(\.error) if errors.isNotEmpty { let mostFrequent = errors.mostFrequent()?.description ?? "" + let mostFrequentCount = errors.filter({ $0 == mostFrequent }).count RoundedRectangle(cornerRadius: 6) .fill(Color(.systemGray2)) .frame(width: 380, height: 250) @@ -151,12 +152,13 @@ struct StatsView: View { .padding(.horizontal, 5) .padding(.top, 5) .padding(.bottom, 3) + .foregroundStyle(.secondary) HStack { Text("\(nonCompleted)").bold() Text("Non-completed Loops") } .padding(.horizontal, 5).padding(.bottom, 10) - Text("Most Frequent Error:") + Text("Most Frequent Error (\(mostFrequentCount)):") .padding(.horizontal, 5) .padding(.vertical, 3) .bold() From 465f34855a3137a5ded042ce47ecd1d440d4a03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20M=C3=A5rtensson?= Date: Thu, 10 Oct 2024 16:10:21 +0200 Subject: [PATCH 5/5] Alignments --- .../Main/en.lproj/Localizable.strings | 4 +-- .../Main/sv.lproj/Localizable.strings | 4 +-- .../Sources/Modules/Stat/View/StatsView.swift | 34 +++++++++++-------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings index 24c874025f..795be562a7 100644 --- a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings @@ -1881,10 +1881,10 @@ Enact a temp Basal or a temp target */ "Errors" = "Errors"; /* Loop Statistics pop-up description */ -"Success = Started / Completed (Loops)" = "Success = Started / Completed (Loops)"; +"Success = Started / Completed (loops)" = "Success = Started / Completed (loops)"; /* Loop Statistics pop-up */ -"Most Frequent Error:" = "Most Frequent Error:"; +"Most Frequent Error" = "Most Frequent Error"; /* Loop Statistics pop-up */ "Non-completed Loops" = "Non-completed Loops"; diff --git a/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings index 300d1bffd1..c081f660a2 100644 --- a/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings @@ -1878,10 +1878,10 @@ Enact a temp Basal or a temp target */ "Errors" = "Fel"; /* Loop Statistics pop-up description */ -"Success = Started / Completed (Loops)" = "Lyckades = Påbörjade / Avslutade (loopar)"; +"Success = Started / Completed (loops)" = "Lyckades = Påbörjade / Avslutade (loopar)"; /* Loop Statistics pop-up */ -"Most Frequent Error:" = "Vanligaste fel:"; +"Most Frequent Error" = "Vanligaste fel"; /* Loop Statistics pop-up */ "Non-completed Loops" = "ej avslutade loopar"; diff --git a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift index 410591d46e..3c1a595c14 100644 --- a/FreeAPS/Sources/Modules/Stat/View/StatsView.swift +++ b/FreeAPS/Sources/Modules/Stat/View/StatsView.swift @@ -144,27 +144,31 @@ struct StatsView: View { let mostFrequentCount = errors.filter({ $0 == mostFrequent }).count RoundedRectangle(cornerRadius: 6) .fill(Color(.systemGray2)) - .frame(width: 380, height: 250) + .frame(width: 380, height: 200) .shadow(radius: 5) .overlay { - VStack(alignment: .leading) { - Text("Success = Started / Completed (Loops)") + ZStack { + Text("Success = Started / Completed (loops)") .padding(.horizontal, 5) - .padding(.top, 5) - .padding(.bottom, 3) + .padding(.top, 20) .foregroundStyle(.secondary) - HStack { - Text("\(nonCompleted)").bold() - Text("Non-completed Loops") - } - .padding(.horizontal, 5).padding(.bottom, 10) - Text("Most Frequent Error (\(mostFrequentCount)):") - .padding(.horizontal, 5) + .frame(maxHeight: .infinity, alignment: .top) + VStack { + Text( + NSLocalizedString("Most Frequent Error", comment: "Loop Statistics pop-up") + + " (\(mostFrequentCount) " + + NSLocalizedString("of", comment: "") + + " \(nonCompleted)):" + ) + .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 3) .bold() - Text(mostFrequent) - .padding(.horizontal, 5) - .padding(.bottom, 5) + Text(mostFrequent) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: .infinity, alignment: .bottom) + .padding(.horizontal, 20) + .padding(.bottom, 20) } } }