|
| 1 | +// |
| 2 | +// ChartValues.swift |
| 3 | +// Loop Widget Extension |
| 4 | +// |
| 5 | +// Created by Bastiaan Verhaar on 25/06/2024. |
| 6 | +// Copyright © 2024 LoopKit Authors. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +import Foundation |
| 10 | +import SwiftUI |
| 11 | +import Charts |
| 12 | + |
| 13 | +@available(iOS 16.2, *) |
| 14 | +struct ChartView: View { |
| 15 | + private let glucoseSampleData: [ChartValues] |
| 16 | + private let predicatedData: [ChartValues] |
| 17 | + private let glucoseRanges: [GlucoseRangeValue] |
| 18 | + private let preset: Preset? |
| 19 | + private let yAxisMarks: [Double] |
| 20 | + |
| 21 | + init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) { |
| 22 | + self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit) |
| 23 | + self.predicatedData = ChartValues.convert( |
| 24 | + data: predicatedGlucose, |
| 25 | + startDate: predicatedStartDate ?? Date.now, |
| 26 | + interval: predicatedInterval ?? .minutes(5), |
| 27 | + useLimits: useLimits, |
| 28 | + lowerLimit: lowerLimit, |
| 29 | + upperLimit: upperLimit |
| 30 | + ) |
| 31 | + self.preset = preset |
| 32 | + self.glucoseRanges = glucoseRanges |
| 33 | + self.yAxisMarks = yAxisMarks |
| 34 | + } |
| 35 | + |
| 36 | + init(glucoseSamples: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) { |
| 37 | + self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit) |
| 38 | + self.predicatedData = [] |
| 39 | + self.preset = preset |
| 40 | + self.glucoseRanges = glucoseRanges |
| 41 | + self.yAxisMarks = yAxisMarks |
| 42 | + } |
| 43 | + |
| 44 | + var body: some View { |
| 45 | + ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){ |
| 46 | + Chart { |
| 47 | + if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) { |
| 48 | + RectangleMark( |
| 49 | + xStart: .value("Start", preset.startDate), |
| 50 | + xEnd: .value("End", preset.endDate), |
| 51 | + yStart: .value("Preset override", preset.minValue), |
| 52 | + yEnd: .value("Preset override", preset.maxValue) |
| 53 | + ) |
| 54 | + .foregroundStyle(.primary) |
| 55 | + .opacity(0.6) |
| 56 | + } |
| 57 | + |
| 58 | + ForEach(glucoseRanges) { item in |
| 59 | + RectangleMark( |
| 60 | + xStart: .value("Start", item.startDate), |
| 61 | + xEnd: .value("End", item.endDate), |
| 62 | + yStart: .value("Glucose range", item.minValue), |
| 63 | + yEnd: .value("Glucose range", item.maxValue) |
| 64 | + ) |
| 65 | + .foregroundStyle(.primary) |
| 66 | + .opacity(0.3) |
| 67 | + } |
| 68 | + |
| 69 | + ForEach(glucoseSampleData) { item in |
| 70 | + PointMark (x: .value("Date", item.x), |
| 71 | + y: .value("Glucose level", item.y) |
| 72 | + ) |
| 73 | + .symbolSize(10) |
| 74 | + .foregroundStyle(by: .value("Color", item.color)) |
| 75 | + } |
| 76 | + |
| 77 | + ForEach(predicatedData) { item in |
| 78 | + LineMark (x: .value("Date", item.x), |
| 79 | + y: .value("Glucose level", item.y) |
| 80 | + ) |
| 81 | + .lineStyle(StrokeStyle(lineWidth: 2, dash: [6, 5])) |
| 82 | + .foregroundStyle(by: .value("Color", item.color)) |
| 83 | + } |
| 84 | + } |
| 85 | + .chartForegroundStyleScale([ |
| 86 | + "Good": .green, |
| 87 | + "High": .orange, |
| 88 | + "Low": .red, |
| 89 | + "Default": Color("glucose") |
| 90 | + ]) |
| 91 | + .chartPlotStyle { plotContent in |
| 92 | + plotContent.background(.cyan.opacity(0.15)) |
| 93 | + } |
| 94 | + .chartLegend(.hidden) |
| 95 | + .chartYScale(domain: [yAxisMarks.first ?? 0, yAxisMarks.last ?? 0]) |
| 96 | + .chartYAxis { |
| 97 | + AxisMarks(values: yAxisMarks) |
| 98 | + } |
| 99 | + .chartYAxis { |
| 100 | + AxisMarks(position: .leading) { _ in |
| 101 | + AxisValueLabel().foregroundStyle(Color.primary) |
| 102 | + AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])) |
| 103 | + .foregroundStyle(Color.primary) |
| 104 | + } |
| 105 | + } |
| 106 | + .chartXAxis { |
| 107 | + AxisMarks(position: .automatic, values: .stride(by: .hour)) { _ in |
| 108 | + AxisValueLabel(format: .dateTime.hour(.twoDigits(amPM: .narrow)), anchor: .top) |
| 109 | + .foregroundStyle(Color.primary) |
| 110 | + AxisGridLine(stroke: .init(lineWidth: 0.1, dash: [2, 3])) |
| 111 | + .foregroundStyle(Color.primary) |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + if let preset = self.preset, preset.endDate > Date.now { |
| 116 | + Text(preset.title) |
| 117 | + .font(.footnote) |
| 118 | + .padding(.trailing, 5) |
| 119 | + .padding(.top, 2) |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +struct ChartValues: Identifiable { |
| 126 | + public let id: UUID |
| 127 | + public let x: Date |
| 128 | + public let y: Double |
| 129 | + public let color: String |
| 130 | + |
| 131 | + init(x: Date, y: Double, color: String) { |
| 132 | + self.id = UUID() |
| 133 | + self.x = x |
| 134 | + self.y = y |
| 135 | + self.color = color |
| 136 | + } |
| 137 | + |
| 138 | + static func convert(data: [Double], startDate: Date, interval: TimeInterval, useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] { |
| 139 | + let twoHours = Date.now.addingTimeInterval(.hours(4)) |
| 140 | + |
| 141 | + return data.enumerated().filter { (index, item) in |
| 142 | + return startDate.addingTimeInterval(interval * Double(index)) < twoHours |
| 143 | + }.map { (index, item) in |
| 144 | + return ChartValues( |
| 145 | + x: startDate.addingTimeInterval(interval * Double(index)), |
| 146 | + y: item, |
| 147 | + color: !useLimits ? "Default" : item < lowerLimit ? "Low" : item > upperLimit ? "High" : "Good" |
| 148 | + ) |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + static func convert(data: [GlucoseSampleAttributes], useLimits: Bool, lowerLimit: Double, upperLimit: Double) -> [ChartValues] { |
| 153 | + return data.map { item in |
| 154 | + return ChartValues( |
| 155 | + x: item.x, |
| 156 | + y: item.y, |
| 157 | + color: !useLimits ? "Default" : item.y < lowerLimit ? "Low" : item.y > upperLimit ? "High" : "Good" |
| 158 | + ) |
| 159 | + } |
| 160 | + } |
| 161 | +} |
0 commit comments