LiveActivityChartView.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. //
  2. // LiveActivityChartView.swift
  3. // Trio
  4. //
  5. // Created by Cengiz Deniz on 17.10.24.
  6. //
  7. import Charts
  8. import Foundation
  9. import SwiftUI
  10. import WidgetKit
  11. struct LiveActivityChartView: View {
  12. @Environment(\.colorScheme) var colorScheme
  13. @Environment(\.isWatchOS) var isWatchOS
  14. var context: ActivityViewContext<LiveActivityAttributes>
  15. var additionalState: LiveActivityAttributes.ContentAdditionalState
  16. var body: some View {
  17. let state = context.state
  18. let isMgdL: Bool = state.unit == "mg/dL"
  19. let maxThreshhold: Decimal = isWatchOS ? 220 : 300
  20. // Determine scale
  21. let minValue = min(additionalState.chart.min(by: { $0.value < $1.value })?.value ?? 39, 39)
  22. let maxValue = max(additionalState.chart.max(by: { $0.value < $1.value })?.value ?? maxThreshhold, maxThreshhold)
  23. let yAxisRuleMarkMin = isMgdL ? state.lowGlucose : state.lowGlucose
  24. .asMmolL
  25. let yAxisRuleMarkMax = isMgdL ? state.highGlucose : state.highGlucose
  26. .asMmolL
  27. let target = isMgdL ? state.target : state.target.asMmolL
  28. let isOverrideActive = additionalState.isOverrideActive == true
  29. let isTempTargetActive = additionalState.isTempTargetActive == true
  30. let calendar = Calendar.current
  31. let now = Date()
  32. let startDate = calendar.date(byAdding: .hour, value: isWatchOS ? -3 : -6, to: now) ?? now
  33. let endDate = (isOverrideActive || isTempTargetActive) ? (calendar.date(byAdding: .hour, value: 2, to: now) ?? now) :
  34. (calendar.date(byAdding: .minute, value: isWatchOS ? 5 : 0, to: now) ?? now)
  35. // 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
  36. let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
  37. let hardCodedHigh = isMgdL ? Decimal(220) : 220.asMmolL
  38. let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
  39. let highColor = Color.getDynamicGlucoseColor(
  40. glucoseValue: yAxisRuleMarkMax,
  41. highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : yAxisRuleMarkMax,
  42. lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : yAxisRuleMarkMin,
  43. targetGlucose: target,
  44. glucoseColorScheme: context.state.glucoseColorScheme
  45. )
  46. let lowColor = Color.getDynamicGlucoseColor(
  47. glucoseValue: yAxisRuleMarkMin,
  48. highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : yAxisRuleMarkMax,
  49. lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : yAxisRuleMarkMin,
  50. targetGlucose: target,
  51. glucoseColorScheme: context.state.glucoseColorScheme
  52. )
  53. Chart {
  54. RuleMark(y: .value("High", yAxisRuleMarkMax))
  55. .foregroundStyle(highColor)
  56. .lineStyle(.init(lineWidth: 1, dash: [5]))
  57. RuleMark(y: .value("Low", yAxisRuleMarkMin))
  58. .foregroundStyle(lowColor)
  59. .lineStyle(.init(lineWidth: 1, dash: [5]))
  60. RuleMark(y: .value("Target", target))
  61. .foregroundStyle(.green.gradient)
  62. .lineStyle(.init(lineWidth: 1.5))
  63. if isOverrideActive {
  64. drawActiveOverrides()
  65. }
  66. if isTempTargetActive {
  67. drawActiveTempTarget()
  68. }
  69. drawChart(yAxisRuleMarkMin: yAxisRuleMarkMin, yAxisRuleMarkMax: yAxisRuleMarkMax)
  70. }
  71. .chartYAxis {
  72. AxisMarks(position: .trailing) { _ in
  73. AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
  74. .foregroundStyle(Color.white.opacity(colorScheme == .light ? 1 : 0.5))
  75. AxisValueLabel().foregroundStyle(.primary).font(.footnote)
  76. }
  77. }
  78. .chartYScale(domain: state.unit == "mg/dL" ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
  79. .chartYAxis(.hidden)
  80. .chartPlotStyle { plotContent in
  81. plotContent
  82. .background(
  83. RoundedRectangle(cornerRadius: 12)
  84. .fill(colorScheme == .light ? Color.black.opacity(0.2) : .clear)
  85. )
  86. .clipShape(RoundedRectangle(cornerRadius: 12))
  87. }
  88. .chartXScale(domain: startDate ... endDate)
  89. .chartXAxis {
  90. AxisMarks(position: .automatic) { _ in
  91. AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
  92. .foregroundStyle(Color.primary.opacity(colorScheme == .light ? 1 : 0.5))
  93. }
  94. }
  95. }
  96. private func drawActiveOverrides() -> some ChartContent {
  97. let start: Date = context.state.detailedViewState.overrideDate
  98. let duration = context.state.detailedViewState.overrideDuration
  99. let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
  100. let end: Date = start.addingTimeInterval(durationAsTimeInterval)
  101. let target = context.state.detailedViewState.overrideTarget
  102. return RuleMark(
  103. xStart: .value("Start", start, unit: .second),
  104. xEnd: .value("End", end, unit: .second),
  105. y: .value("Value", target)
  106. )
  107. .foregroundStyle(Color.purple.opacity(0.6))
  108. .lineStyle(.init(lineWidth: 8))
  109. }
  110. private func drawActiveTempTarget() -> some ChartContent {
  111. let start: Date = context.state.detailedViewState.tempTargetDate
  112. let duration = context.state.detailedViewState.tempTargetDuration
  113. let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
  114. let end: Date = start.addingTimeInterval(durationAsTimeInterval)
  115. let target = context.state.detailedViewState.tempTargetTarget
  116. return RuleMark(
  117. xStart: .value("Start", start, unit: .second),
  118. xEnd: .value("End", end, unit: .second),
  119. y: .value("Value", target)
  120. )
  121. .foregroundStyle(Color("LoopGreen").opacity(0.6))
  122. .lineStyle(.init(lineWidth: 8))
  123. }
  124. private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
  125. // 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
  126. let hardCodedLow = Decimal(55)
  127. let hardCodedHigh = Decimal(220)
  128. let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
  129. let isMgdL = context.state.unit == "mg/dL"
  130. let threeHours = TimeInterval(10800)
  131. let chartData = isWatchOS ? additionalState.chart
  132. .filter { abs($0.date.timeIntervalSinceNow) < threeHours } : additionalState
  133. .chart
  134. return ForEach(chartData, id: \.self) { item in
  135. let displayValue = isMgdL ? item.value : item.value.asMmolL
  136. let pointMarkColor = Color.getDynamicGlucoseColor(
  137. glucoseValue: item.value,
  138. highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : context.state.highGlucose,
  139. lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : context.state.lowGlucose,
  140. targetGlucose: context.state.target,
  141. glucoseColorScheme: context.state.glucoseColorScheme
  142. )
  143. let pointMark = PointMark(
  144. x: .value("Time", item.date),
  145. y: .value("Value", displayValue)
  146. )
  147. .symbolSize(16)
  148. .shadow(color: Color.black.opacity(0.25), radius: 2, x: 0, y: 0)
  149. pointMark.foregroundStyle(pointMarkColor)
  150. }
  151. }
  152. }