Skip to content

Commit

Permalink
charts: begin implementation of Apple Charts
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcharger committed Jan 11, 2025
1 parent f20e048 commit aff10bb
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 122 deletions.
Binary file modified .DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions InfiniLink.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
E09696DF2D2EC7EC00CCCBF8 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696DE2D2EC7EC00CCCBF8 /* String+Extension.swift */; };
E09696E32D2F807700CCCBF8 /* HeartChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696E22D2F807700CCCBF8 /* HeartChartView.swift */; };
E09696E52D318AFB00CCCBF8 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696E42D318AFB00CCCBF8 /* Data+Extension.swift */; };
E09696E72D323A6D00CCCBF8 /* StepChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696E62D323A6D00CCCBF8 /* StepChartView.swift */; };
E0A7C06B2CB0DE4C0042A12D /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A7C06A2CB0DE4C0042A12D /* Weather.swift */; };
E0A7C0732CB0ECCE0042A12D /* NotificationsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A7C0722CB0ECCE0042A12D /* NotificationsSettingsView.swift */; };
E0A7C0762CB17E520042A12D /* MusicSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A7C0752CB17E520042A12D /* MusicSettingsView.swift */; };
Expand Down Expand Up @@ -190,6 +191,7 @@
E09696DE2D2EC7EC00CCCBF8 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
E09696E22D2F807700CCCBF8 /* HeartChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartChartView.swift; sourceTree = "<group>"; };
E09696E42D318AFB00CCCBF8 /* Data+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extension.swift"; sourceTree = "<group>"; };
E09696E62D323A6D00CCCBF8 /* StepChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepChartView.swift; sourceTree = "<group>"; };
E0A7C06A2CB0DE4C0042A12D /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = "<group>"; };
E0A7C0722CB0ECCE0042A12D /* NotificationsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsView.swift; sourceTree = "<group>"; };
E0A7C0752CB17E520042A12D /* MusicSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicSettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -433,6 +435,7 @@
isa = PBXGroup;
children = (
E09696E22D2F807700CCCBF8 /* HeartChartView.swift */,
E09696E62D323A6D00CCCBF8 /* StepChartView.swift */,
);
path = Charts;
sourceTree = "<group>";
Expand Down Expand Up @@ -795,6 +798,7 @@
E0A7C0922CB1EE8F0042A12D /* DFUUpdater.swift in Sources */,
E0A7C0932CB1EE8F0042A12D /* DownloadManager.swift in Sources */,
E08C4A492CC4048900013D15 /* SleepData.swift in Sources */,
E09696E72D323A6D00CCCBF8 /* StepChartView.swift in Sources */,
E05999BC2CB336AE00D64E0B /* HealthKitManager.swift in Sources */,
E0599A022CB4C38700D64E0B /* FilesystemView.swift in Sources */,
E03C30B72CC7417D00DD8363 /* WeatherView.swift in Sources */,
Expand Down
89 changes: 78 additions & 11 deletions InfiniLink/Core/Components/Charts/HeartChartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,89 @@
//

import SwiftUI
import SwiftUICharts
import Charts

struct HeartChartDataPoint {
var id = UUID()
let date: Date
let min: Double
let max: Double
}

struct HeartChartView: View {
@ObservedObject var chartManager = ChartManager.shared

let heartPoints: [HeartDataPoint]
let showHeader: Bool

var body: some View {
let chartStyle = LineChartStyle(infoBoxPlacement: .floating /* TODO: fork and set to infoBox */, infoBoxBackgroundColour: Color(.secondarySystemBackground), baseline: .minimumValue, topLine: .maximumValue)
let lineStyle = LineStyle(lineColour: ColourStyle(colours: [Color.red.opacity(0.8), Color.red.opacity(0.5)], startPoint: .top, endPoint: .bottom), lineType: .line, ignoreZero: true)
let data = LineChartData(dataSets: LineDataSet(dataPoints: chartManager.convert(heartPoints), style: lineStyle), chartStyle: chartStyle)
func data() -> [HeartChartDataPoint] {
let points = [
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 1), min: 200, max: 239),
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 2), min: 101, max: 184),
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 3), min: 96, max: 193),
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 4), min: 104, max: 202),
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 5), min: 90, max: 95),
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 6), min: 96, max: 203),
HeartChartDataPoint(date: date(year: 2025, month: 7, day: 7), min: 98, max: 200)
]

// TODO: return data in proper format

FilledLineChart(chartData: data)
.floatingInfoBox(chartData: data)
.touchOverlay(chartData: data, unit: .suffix(of: "BPM"))
.yAxisLabels(chartData: data)
.animation(.none)
return points
}
var earliestDate: Date {
data().compactMap({ $0.date }).min() ?? Date()
}
var latestDate: Date {
data().compactMap({ $0.date }).max() ?? Date()
}

var header: some View {
VStack(alignment: .leading) {
Text(data().count > 1 ? "Range" : " ")
Text({
let max = Int(data().compactMap({ $0.max }).max() ?? 0)
let min = Int(data().compactMap({ $0.min }).min() ?? 0)

if max == 0 || min == 0 {
return "0 "
} else {
return "\(min)-\(max) "
}
}())
.font(.system(.title, design: .rounded))
.foregroundColor(.primary)
+ Text("BPM")
Text("\(earliestDate.formatted())-\(latestDate.formatted())")
}
.fontWeight(.semibold)
}

init(showHeader: Bool = true) {
self.showHeader = showHeader
}

