|
@@ -56,13 +56,13 @@ struct MainChartView: View {
|
|
|
|
|
|
|
|
var selectedCOBValue: OrefDetermination? {
|
|
var selectedCOBValue: OrefDetermination? {
|
|
|
guard let selection = selection else { return nil }
|
|
guard let selection = selection else { return nil }
|
|
|
- let range = selection.addingTimeInterval(-120) ... selection.addingTimeInterval(120)
|
|
|
|
|
|
|
+ let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
|
|
|
return findDetermination(in: range)
|
|
return findDetermination(in: range)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var selectedIOBValue: OrefDetermination? {
|
|
var selectedIOBValue: OrefDetermination? {
|
|
|
guard let selection = selection else { return nil }
|
|
guard let selection = selection else { return nil }
|
|
|
- let range = selection.addingTimeInterval(-120) ... selection.addingTimeInterval(120)
|
|
|
|
|
|
|
+ let range = selection.addingTimeInterval(-150) ... selection.addingTimeInterval(150)
|
|
|
return findDetermination(in: range)
|
|
return findDetermination(in: range)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -171,43 +171,17 @@ extension MainChartView {
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
/// show glucose value when hovering over it
|
|
/// show glucose value when hovering over it
|
|
|
- if #available(iOS 17, *) {
|
|
|
|
|
- if let selectedGlucose {
|
|
|
|
|
- RuleMark(x: .value("Selection", selectedGlucose.date ?? now, unit: .minute))
|
|
|
|
|
- .foregroundStyle(Color.tabBar)
|
|
|
|
|
- .offset(yStart: 70)
|
|
|
|
|
- .lineStyle(.init(lineWidth: 2))
|
|
|
|
|
- .annotation(
|
|
|
|
|
- position: .top,
|
|
|
|
|
- alignment: .center,
|
|
|
|
|
- overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
|
|
|
|
|
- ) {
|
|
|
|
|
- selectionPopover
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- PointMark(
|
|
|
|
|
- x: .value("Time", selectedGlucose.date ?? now, unit: .minute),
|
|
|
|
|
- y: .value("Value", selectedGlucose.glucose)
|
|
|
|
|
- )
|
|
|
|
|
- .zIndex(-1)
|
|
|
|
|
- .symbolSize(CGSize(width: 15, height: 15))
|
|
|
|
|
- .foregroundStyle(
|
|
|
|
|
- Decimal(selectedGlucose.glucose) > highGlucose ? Color.orange
|
|
|
|
|
- .opacity(0.8) :
|
|
|
|
|
- (
|
|
|
|
|
- Decimal(selectedGlucose.glucose) < lowGlucose ? Color.red.opacity(0.8) : Color.green
|
|
|
|
|
- .opacity(0.8)
|
|
|
|
|
- )
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- PointMark(
|
|
|
|
|
- x: .value("Time", selectedGlucose.date ?? now, unit: .minute),
|
|
|
|
|
- y: .value("Value", selectedGlucose.glucose)
|
|
|
|
|
- )
|
|
|
|
|
- .zIndex(-1)
|
|
|
|
|
- .symbolSize(CGSize(width: 6, height: 6))
|
|
|
|
|
- .foregroundStyle(Color.primary)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if let selectedGlucose {
|
|
|
|
|
+ SelectionPopoverView(
|
|
|
|
|
+ selectedGlucose: selectedGlucose,
|
|
|
|
|
+ selectedIOBValue: selectedIOBValue,
|
|
|
|
|
+ selectedCOBValue: selectedCOBValue,
|
|
|
|
|
+ units: units,
|
|
|
|
|
+ highGlucose: highGlucose,
|
|
|
|
|
+ lowGlucose: lowGlucose,
|
|
|
|
|
+ currentGlucoseTarget: currentGlucoseTarget,
|
|
|
|
|
+ glucoseColorScheme: glucoseColorScheme
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.id("MainChart")
|
|
.id("MainChart")
|
|
@@ -222,7 +196,7 @@ extension MainChartView {
|
|
|
.chartXAxis { mainChartXAxis }
|
|
.chartXAxis { mainChartXAxis }
|
|
|
.chartYAxis { mainChartYAxis }
|
|
.chartYAxis { mainChartYAxis }
|
|
|
.chartYAxis(.hidden)
|
|
.chartYAxis(.hidden)
|
|
|
- .backport.chartXSelection(value: $selection)
|
|
|
|
|
|
|
+ .chartXSelection(value: $selection)
|
|
|
.chartYScale(
|
|
.chartYScale(
|
|
|
domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue
|
|
domain: units == .mgdL ? state.minYAxisValue ... state.maxYAxisValue : state.minYAxisValue
|
|
|
.asMmolL ... state.maxYAxisValue.asMmolL
|
|
.asMmolL ... state.maxYAxisValue.asMmolL
|
|
@@ -230,186 +204,4 @@ extension MainChartView {
|
|
|
.backport.chartForegroundStyleScale(state: state)
|
|
.backport.chartForegroundStyleScale(state: state)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- @ViewBuilder var selectionPopover: some View {
|
|
|
|
|
- if let sgv = selectedGlucose?.glucose {
|
|
|
|
|
- VStack(alignment: .leading) {
|
|
|
|
|
- HStack {
|
|
|
|
|
- Image(systemName: "clock")
|
|
|
|
|
- Text(selectedGlucose?.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
|
|
|
|
|
- .font(.body).bold()
|
|
|
|
|
- }.font(.body).padding(.bottom, 5)
|
|
|
|
|
-
|
|
|
|
|
- // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
|
|
|
|
|
- let hardCodedLow = Decimal(55)
|
|
|
|
|
- let hardCodedHigh = Decimal(220)
|
|
|
|
|
- let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
|
|
|
|
|
-
|
|
|
|
|
- let glucoseColor = FreeAPS.getDynamicGlucoseColor(
|
|
|
|
|
- glucoseValue: Decimal(sgv),
|
|
|
|
|
- highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
|
|
|
|
|
- lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
|
|
|
|
|
- targetGlucose: currentGlucoseTarget,
|
|
|
|
|
- glucoseColorScheme: glucoseColorScheme
|
|
|
|
|
- )
|
|
|
|
|
- HStack {
|
|
|
|
|
- Text(units == .mgdL ? Decimal(sgv).description : Decimal(sgv).formattedAsMmolL)
|
|
|
|
|
- .bold()
|
|
|
|
|
- + Text(" \(units.rawValue)")
|
|
|
|
|
- }.foregroundStyle(
|
|
|
|
|
- Color(glucoseColor)
|
|
|
|
|
- ).font(.body)
|
|
|
|
|
-
|
|
|
|
|
- if let selectedIOBValue, let iob = selectedIOBValue.iob {
|
|
|
|
|
- HStack {
|
|
|
|
|
- Image(systemName: "syringe.fill").frame(width: 15)
|
|
|
|
|
- Text(MainChartHelper.bolusFormatter.string(from: iob) ?? "")
|
|
|
|
|
- .bold()
|
|
|
|
|
- + Text(NSLocalizedString(" U", comment: "Insulin unit"))
|
|
|
|
|
- }.foregroundStyle(Color.insulin).font(.body)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if let selectedCOBValue {
|
|
|
|
|
- HStack {
|
|
|
|
|
- Image(systemName: "fork.knife").frame(width: 15)
|
|
|
|
|
- Text(MainChartHelper.carbsFormatter.string(from: selectedCOBValue.cob as NSNumber) ?? "")
|
|
|
|
|
- .bold()
|
|
|
|
|
- + Text(NSLocalizedString(" g", comment: "gram of carbs"))
|
|
|
|
|
- }.foregroundStyle(Color.orange).font(.body)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- .padding()
|
|
|
|
|
- .background {
|
|
|
|
|
- RoundedRectangle(cornerRadius: 4)
|
|
|
|
|
- .fill(Color.chart.opacity(0.85))
|
|
|
|
|
- .shadow(color: Color.secondary, radius: 2)
|
|
|
|
|
- .overlay(
|
|
|
|
|
- RoundedRectangle(cornerRadius: 4)
|
|
|
|
|
- .stroke(Color.secondary, lineWidth: 2)
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// MARK: - Rule Marks and Charts configurations
|
|
|
|
|
-
|
|
|
|
|
-extension MainChartView {
|
|
|
|
|
- func drawCurrentTimeMarker() -> some ChartContent {
|
|
|
|
|
- RuleMark(
|
|
|
|
|
- x: .value(
|
|
|
|
|
- "",
|
|
|
|
|
- Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
|
|
|
|
|
- unit: .second
|
|
|
|
|
- )
|
|
|
|
|
- ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func drawStartRuleMark() -> some ChartContent {
|
|
|
|
|
- RuleMark(
|
|
|
|
|
- x: .value(
|
|
|
|
|
- "",
|
|
|
|
|
- startMarker,
|
|
|
|
|
- unit: .second
|
|
|
|
|
- )
|
|
|
|
|
- ).foregroundStyle(Color.clear)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func drawEndRuleMark() -> some ChartContent {
|
|
|
|
|
- RuleMark(
|
|
|
|
|
- x: .value(
|
|
|
|
|
- "",
|
|
|
|
|
- endMarker,
|
|
|
|
|
- unit: .second
|
|
|
|
|
- )
|
|
|
|
|
- ).foregroundStyle(Color.clear)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- 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 cobIobChartYAxis: 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]))
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// MARK: - Calculations and formatting
|
|
|
|
|
-
|
|
|
|
|
-extension MainChartView {
|
|
|
|
|
- func fullWidth(viewWidth: CGFloat) -> CGFloat {
|
|
|
|
|
- viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 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))
|
|
|
|
|
-
|
|
|
|
|
- // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
|
|
|
|
|
- let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
|
|
|
|
|
- Int(1.5) * 5 * state
|
|
|
|
|
- .minCount * 60
|
|
|
|
|
- ))
|
|
|
|
|
-
|
|
|
|
|
- endMarker = state
|
|
|
|
|
- .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
|
|
|
|
|
- dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-extension Int16 {
|
|
|
|
|
- var minutes: TimeInterval {
|
|
|
|
|
- TimeInterval(self) * 60
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|