| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- import Charts
- import Foundation
- import SwiftUI
- 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
- }
- }
- extension MainChartView {
- var basalChart: some View {
- VStack {
- Chart {
- drawStartRuleMark()
- drawEndRuleMark()
- drawCurrentTimeMarker()
- drawTempBasals(dummy: false)
- drawBasalProfile()
- drawSuspensions()
- }.onChange(of: state.tempBasals) {
- calculateBasals()
- calculateTempBasalsInBackground()
- }
- .onChange(of: state.maxBasal) {
- 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: - Draw functions
- extension MainChartView {
- func drawTempBasals(dummy: Bool) -> some ChartContent {
- ForEach(preparedTempBasals, 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)
- } 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(
- colors: [
- Color.insulin.opacity(0.6),
- Color.insulin.opacity(0.1)
- ],
- startPoint: .bottom,
- endPoint: .top
- )
- ).alignsMarkStylesWithPlotArea()
- 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)
- }
- }
- }
- 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)
- }
- }
- 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))
- }
- }
- }
- }
- // MARK: - Calculation
- extension MainChartView {
- func calculateTempBasalsInBackground() {
- Task {
- let basals = await prepareTempBasals()
- await MainActor.run {
- preparedTempBasals = basals
- }
- }
- }
- func prepareTempBasals() async -> [(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 }
- let rate = Double(truncating: temp.tempBasal?.rate ?? Decimal.zero as NSDecimalNumber) * (isInsulinSuspended ? 0 : 1)
- // 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)
- }
- return (timestamp, nextTemp.timestamp ?? Date(), rate)
- }
- }
- func findRegularBasalPoints(
- timeBegin: TimeInterval,
- timeEnd: TimeInterval
- ) async -> [BasalProfile] {
- guard timeBegin < timeEnd else { return [] }
- let beginDate = Date(timeIntervalSince1970: timeBegin)
- let startOfDay = Calendar.current.startOfDay(for: beginDate)
- let profile = state.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
- }
- func calculateBasals() {
- Task {
- let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
- // Get Regular Basal
- let basalPoints = await findRegularBasalPoints(
- timeBegin: dayAgoTime,
- timeEnd: endMarker.timeIntervalSince1970
- )
- var totalBasal = basalPoints
- 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
- }
- }
- }
- }
|