var body: some View {
Section(header: showHeader ? AnyView(header) : AnyView(Text("Heart Rate"))) {
Chart(data(), id: \.id) { point in
Plot {
BarMark(
x: .value("Day", point.date, unit: .day),
yStart: .value("BPM Min", point.min),
yEnd: .value("BPM Max", point.max),
width: .fixed(8)
)
.clipShape(Capsule())
.foregroundStyle(Color.red)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { _ in
AxisTick()
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
}
.frame(height: 300)
}
}
}
67 changes: 62 additions & 5 deletions InfiniLink/Core/Components/Charts/StepChartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,70 @@
//

import SwiftUI
import Charts

struct StepChartDataPoint {
var id = UUID()
let date: Date
let steps: Int
}

struct StepChartView: View {
func data() -> [StepChartDataPoint] {
let points = [
StepChartDataPoint(date: date(year: 2025, month: 7, day: 1), steps: 239),
StepChartDataPoint(date: date(year: 2025, month: 6, day: 2), steps: 184),
StepChartDataPoint(date: date(year: 2025, month: 5, day: 3), steps: 7655),
StepChartDataPoint(date: date(year: 2025, month: 4, day: 4), steps: 202),
StepChartDataPoint(date: date(year: 2025, month: 3, day: 5), steps: 3402),
StepChartDataPoint(date: date(year: 2025, month: 2, day: 6), steps: 1890),
StepChartDataPoint(date: date(year: 2025, month: 1, day: 3), steps: 9002),
StepChartDataPoint(date: date(year: 2025, month: 1, day: 7), steps: 788)
]

// TODO: return data in proper format

return points
}

var header: some View {
VStack(alignment: .leading) {
Text(data().count > 1 ? "Range" : " ")
Text({
let max = Int(data().compactMap({ $0.steps }).max() ?? 0)
let min = Int(data().compactMap({ $0.steps }).min() ?? 0)

if max == 0 || min == 0 {
return "0 "
} else {
return "\(min)-\(max) "
}
}())
.font(.system(.title, design: .rounded))
.foregroundColor(.primary)
+ Text("BPM")
Text("\(earliestDate.formatted())-\(latestDate.formatted())")
}
.fontWeight(.semibold)
}
var earliestDate: Date {
data().compactMap({ $0.date }).min() ?? Date()
}
var latestDate: Date {
data().compactMap({ $0.date }).max() ?? Date()
}

var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
Chart(data(), id: \.date) {
BarMark(
x: .value("Date", $0.date),
y: .value("Steps", $0.steps),
width: .automatic
)
.accessibilityLabel($0.date.formatted(date: .complete, time: .omitted))
.accessibilityValue("\($0.steps) steps")
.foregroundStyle(.blue)
}
.frame(height: 300)
}
}

#Preview {
StepChartView()
}
3 changes: 1 addition & 2 deletions InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ struct ExerciseDetailView: View {
}
if heartPoints.count > 1 {
Section("Heart Rate") {
HeartChartView(heartPoints: heartPoints)
.frame(height: geo.size.width / 1.6)
HeartChartView(showHeader: false)
}
.listRowBackground(Color.clear)
}
Expand Down
24 changes: 8 additions & 16 deletions InfiniLink/Core/HeartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
//

import SwiftUI
import Accelerate
import CoreData
import SwiftUICharts

struct HeartView: View {
@ObservedObject var bleManager = BLEManager.shared
Expand Down Expand Up @@ -41,15 +39,15 @@ struct HeartView: View {
return NSLocalizedString("Now", comment: "")
}
func timestamp(for heartPoint: HeartDataPoint?) -> String? {
guard let timeInterval = heartPoint?.timestamp?.timeIntervalSinceNow else { return "" }
guard let timeInterval = heartPoint?.timestamp?.timeIntervalSinceNow else { return "No data" }

return units(for: Int(abs(timeInterval)))
}

var body: some View {
GeometryReader { geo in
ScrollView {
VStack(spacing: 20) {
List {
Group {
Section {
DetailHeaderView(Header(title: String(format: "%.0f", heartPointValues.last ?? 0), subtitle: timestamp(for: chartManager.heartPoints().last), units: "BPM", icon: "heart.fill", accent: .red), width: geo.size.width, animate: (heartDataPoints.last?.timestamp?.timeIntervalSinceNow ?? 60) < 60) {
HStack {
Expand All @@ -59,18 +57,15 @@ struct HeartView: View {
)
DetailHeaderSubItemView(
title: "Avg",
value: {
let meanValue = heartPointValues.isEmpty ? 0 : vDSP.mean(heartPointValues)
return heartRate(for: Double(meanValue))
}()
)
value: heartRate(for: Double(heartPointValues.compactMap({ Int($0) }).reduce(0, +) / heartPointValues.count)))
DetailHeaderSubItemView(
title: "Max",
value: heartRate(for: heartPointValues.max() ?? 0)
)
}
}
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section {
Picker("Range", selection: $dataSelection) {
ForEach(0...3, id: \.self) { index in
Expand All @@ -88,13 +83,10 @@ struct HeartView: View {
}
.pickerStyle(.segmented)
}
Section {
HeartChartView(heartPoints: chartManager.heartPoints())
.frame(height: geo.size.width / 1.8)
.padding(.vertical)
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
HeartChartView()
}
.padding()
.listRowBackground(Color.clear)
}
}
.navigationTitle("Heart Rate")
Expand Down
Loading

0 comments on commit aff10bb

Please sign in to comment.