LiveActivityChartView.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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 calendar = Calendar.current
  30. let now = Date()
  31. let startDate = calendar.date(byAdding: .hour, value: isWatchOS ? -3 : -6, to: now) ?? now
  32. let endDate = isOverrideActive ? (calendar.date(byAdding: .hour, value: 2, to: now) ?? now) :
  33. (calendar.date(byAdding: .minute, value: isWatchOS ? 5 : 0, to: now) ?? now)
  34. // 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
  35. let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
  36. let hardCodedHigh = isMgdL ? Decimal(220) : 220.asMmolL
  37. let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
  38. let highColor = Color.getDynamicGlucoseColor(
  39. glucoseValue: yAxisRuleMarkMax,
  40. highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : yAxisRuleMarkMax,
  41. lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : yAxisRuleMarkMin,
  42. targetGlucose: target,
  43. glucoseColorScheme: context.state.glucoseColorScheme
  44. )
  45. let lowColor = Color.getDynamicGlucoseColor(
  46. glucoseValue: yAxisRuleMarkMin,
  47. highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : yAxisRuleMarkMax,
  48. lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : yAxisRuleMarkMin,
  49. targetGlucose: target,
  50. glucoseColorScheme: context.state.glucoseColorScheme
  51. )
  52. Chart {
  53. RuleMark(y: .value("High", yAxisRuleMarkMax))
  54. .foregroundStyle(highColor)
  55. .lineStyle(.init(lineWidth: 1, dash: [5]))
  56. RuleMark(y: .value("Low", yAxisRuleMarkMin))
  57. .foregroundStyle(lowColor)
  58. .lineStyle(.init(lineWidth: 1, dash: [5]))
  59. RuleMark(y: .value("Target", target))
  60. .foregroundStyle(.green.gradient)
  61. .lineStyle(.init(lineWidth: 1.5))
  62. if isOverrideActive {
  63. drawActiveOverrides()
  64. }
  65. drawChart(yAxisRuleMarkMin: yAxisRuleMarkMin, yAxisRuleMarkMax: yAxisRuleMarkMax)
  66. }
  67. .chartYAxis {
  68. AxisMarks(position: .trailing) { _ in
  69. AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
  70. .foregroundStyle(Color.white.opacity(colorScheme == .light ? 1 : 0.5))
  71. AxisValueLabel().foregroundStyle(.primary).font(.footnote)
  72. }
  73. }
  74. .chartYScale(domain: state.unit == "mg/dL" ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
  75. .chartYAxis(.hidden)
  76. .chartPlotStyle { plotContent in
  77. plotContent
  78. .background(
  79. RoundedRectangle(cornerRadius: 12)
  80. .fill(colorScheme == .light ? Color.black.opacity(0.2) : .clear)
  81. )
  82. .clipShape(RoundedRectangle(cornerRadius: 12))
  83. }
  84. .chartXScale(domain: startDate ... endDate)
  85. .chartXAxis {
  86. AxisMarks(position: .automatic) { _ in
  87. AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
  88. .foregroundStyle(Color.primary.opacity(colorScheme == .light ? 1 : 0.5))
  89. }
  90. }
  91. }
  92. private func drawActiveOverrides() -> some ChartContent {
  93. let start: Date = context.state.detailedViewState.overrideDate
  94. let duration = context.state.detailedViewState.overrideDuration
  95. let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
  96. let end: Date = start.addingTimeInterval(durationAsTimeInterval)
  97. let target = context.state.detailedViewState.overrideTarget
  98. return RuleMark(
  99. xStart: .value("Start", start, unit: .second),
  100. xEnd: .value("End", end, unit: .second),
  101. y: .value("Value", target)
  102. )
  103. .foregroundStyle(Color.purple.opacity(0.6))
  104. .lineStyle(.init(lineWidth: 8))
  105. }
  106. private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
  107. // 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
  108. let hardCodedLow = Decimal(55)
  109. let hardCodedHigh = Decimal(220)
  110. let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
  111. let isMgdL = context.state.unit == "mg/dL"
  112. let threeHours = TimeInterval(10800)
  113. let chartData = isWatchOS ? additionalState.chart
  114. .filter { abs($0.date.timeIntervalSinceNow) < threeHours } : additionalState
  115. .chart
  116. return ForEach(chartData, id: \.self) { item in
  117. let displayValue = isMgdL ? item.value : item.value.asMmolL
  118. let pointMarkColor = Color.getDynamicGlucoseColor(
  119. glucoseValue: item.value,
  120. highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : context.state.highGlucose,
  121. lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : context.state.lowGlucose,
  122. targetGlucose: context.state.target,
  123. glucoseColorScheme: context.state.glucoseColorScheme
  124. )
  125. let pointMark = PointMark(
  126. x: .value("Time", item.date),
  127. y: .value("Value", displayValue)
  128. )
  129. .symbolSize(16)
  130. .shadow(color: Color.black.opacity(0.25), radius: 2, x: 0, y: 0)
  131. pointMark.foregroundStyle(pointMarkColor)
  132. }
  133. }
  134. }