|
|
@@ -5,35 +5,11 @@ import SwiftUI
|
|
|
let screenSize: CGRect = UIScreen.main.bounds
|
|
|
let calendar = Calendar.current
|
|
|
|
|
|
-private struct BasalProfile: Hashable {
|
|
|
- let amount: Double
|
|
|
- var isOverwritten: Bool
|
|
|
- let startDate: Date
|
|
|
- let endDate: Date?
|
|
|
- init(amount: Double, isOverwritten: Bool, startDate: Date, endDate: Date? = nil) {
|
|
|
- self.amount = amount
|
|
|
- self.isOverwritten = isOverwritten
|
|
|
- self.startDate = startDate
|
|
|
- self.endDate = endDate
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-private struct ChartTempTarget: Hashable {
|
|
|
- let amount: Decimal
|
|
|
- let start: Date
|
|
|
- let end: Date
|
|
|
-}
|
|
|
-
|
|
|
struct MainChartView: View {
|
|
|
var geo: GeometryProxy
|
|
|
@Binding var units: GlucoseUnits
|
|
|
- @Binding var announcement: [Announcement]
|
|
|
@Binding var hours: Int
|
|
|
- @Binding var maxBasal: Decimal
|
|
|
- @Binding var autotunedBasalProfile: [BasalProfileEntry]
|
|
|
- @Binding var basalProfile: [BasalProfileEntry]
|
|
|
@Binding var tempTargets: [TempTarget]
|
|
|
- @Binding var smooth: Bool
|
|
|
@Binding var highGlucose: Decimal
|
|
|
@Binding var lowGlucose: Decimal
|
|
|
@Binding var screenHours: Int16
|
|
|
@@ -43,10 +19,8 @@ struct MainChartView: View {
|
|
|
|
|
|
@StateObject var state: Home.StateModel
|
|
|
|
|
|
- @State var didAppearTrigger = false
|
|
|
- @State private var basalProfiles: [BasalProfile] = []
|
|
|
- @State private var chartTempTargets: [ChartTempTarget] = []
|
|
|
- @State private var count: Decimal = 1
|
|
|
+ @State var basalProfiles: [BasalProfile] = []
|
|
|
+ @State var chartTempTargets: [ChartTempTarget] = []
|
|
|
@State var startMarker =
|
|
|
Date(timeIntervalSinceNow: TimeInterval(hours: -24))
|
|
|
@State var endMarker = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
|
|
|
@@ -66,7 +40,7 @@ struct MainChartView: View {
|
|
|
@Environment(\.colorScheme) var colorScheme
|
|
|
@Environment(\.calendar) var calendar
|
|
|
|
|
|
- private var upperLimit: Decimal {
|
|
|
+ var upperLimit: Decimal {
|
|
|
units == .mgdL ? 400 : 22.2
|
|
|
}
|
|
|
|
|
|
@@ -160,7 +134,7 @@ struct MainChartView: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// MARK: - Components
|
|
|
+// MARK: - Main Chart with selection Popover
|
|
|
|
|
|
extension MainChartView {
|
|
|
private var mainChart: some View {
|
|
|
@@ -260,11 +234,6 @@ extension MainChartView {
|
|
|
await calculateTTs()
|
|
|
}
|
|
|
}
|
|
|
- .onChange(of: didAppearTrigger) { _ in
|
|
|
- Task {
|
|
|
- await calculateTTs()
|
|
|
- }
|
|
|
- }
|
|
|
.frame(minHeight: geo.size.height * 0.28)
|
|
|
.frame(width: fullWidth(viewWidth: screenSize.width))
|
|
|
.chartXScale(domain: startMarker ... endMarker)
|
|
|
@@ -326,42 +295,9 @@ extension MainChartView {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- private var basalChart: some View {
|
|
|
- VStack {
|
|
|
- Chart {
|
|
|
- drawStartRuleMark()
|
|
|
- drawEndRuleMark()
|
|
|
- drawCurrentTimeMarker()
|
|
|
- drawTempBasals(dummy: false)
|
|
|
- drawBasalProfile()
|
|
|
- drawSuspensions()
|
|
|
- }.onChange(of: state.tempBasals) { _ in
|
|
|
- calculateBasals()
|
|
|
- }
|
|
|
- .onChange(of: maxBasal) { _ in
|
|
|
- calculateBasals()
|
|
|
- }
|
|
|
- .onChange(of: autotunedBasalProfile) { _ in
|
|
|
- calculateBasals()
|
|
|
- }
|
|
|
- .onChange(of: didAppearTrigger) { _ in
|
|
|
- calculateBasals()
|
|
|
- }.onChange(of: basalProfile) { _ in
|
|
|
- calculateBasals()
|
|
|
- }
|
|
|
- .frame(minHeight: geo.size.height * 0.05)
|
|
|
- .frame(width: fullWidth(viewWidth: screenSize.width))
|
|
|
- .chartXScale(domain: startMarker ... endMarker)
|
|
|
- .chartXAxis { basalChartXAxis }
|
|
|
- .chartXAxis(.hidden)
|
|
|
- .chartYAxis(.hidden)
|
|
|
- .chartPlotStyle { basalChartPlotStyle($0) }
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
-// MARK: - Calculations
|
|
|
+// MARK: - Rule Marks and Charts configurations
|
|
|
|
|
|
extension MainChartView {
|
|
|
func drawCurrentTimeMarker() -> some ChartContent {
|
|
|
@@ -374,7 +310,7 @@ extension MainChartView {
|
|
|
).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
|
|
|
}
|
|
|
|
|
|
- private func drawStartRuleMark() -> some ChartContent {
|
|
|
+ func drawStartRuleMark() -> some ChartContent {
|
|
|
RuleMark(
|
|
|
x: .value(
|
|
|
"",
|
|
|
@@ -384,7 +320,7 @@ extension MainChartView {
|
|
|
).foregroundStyle(Color.clear)
|
|
|
}
|
|
|
|
|
|
- private func drawEndRuleMark() -> some ChartContent {
|
|
|
+ func drawEndRuleMark() -> some ChartContent {
|
|
|
RuleMark(
|
|
|
x: .value(
|
|
|
"",
|
|
|
@@ -394,280 +330,73 @@ extension MainChartView {
|
|
|
).foregroundStyle(Color.clear)
|
|
|
}
|
|
|
|
|
|
- private func drawTempTargets() -> some ChartContent {
|
|
|
- /// temp targets
|
|
|
- ForEach(chartTempTargets, id: \.self) { target in
|
|
|
- let targetLimited = min(max(target.amount, 0), upperLimit)
|
|
|
-
|
|
|
- RuleMark(
|
|
|
- xStart: .value("Start", target.start),
|
|
|
- xEnd: .value("End", target.end),
|
|
|
- y: .value("Value", targetLimited)
|
|
|
- )
|
|
|
- .foregroundStyle(Color.purple.opacity(0.75)).lineStyle(.init(lineWidth: 8))
|
|
|
- }
|
|
|
+ func basalChartPlotStyle(_ plotContent: ChartPlotContent) -> some View {
|
|
|
+ plotContent
|
|
|
+ .rotationEffect(.degrees(180))
|
|
|
+ .scaleEffect(x: -1, y: 1)
|
|
|
}
|
|
|
|
|
|
- private func drawSuspensions() -> some ChartContent {
|
|
|
- let suspensions = state.suspensions
|
|
|
- return ForEach(suspensions) { suspension in
|
|
|
- let now = Date()
|
|
|
-
|
|
|
- if let type = suspension.type, type == EventType.pumpSuspend.rawValue, let suspensionStart = suspension.timestamp {
|
|
|
- let suspensionEnd = min(
|
|
|
- (
|
|
|
- suspensions
|
|
|
- .first(where: {
|
|
|
- $0.timestamp ?? now > suspensionStart && $0.type == EventType.pumpResume.rawValue })?
|
|
|
- .timestamp
|
|
|
- ) ?? now,
|
|
|
- now
|
|
|
- )
|
|
|
-
|
|
|
- let basalProfileDuringSuspension = basalProfiles.first(where: { $0.startDate <= suspensionStart })
|
|
|
- let suspensionMarkHeight = basalProfileDuringSuspension?.amount ?? 1
|
|
|
-
|
|
|
- RectangleMark(
|
|
|
- xStart: .value("start", suspensionStart),
|
|
|
- xEnd: .value("end", suspensionEnd),
|
|
|
- yStart: .value("suspend-start", 0),
|
|
|
- yEnd: .value("suspend-end", suspensionMarkHeight)
|
|
|
- )
|
|
|
- .foregroundStyle(Color.loopGray.opacity(colorScheme == .dark ? 0.3 : 0.8))
|
|
|
+ var mainChartXAxis: some AxisContent {
|
|
|
+ AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
|
|
|
+ if displayXgridLines {
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
+ } else {
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- func drawIOB() -> some ChartContent {
|
|
|
- ForEach(state.enactedAndNonEnactedDeterminations) { iob in
|
|
|
- let rawAmount = iob.iob?.doubleValue ?? 0
|
|
|
- let amount: Double = rawAmount > 0 ? rawAmount : rawAmount * 2 // weigh negative iob with factor 2
|
|
|
- let date: Date = iob.deliverAt ?? Date()
|
|
|
-
|
|
|
- LineMark(x: .value("Time", date), y: .value("Amount", amount))
|
|
|
- .foregroundStyle(Color.darkerBlue)
|
|
|
- AreaMark(x: .value("Time", date), y: .value("Amount", amount))
|
|
|
- .foregroundStyle(
|
|
|
- LinearGradient(
|
|
|
- gradient: Gradient(
|
|
|
- colors: [
|
|
|
- Color.darkerBlue.opacity(0.8),
|
|
|
- Color.darkerBlue.opacity(0.01)
|
|
|
- ]
|
|
|
- ),
|
|
|
- startPoint: .top,
|
|
|
- endPoint: .bottom
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- func drawCOB(dummy: Bool) -> some ChartContent {
|
|
|
- ForEach(state.enactedAndNonEnactedDeterminations) { cob in
|
|
|
- let amount = Int(cob.cob)
|
|
|
- let date: Date = cob.deliverAt ?? Date()
|
|
|
-
|
|
|
- if dummy {
|
|
|
- LineMark(x: .value("Time", date), y: .value("Value", amount))
|
|
|
- .foregroundStyle(Color.clear)
|
|
|
- AreaMark(x: .value("Time", date), y: .value("Value", amount)).foregroundStyle(
|
|
|
- Color.clear
|
|
|
- )
|
|
|
+ var basalChartXAxis: some AxisContent {
|
|
|
+ AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
|
|
|
+ if displayXgridLines {
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
} else {
|
|
|
- LineMark(x: .value("Time", date), y: .value("Value", amount))
|
|
|
- .foregroundStyle(Color.orange.gradient)
|
|
|
- AreaMark(x: .value("Time", date), y: .value("Value", amount)).foregroundStyle(
|
|
|
- LinearGradient(
|
|
|
- gradient: Gradient(
|
|
|
- colors: [
|
|
|
- Color.orange.opacity(0.8),
|
|
|
- Color.orange.opacity(0.01)
|
|
|
- ]
|
|
|
- ),
|
|
|
- startPoint: .top,
|
|
|
- endPoint: .bottom
|
|
|
- )
|
|
|
- )
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
}
|
|
|
+ AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
|
|
|
+ .font(.footnote).foregroundStyle(Color.primary)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func prepareTempBasals() -> [(start: Date, end: Date, rate: Double)] {
|
|
|
- let now = Date()
|
|
|
- let tempBasals = state.tempBasals
|
|
|
-
|
|
|
- return tempBasals.compactMap { temp -> (start: Date, end: Date, rate: Double)? in
|
|
|
- let duration = temp.tempBasal?.duration ?? 0
|
|
|
- let timestamp = temp.timestamp ?? Date()
|
|
|
- let end = min(timestamp + duration.minutes, now)
|
|
|
- let isInsulinSuspended = state.suspensions.contains { $0.timestamp ?? now >= timestamp && $0.timestamp ?? now <= end }
|
|
|
+ var mainChartYAxis: some AxisContent {
|
|
|
+ AxisMarks(position: .trailing) { value in
|
|
|
|
|
|
- let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
|
|
|
+ if displayYgridLines {
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
+ } else {
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
+ }
|
|
|
|
|
|
- // Check if there's a subsequent temp basal to determine the end time
|
|
|
- guard let nextTemp = state.tempBasals.first(where: { $0.timestamp ?? .distantPast > timestamp }) else {
|
|
|
- return (timestamp, end, rate)
|
|
|
+ if let glucoseValue = value.as(Double.self), glucoseValue > 0 {
|
|
|
+ /// fix offset between the two charts...
|
|
|
+ if units == .mmolL {
|
|
|
+ AxisTick(length: 7, stroke: .init(lineWidth: 7)).foregroundStyle(Color.clear)
|
|
|
+ }
|
|
|
+ AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
|
|
|
}
|
|
|
- return (timestamp, nextTemp.timestamp ?? Date(), rate) // end defaults to current time
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func drawTempBasals(dummy: Bool) -> some ChartContent {
|
|
|
- ForEach(prepareTempBasals(), id: \.rate) { basal in
|
|
|
- if dummy {
|
|
|
- RectangleMark(
|
|
|
- xStart: .value("start", basal.start),
|
|
|
- xEnd: .value("end", basal.end),
|
|
|
- yStart: .value("rate-start", 0),
|
|
|
- yEnd: .value("rate-end", basal.rate)
|
|
|
- ).foregroundStyle(Color.clear)
|
|
|
-
|
|
|
- LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
|
|
|
- .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.clear)
|
|
|
-
|
|
|
- LineMark(x: .value("End Date", basal.end), y: .value("Amount", basal.rate))
|
|
|
- .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.clear)
|
|
|
+ var cobChartYAxis: some AxisContent {
|
|
|
+ AxisMarks(position: .trailing) { _ in
|
|
|
+ if displayYgridLines {
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
} else {
|
|
|
- RectangleMark(
|
|
|
- xStart: .value("start", basal.start),
|
|
|
- xEnd: .value("end", basal.end),
|
|
|
- yStart: .value("rate-start", 0),
|
|
|
- yEnd: .value("rate-end", basal.rate)
|
|
|
- ).foregroundStyle(
|
|
|
- LinearGradient(
|
|
|
- gradient: Gradient(
|
|
|
- colors: [
|
|
|
- Color.insulin.opacity(0.6),
|
|
|
- Color.insulin.opacity(0.1)
|
|
|
- ]
|
|
|
- ),
|
|
|
- startPoint: .top,
|
|
|
- endPoint: .bottom
|
|
|
- )
|
|
|
- )
|
|
|
-
|
|
|
- LineMark(x: .value("Start Date", basal.start), y: .value("Amount", basal.rate))
|
|
|
- .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
|
|
|
-
|
|
|
- LineMark(x: .value("End Date", basal.end), y: .value("Amount", basal.rate))
|
|
|
- .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.insulin)
|
|
|
+ AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- private func drawBasalProfile() -> some ChartContent {
|
|
|
- /// dashed profile line
|
|
|
- ForEach(basalProfiles, id: \.self) { profile in
|
|
|
- LineMark(
|
|
|
- x: .value("Start Date", profile.startDate),
|
|
|
- y: .value("Amount", profile.amount),
|
|
|
- series: .value("profile", "profile")
|
|
|
- ).lineStyle(.init(lineWidth: 2, dash: [2, 4])).foregroundStyle(Color.insulin)
|
|
|
- LineMark(
|
|
|
- x: .value("End Date", profile.endDate ?? endMarker),
|
|
|
- y: .value("Amount", profile.amount),
|
|
|
- series: .value("profile", "profile")
|
|
|
- ).lineStyle(.init(lineWidth: 2.5, dash: [2, 4])).foregroundStyle(Color.insulin)
|
|
|
- }
|
|
|
- }
|
|
|
+// MARK: - Calculations and formatting
|
|
|
|
|
|
+extension MainChartView {
|
|
|
func fullWidth(viewWidth: CGFloat) -> CGFloat {
|
|
|
viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
|
|
|
}
|
|
|
|
|
|
- /// calculations for temp target bar mark
|
|
|
- private func calculateTTs() async {
|
|
|
- // Perform calculations off the main thread
|
|
|
- let calculatedTTs = await Task.detached { () -> [ChartTempTarget] in
|
|
|
- var groupedPackages: [[TempTarget]] = []
|
|
|
- var currentPackage: [TempTarget] = []
|
|
|
- var calculatedTTs: [ChartTempTarget] = []
|
|
|
-
|
|
|
- for target in await tempTargets {
|
|
|
- if target.duration > 0 {
|
|
|
- if !currentPackage.isEmpty {
|
|
|
- groupedPackages.append(currentPackage)
|
|
|
- currentPackage = []
|
|
|
- }
|
|
|
- currentPackage.append(target)
|
|
|
- } else if let lastNonZeroTempTarget = currentPackage.last(where: { $0.duration > 0 }) {
|
|
|
- // Ensure this cancel target is within the valid time range
|
|
|
- if target.createdAt >= lastNonZeroTempTarget.createdAt,
|
|
|
- target.createdAt <= lastNonZeroTempTarget.createdAt
|
|
|
- .addingTimeInterval(TimeInterval(lastNonZeroTempTarget.duration * 60))
|
|
|
- {
|
|
|
- currentPackage.append(target)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Append the last group, if any
|
|
|
- if !currentPackage.isEmpty {
|
|
|
- groupedPackages.append(currentPackage)
|
|
|
- }
|
|
|
-
|
|
|
- for package in groupedPackages {
|
|
|
- guard let firstNonZeroTarget = package.first(where: { $0.duration > 0 }) else { continue }
|
|
|
-
|
|
|
- var end = firstNonZeroTarget.createdAt.addingTimeInterval(TimeInterval(firstNonZeroTarget.duration * 60))
|
|
|
-
|
|
|
- let earliestCancelTarget = package.filter({ $0.duration == 0 }).min(by: { $0.createdAt < $1.createdAt })
|
|
|
-
|
|
|
- if let earliestCancelTarget = earliestCancelTarget {
|
|
|
- end = min(earliestCancelTarget.createdAt, end)
|
|
|
- }
|
|
|
-
|
|
|
- if let targetTop = firstNonZeroTarget.targetTop {
|
|
|
- let adjustedTarget = await units == .mgdL ? targetTop : targetTop.asMmolL
|
|
|
- calculatedTTs
|
|
|
- .append(ChartTempTarget(amount: adjustedTarget, start: firstNonZeroTarget.createdAt, end: end))
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return calculatedTTs
|
|
|
- }.value
|
|
|
-
|
|
|
- // Update chartTempTargets on the main thread
|
|
|
- await MainActor.run {
|
|
|
- self.chartTempTargets = calculatedTTs
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private func findRegularBasalPoints(
|
|
|
- timeBegin: TimeInterval,
|
|
|
- timeEnd: TimeInterval,
|
|
|
- autotuned: Bool
|
|
|
- ) async -> [BasalProfile] {
|
|
|
- guard timeBegin < timeEnd else { return [] }
|
|
|
-
|
|
|
- let beginDate = Date(timeIntervalSince1970: timeBegin)
|
|
|
- let startOfDay = Calendar.current.startOfDay(for: beginDate)
|
|
|
- let profile = autotuned ? autotunedBasalProfile : basalProfile
|
|
|
- var basalPoints: [BasalProfile] = []
|
|
|
-
|
|
|
- // Iterate over the next three days, multiplying the time intervals
|
|
|
- for dayOffset in 0 ..< 3 {
|
|
|
- let dayTimeOffset = TimeInterval(dayOffset * 24 * 60 * 60) // One Day in seconds
|
|
|
- for entry in profile {
|
|
|
- let basalTime = startOfDay.addingTimeInterval(entry.minutes.minutes.timeInterval + dayTimeOffset)
|
|
|
- let basalTimeInterval = basalTime.timeIntervalSince1970
|
|
|
-
|
|
|
- // Only append points within the timeBegin and timeEnd range
|
|
|
- if basalTimeInterval >= timeBegin, basalTimeInterval < timeEnd {
|
|
|
- basalPoints.append(BasalProfile(
|
|
|
- amount: Double(entry.rate),
|
|
|
- isOverwritten: false,
|
|
|
- startDate: basalTime
|
|
|
- ))
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return basalPoints
|
|
|
- }
|
|
|
-
|
|
|
- /// update start and end marker to fix scroll update problem with x axis
|
|
|
- private func updateStartEndMarkers() {
|
|
|
+ // Update start and end marker to fix scroll update problem with x axis
|
|
|
+ func updateStartEndMarkers() {
|
|
|
startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
|
|
|
|
|
|
let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
|
|
|
@@ -683,48 +412,6 @@ extension MainChartView {
|
|
|
dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
|
|
|
}
|
|
|
|
|
|
- private func calculateBasals() {
|
|
|
- Task {
|
|
|
- let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
|
|
|
-
|
|
|
- // Get Regular and Autotuned Basal parallel
|
|
|
- async let getRegularBasalPoints = findRegularBasalPoints(
|
|
|
- timeBegin: dayAgoTime,
|
|
|
- timeEnd: endMarker.timeIntervalSince1970,
|
|
|
- autotuned: false
|
|
|
- )
|
|
|
-
|
|
|
- async let getAutotunedBasalPoints = findRegularBasalPoints(
|
|
|
- timeBegin: dayAgoTime,
|
|
|
- timeEnd: endMarker.timeIntervalSince1970,
|
|
|
- autotuned: true
|
|
|
- )
|
|
|
-
|
|
|
- let (regularPoints, autotunedBasalPoints) = await (getRegularBasalPoints, getAutotunedBasalPoints)
|
|
|
-
|
|
|
- var totalBasal = regularPoints + autotunedBasalPoints
|
|
|
- totalBasal.sort {
|
|
|
- $0.startDate.timeIntervalSince1970 < $1.startDate.timeIntervalSince1970
|
|
|
- }
|
|
|
-
|
|
|
- var basals: [BasalProfile] = []
|
|
|
- totalBasal.indices.forEach { index in
|
|
|
- basals.append(BasalProfile(
|
|
|
- amount: totalBasal[index].amount,
|
|
|
- isOverwritten: totalBasal[index].isOverwritten,
|
|
|
- startDate: totalBasal[index].startDate,
|
|
|
- endDate: totalBasal.count > index + 1 ? totalBasal[index + 1].startDate : endMarker
|
|
|
- ))
|
|
|
- }
|
|
|
-
|
|
|
- await MainActor.run {
|
|
|
- basalProfiles = basals
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // MARK: - Chart formatting
|
|
|
-
|
|
|
private func yAxisChartData() {
|
|
|
Task {
|
|
|
let (minGlucose, maxGlucose, minForecast, maxForecast) = await Task
|
|
|
@@ -802,77 +489,6 @@ extension MainChartView {
|
|
|
minValueIobChart = minValue
|
|
|
maxValueIobChart = maxValue
|
|
|
}
|
|
|
-
|
|
|
- func basalChartPlotStyle(_ plotContent: ChartPlotContent) -> some View {
|
|
|
- plotContent
|
|
|
- .rotationEffect(.degrees(180))
|
|
|
- .scaleEffect(x: -1, y: 1)
|
|
|
- }
|
|
|
-
|
|
|
- var mainChartXAxis: some AxisContent {
|
|
|
- AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
|
|
|
- if displayXgridLines {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
- } else {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var basalChartXAxis: some AxisContent {
|
|
|
- AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
|
|
|
- if displayXgridLines {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
- } else {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
- }
|
|
|
- AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
|
|
|
- .font(.footnote).foregroundStyle(Color.primary)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var mainChartYAxis: some AxisContent {
|
|
|
- AxisMarks(position: .trailing) { value in
|
|
|
-
|
|
|
- if displayYgridLines {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
- } else {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
- }
|
|
|
-
|
|
|
- if let glucoseValue = value.as(Double.self), glucoseValue > 0 {
|
|
|
- /// fix offset between the two charts...
|
|
|
- if units == .mmolL {
|
|
|
- AxisTick(length: 7, stroke: .init(lineWidth: 7)).foregroundStyle(Color.clear)
|
|
|
- }
|
|
|
- AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var cobChartYAxis: some AxisContent {
|
|
|
- AxisMarks(position: .trailing) { _ in
|
|
|
- if displayYgridLines {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
|
|
|
- } else {
|
|
|
- AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-struct LegendItem: View {
|
|
|
- var color: Color
|
|
|
- var label: String
|
|
|
-
|
|
|
- var body: some View {
|
|
|
- Group {
|
|
|
- Circle().fill(color).frame(width: 8, height: 8)
|
|
|
- Text(label)
|
|
|
- .font(.system(size: 10, weight: .bold))
|
|
|
- .foregroundColor(color)
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
extension Int16 {
|