Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 147 additions & 29 deletions InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import Charts
struct HeartChartDataPoint: Identifiable {
var id = UUID()
let date: Date
let value: Double
let min: Double
let max: Double
let average: Double
let values: [Double]
}

struct HeartChartView: View {
Expand All @@ -22,21 +25,89 @@ struct HeartChartView: View {
@AppStorage("maxHeartRange") private var maxHeartRange = 200

@State private var points = [HeartChartDataPoint]()
@State private var dayOffset: Int = 0
@State private var displayedDate: Date = Date()
@State private var displayedMin: Int = 0
@State private var displayedMax: Int = 0

var windowStart: Date {
Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: dayOffset, to: Date())!)
}
var windowEnd: Date {
Date(timeInterval: 86400, since: windowStart)
}
var windowPoints: [HeartChartDataPoint] {
points.filter { $0.date >= windowStart && $0.date <= windowEnd }
}

func heartPoints() -> [HeartChartDataPoint] {
return ChartManager.shared.heartPoints().map { HeartChartDataPoint(date: $0.timestamp ?? Date(), value: $0.value) }
let raw = ChartManager.shared.heartPoints()

let grouped = Dictionary(grouping: raw) { sample -> Date in
let comps = Calendar.current.dateComponents([.year, .month, .day, .hour], from: sample.timestamp ?? Date())
return Calendar.current.date(from: comps) ?? Date()
}

return grouped.map { (bucket, samples) in
let values = samples.map { $0.value }
return HeartChartDataPoint(
date: bucket,
min: values.min() ?? 0,
max: values.max() ?? 0,
average: values.reduce(0, +) / Double(values.count),
values: values
)
}.sorted { $0.date < $1.date }
}

var earliestDate: Date {
return points.compactMap({ $0.date }).min() ?? Date()
points.map({ $0.date }).min() ?? Date()
}
var latestDate: Date {
return points.compactMap({ $0.date }).max() ?? Date()
points.map({ $0.date }).max() ?? Date()
}
var max: Int {
return Int(points.compactMap({ $0.value }).max() ?? 0)

let heartColor = Color(red: 0.996, green: 0.212, blue: 0.369)
let darkHeartColor = Color(red: 0.369, green: 0.090, blue: 0.145)

func isSingleReading(_ point: HeartChartDataPoint) -> Bool {
point.min == point.max
}

@ChartContentBuilder
func chartContent(for point: HeartChartDataPoint) -> some ChartContent {
if isSingleReading(point) {
PointMark(
x: .value("Time", point.date),
y: .value("BPM", point.min)
)
.foregroundStyle(heartColor)
.symbolSize(40)
.symbol(.circle)
} else {
RectangleMark(
x: .value("Time", point.date),
yStart: .value("Min", point.min),
yEnd: .value("Max", point.max),
width: 7
)
.foregroundStyle(darkHeartColor)
.clipShape(Capsule())

PointMark(
x: .value("Time", point.date),
y: .value("BPM", point.average)
)
.foregroundStyle(heartColor)
.symbolSize(CGSize(width: 7, height: 7))
.symbol(.circle)
}
}
var min: Int {
return Int(points.compactMap({ $0.value }).min() ?? 0)

func updateDisplayed() {
displayedDate = windowStart
displayedMin = Int(windowPoints.map({ $0.min }).min() ?? 0)
displayedMax = Int(windowPoints.map({ $0.max }).max() ?? 0)
}

var body: some View {
Expand All @@ -46,30 +117,67 @@ struct HeartChartView: View {
EmptyChartView(.heart)
} else {
Section {
Chart(points) { point in
PointMark(
x: .value("Time", point.date),
y: .value("BPM", point.value)
)
VStack(spacing: 0) {
HStack {
Button {
dayOffset -= 1
} label: {
Image(systemName: "chevron.left")
}
.disabled(Calendar.current.isDate(windowStart, inSameDayAs: earliestDate))

Spacer()

Text(displayedDate.formatted(.dateTime.weekday(.abbreviated).month(.abbreviated).day().year()))
.foregroundColor(.primary)

Spacer()

Button {
dayOffset += 1
} label: {
Image(systemName: "chevron.right")
}
.disabled(dayOffset >= 0)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(.secondarySystemGroupedBackground))
.clipShape(Capsule())
.foregroundStyle(Color.red)
.padding(.bottom, 8)

Chart {
ForEach(windowPoints) { point in
chartContent(for: point)
}
}
.frame(height: 280)
.padding(.horizontal, 8)
.chartYScale(domain: (displayedMin - 20)...(displayedMax + 20))
.chartXScale(domain: windowStart...windowEnd)
.chartXAxis {
AxisMarks(values: .stride(by: .hour, count: 6)) { value in
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4]))
AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .omitted)))
}
}
.chartYAxis {
AxisMarks(position: .trailing) { value in
AxisGridLine()
AxisValueLabel()
}
}
}
.frame(height: 280)
.chartYScale(domain: minHeartRange...maxHeartRange)
.buttonStyle(.plain)
} header: {
VStack(alignment: .leading) {
Text(points.count > 1 ? "Range" : "No Data")
Text({
if max == 0 || min == 0 {
return "0 "
} else {
return "\(min)-\(max) "
}
}())
.font(.system(.title, design: .rounded))
.foregroundColor(.primary)
Text("Range")
.font(.caption)
.foregroundColor(.secondary)
Text(displayedMax == 0 || displayedMin == 0 ? "0 " : "\(displayedMin)–\(displayedMax) ")
.font(.system(.title, design: .rounded))
.foregroundColor(.primary)
+ Text("BPM")
Text("\(earliestDate.formatted(.dateTime.month(.abbreviated).day()))-\(latestDate.formatted(.dateTime.day()))")
}
.fontWeight(.semibold)
}
Expand All @@ -79,16 +187,26 @@ struct HeartChartView: View {
.listRowBackground(Color.clear)
if points.count >= 3 {
Section {
Text("Today your heart rate reached a high of \(max), and dropped to a low of \(min) BPM.")
// Text("Is a heart point in an exercise in the last day: \(ExerciseViewModel.shared.isDateDuringExercise(Date()))")
Text("Today your heart rate reached a high of \(displayedMax), and dropped to a low of \(displayedMin) BPM.")
}
}
}
.onAppear {
points = heartPoints()
updateDisplayed()
}
.onChange(of: dayOffset) { _ in
updateDisplayed()
}
.onChange(of: bleManager.heartRate) { _ in
points = heartPoints()
updateDisplayed()
}
}
}

#Preview {
List {
HeartChartView()
}
}
2 changes: 1 addition & 1 deletion InfiniLink/Utils/ChartManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class ChartManager: ObservableObject {

func heartPoints(predicate: NSPredicate? = nil) -> [HeartDataPoint] {
let fetchRequest: NSFetchRequest<HeartDataPoint> = HeartDataPoint.fetchRequest()
fetchRequest.predicate = predicate ?? dayPredicate
fetchRequest.predicate = predicate ?? weekPredicate

do {
return try persistenceController.container.viewContext.fetch(fetchRequest)
Expand Down