|
@@ -23,6 +23,8 @@ struct TotalDailyDoseChart: View {
|
|
|
@State private var updateTimer = Stat.UpdateTimer()
|
|
@State private var updateTimer = Stat.UpdateTimer()
|
|
|
/// Sum of hourly doses for `Day` view
|
|
/// Sum of hourly doses for `Day` view
|
|
|
@State private var sumOfHourlyDoses: Double = 0
|
|
@State private var sumOfHourlyDoses: Double = 0
|
|
|
|
|
+ /// The actual chart plot's width in pixel
|
|
|
|
|
+ @State private var chartWidth: CGFloat = 0
|
|
|
|
|
|
|
|
/// Computes the visible date range based on the current scroll position.
|
|
/// Computes the visible date range based on the current scroll position.
|
|
|
private var visibleDateRange: (start: Date, end: Date) {
|
|
private var visibleDateRange: (start: Date, end: Date) {
|
|
@@ -62,6 +64,13 @@ struct TotalDailyDoseChart: View {
|
|
|
.padding(.bottom, 4)
|
|
.padding(.bottom, 4)
|
|
|
|
|
|
|
|
chartsView
|
|
chartsView
|
|
|
|
|
+ .background(
|
|
|
|
|
+ GeometryReader { geo in
|
|
|
|
|
+ Color.clear
|
|
|
|
|
+ .onAppear { chartWidth = geo.size.width }
|
|
|
|
|
+ .onChange(of: geo.size.width) { _, newValue in chartWidth = newValue }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.onAppear {
|
|
.onAppear {
|
|
@@ -153,7 +162,13 @@ struct TotalDailyDoseChart: View {
|
|
|
spacing: 0,
|
|
spacing: 0,
|
|
|
overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
|
|
overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
|
|
|
) {
|
|
) {
|
|
|
- TDDSelectionPopover(date: selectedDate, tdd: selectedTDD, selectedInterval: selectedInterval)
|
|
|
|
|
|
|
+ TDDSelectionPopover(
|
|
|
|
|
+ selectedDate: selectedDate,
|
|
|
|
|
+ tdd: selectedTDD,
|
|
|
|
|
+ selectedInterval: selectedInterval,
|
|
|
|
|
+ domain: visibleDateRange,
|
|
|
|
|
+ chartWidth: chartWidth
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -240,33 +255,88 @@ struct TotalDailyDoseChart: View {
|
|
|
/// - tdd: The TDDStats containing insulin usage data.
|
|
/// - tdd: The TDDStats containing insulin usage data.
|
|
|
/// - selectedInterval: The selected time interval (hourly or daily).
|
|
/// - selectedInterval: The selected time interval (hourly or daily).
|
|
|
private struct TDDSelectionPopover: View {
|
|
private struct TDDSelectionPopover: View {
|
|
|
- let date: Date
|
|
|
|
|
|
|
+ let selectedDate: Date
|
|
|
let tdd: TDDStats
|
|
let tdd: TDDStats
|
|
|
let selectedInterval: Stat.StateModel.StatsTimeInterval
|
|
let selectedInterval: Stat.StateModel.StatsTimeInterval
|
|
|
|
|
+ let domain: (start: Date, end: Date)
|
|
|
|
|
+ let chartWidth: CGFloat
|
|
|
|
|
+
|
|
|
|
|
+ @State private var popoverSize: CGSize = .zero
|
|
|
|
|
+
|
|
|
|
|
+ @Environment(\.colorScheme) var colorScheme
|
|
|
|
|
|
|
|
private var timeText: String {
|
|
private var timeText: String {
|
|
|
if selectedInterval == .day {
|
|
if selectedInterval == .day {
|
|
|
- let hour = Calendar.current.component(.hour, from: date)
|
|
|
|
|
- return "\(hour):00-\(hour + 1):00"
|
|
|
|
|
|
|
+ let hour = Calendar.current.component(.hour, from: selectedDate)
|
|
|
|
|
+ return selectedDate.formatted(.dateTime.month().day().weekday()) + "\n" + "\(hour):00-\(hour + 1):00"
|
|
|
} else {
|
|
} else {
|
|
|
- return date.formatted(.dateTime.month().day())
|
|
|
|
|
|
|
+ return selectedDate.formatted(.dateTime.month().day().weekday())
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func xOffset() -> CGFloat {
|
|
|
|
|
+ let domainDuration = domain.end.timeIntervalSince(domain.start)
|
|
|
|
|
+ guard domainDuration > 0, chartWidth > 0 else { return 0 }
|
|
|
|
|
+
|
|
|
|
|
+ let popoverWidth = popoverSize.width
|
|
|
|
|
+
|
|
|
|
|
+ // Convert dates to pixel'd x-condition
|
|
|
|
|
+ let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
|
|
|
|
|
+ let x_selected = dateFraction * chartWidth
|
|
|
|
|
+
|
|
|
|
|
+ // TODO: this is semi hacky, can this be improved?
|
|
|
|
|
+ let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
|
|
|
|
|
+ let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
|
|
|
|
|
+
|
|
|
|
|
+ var offset: CGFloat = 0 // Default = no shift
|
|
|
|
|
+
|
|
|
|
|
+ // Push popover to right if its left edge is (nearing) out-of-bounds
|
|
|
|
|
+ if x_left < 0 {
|
|
|
|
|
+ offset = abs(x_left) // push to right
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Push popover to left if its right edge is (nearing) out-of-bounds)
|
|
|
|
|
+ if x_right > chartWidth {
|
|
|
|
|
+ offset = -(x_right - chartWidth) // push to left
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return offset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
var body: some View {
|
|
var body: some View {
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
Text(timeText)
|
|
Text(timeText)
|
|
|
.font(.subheadline)
|
|
.font(.subheadline)
|
|
|
- .fontWeight(.bold)
|
|
|
|
|
|
|
+ .bold()
|
|
|
|
|
+ .foregroundStyle(Color.secondary)
|
|
|
|
|
+
|
|
|
|
|
+ Divider()
|
|
|
|
|
|
|
|
- Text(tdd.amount.formatted(.number.precision(.fractionLength(1))) + " U")
|
|
|
|
|
- .font(.title3.bold())
|
|
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text(tdd.amount.formatted(.number.precision(.fractionLength(1))))
|
|
|
|
|
+ Text("U").foregroundStyle(Color.secondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ .font(.headline)
|
|
|
}
|
|
}
|
|
|
- .foregroundStyle(.white)
|
|
|
|
|
.padding(20)
|
|
.padding(20)
|
|
|
.background {
|
|
.background {
|
|
|
RoundedRectangle(cornerRadius: 10)
|
|
RoundedRectangle(cornerRadius: 10)
|
|
|
- .fill(Color.insulin)
|
|
|
|
|
|
|
+ .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
|
|
|
|
|
+ .shadow(color: Color.secondary, radius: 2)
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 4)
|
|
|
|
|
+ .stroke(Color.blue, lineWidth: 2)
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
+ .frame(minWidth: 100, maxWidth: .infinity) // Ensures proper width
|
|
|
|
|
+ .background(
|
|
|
|
|
+ GeometryReader { geo in
|
|
|
|
|
+ Color.clear
|
|
|
|
|
+ .onAppear { popoverSize = geo.size }
|
|
|
|
|
+ .onChange(of: geo.size) { _, newValue in popoverSize = newValue }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ // Apply calculated xOffset to keep within bounds
|
|
|
|
|
+ .offset(x: xOffset(), y: 0)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|