Skip to content

Commit aff10bb

Browse files
committed
charts: begin implementation of Apple Charts
1 parent f20e048 commit aff10bb

File tree

12 files changed

+237
-122
lines changed

12 files changed

+237
-122
lines changed

.DS_Store

0 Bytes
Binary file not shown.

InfiniLink.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
E09696DF2D2EC7EC00CCCBF8 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696DE2D2EC7EC00CCCBF8 /* String+Extension.swift */; };
6666
E09696E32D2F807700CCCBF8 /* HeartChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696E22D2F807700CCCBF8 /* HeartChartView.swift */; };
6767
E09696E52D318AFB00CCCBF8 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696E42D318AFB00CCCBF8 /* Data+Extension.swift */; };
68+
E09696E72D323A6D00CCCBF8 /* StepChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09696E62D323A6D00CCCBF8 /* StepChartView.swift */; };
6869
E0A7C06B2CB0DE4C0042A12D /* Weather.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A7C06A2CB0DE4C0042A12D /* Weather.swift */; };
6970
E0A7C0732CB0ECCE0042A12D /* NotificationsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A7C0722CB0ECCE0042A12D /* NotificationsSettingsView.swift */; };
7071
E0A7C0762CB17E520042A12D /* MusicSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A7C0752CB17E520042A12D /* MusicSettingsView.swift */; };
@@ -190,6 +191,7 @@
190191
E09696DE2D2EC7EC00CCCBF8 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = "<group>"; };
191192
E09696E22D2F807700CCCBF8 /* HeartChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartChartView.swift; sourceTree = "<group>"; };
192193
E09696E42D318AFB00CCCBF8 /* Data+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extension.swift"; sourceTree = "<group>"; };
194+
E09696E62D323A6D00CCCBF8 /* StepChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepChartView.swift; sourceTree = "<group>"; };
193195
E0A7C06A2CB0DE4C0042A12D /* Weather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weather.swift; sourceTree = "<group>"; };
194196
E0A7C0722CB0ECCE0042A12D /* NotificationsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsSettingsView.swift; sourceTree = "<group>"; };
195197
E0A7C0752CB17E520042A12D /* MusicSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicSettingsView.swift; sourceTree = "<group>"; };
@@ -433,6 +435,7 @@
433435
isa = PBXGroup;
434436
children = (
435437
E09696E22D2F807700CCCBF8 /* HeartChartView.swift */,
438+
E09696E62D323A6D00CCCBF8 /* StepChartView.swift */,
436439
);
437440
path = Charts;
438441
sourceTree = "<group>";
@@ -795,6 +798,7 @@
795798
E0A7C0922CB1EE8F0042A12D /* DFUUpdater.swift in Sources */,
796799
E0A7C0932CB1EE8F0042A12D /* DownloadManager.swift in Sources */,
797800
E08C4A492CC4048900013D15 /* SleepData.swift in Sources */,
801+
E09696E72D323A6D00CCCBF8 /* StepChartView.swift in Sources */,
798802
E05999BC2CB336AE00D64E0B /* HealthKitManager.swift in Sources */,
799803
E0599A022CB4C38700D64E0B /* FilesystemView.swift in Sources */,
800804
E03C30B72CC7417D00DD8363 /* WeatherView.swift in Sources */,

InfiniLink/Core/Components/Charts/HeartChartView.swift

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,89 @@
66
//
77

88
import SwiftUI
9-
import SwiftUICharts
9+
import Charts
10+
11+
struct HeartChartDataPoint {
12+
var id = UUID()
13+
let date: Date
14+
let min: Double
15+
let max: Double
16+
}
1017

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

14-
let heartPoints: [HeartDataPoint]
21+
let showHeader: Bool
1522

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

21-
FilledLineChart(chartData: data)
22-
.floatingInfoBox(chartData: data)
23-
.touchOverlay(chartData: data, unit: .suffix(of: "BPM"))
24-
.yAxisLabels(chartData: data)
25-
.animation(.none)
36+
return points
37+
}
38+
var earliestDate: Date {
39+
data().compactMap({ $0.date }).min() ?? Date()
40+
}
41+
var latestDate: Date {
42+
data().compactMap({ $0.date }).max() ?? Date()
43+
}
44+
45+
var header: some View {
46+
VStack(alignment: .leading) {
47+
Text(data().count > 1 ? "Range" : " ")
48+
Text({
49+
let max = Int(data().compactMap({ $0.max }).max() ?? 0)
50+
let min = Int(data().compactMap({ $0.min }).min() ?? 0)
51+
52+
if max == 0 || min == 0 {
53+
return "0 "
54+
} else {
55+
return "\(min)-\(max) "
56+
}
57+
}())
58+
.font(.system(.title, design: .rounded))
59+
.foregroundColor(.primary)
60+
+ Text("BPM")
61+
Text("\(earliestDate.formatted())-\(latestDate.formatted())")
62+
}
63+
.fontWeight(.semibold)
64+
}
65+
66+
init(showHeader: Bool = true) {
67+
self.showHeader = showHeader
68+
}
69+
70+
var body: some View {
71+
Section(header: showHeader ? AnyView(header) : AnyView(Text("Heart Rate"))) {
72+
Chart(data(), id: \.id) { point in
73+
Plot {
74+
BarMark(
75+
x: .value("Day", point.date, unit: .day),
76+
yStart: .value("BPM Min", point.min),
77+
yEnd: .value("BPM Max", point.max),
78+
width: .fixed(8)
79+
)
80+
.clipShape(Capsule())
81+
.foregroundStyle(Color.red)
82+
}
83+
}
84+
.chartXAxis {
85+
AxisMarks(values: .stride(by: .day)) { _ in
86+
AxisTick()
87+
AxisGridLine()
88+
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
89+
}
90+
}
91+
.frame(height: 300)
92+
}
2693
}
2794
}

InfiniLink/Core/Components/Charts/StepChartView.swift

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,70 @@
66
//
77

88
import SwiftUI
9+
import Charts
10+
11+
struct StepChartDataPoint {
12+
var id = UUID()
13+
let date: Date
14+
let steps: Int
15+
}
916

1017
struct StepChartView: View {
18+
func data() -> [StepChartDataPoint] {
19+
let points = [
20+
StepChartDataPoint(date: date(year: 2025, month: 7, day: 1), steps: 239),
21+
StepChartDataPoint(date: date(year: 2025, month: 6, day: 2), steps: 184),
22+
StepChartDataPoint(date: date(year: 2025, month: 5, day: 3), steps: 7655),
23+
StepChartDataPoint(date: date(year: 2025, month: 4, day: 4), steps: 202),
24+
StepChartDataPoint(date: date(year: 2025, month: 3, day: 5), steps: 3402),
25+
StepChartDataPoint(date: date(year: 2025, month: 2, day: 6), steps: 1890),
26+
StepChartDataPoint(date: date(year: 2025, month: 1, day: 3), steps: 9002),
27+
StepChartDataPoint(date: date(year: 2025, month: 1, day: 7), steps: 788)
28+
]
29+
30+
// TODO: return data in proper format
31+
32+
return points
33+
}
34+
35+
var header: some View {
36+
VStack(alignment: .leading) {
37+
Text(data().count > 1 ? "Range" : " ")
38+
Text({
39+
let max = Int(data().compactMap({ $0.steps }).max() ?? 0)
40+
let min = Int(data().compactMap({ $0.steps }).min() ?? 0)
41+
42+
if max == 0 || min == 0 {
43+
return "0 "
44+
} else {
45+
return "\(min)-\(max) "
46+
}
47+
}())
48+
.font(.system(.title, design: .rounded))
49+
.foregroundColor(.primary)
50+
+ Text("BPM")
51+
Text("\(earliestDate.formatted())-\(latestDate.formatted())")
52+
}
53+
.fontWeight(.semibold)
54+
}
55+
var earliestDate: Date {
56+
data().compactMap({ $0.date }).min() ?? Date()
57+
}
58+
var latestDate: Date {
59+
data().compactMap({ $0.date }).max() ?? Date()
60+
}
61+
1162
var body: some View {
12-
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
63+
Chart(data(), id: \.date) {
64+
BarMark(
65+
x: .value("Date", $0.date),
66+
y: .value("Steps", $0.steps),
67+
width: .automatic
68+
)
69+
.accessibilityLabel($0.date.formatted(date: .complete, time: .omitted))
70+
.accessibilityValue("\($0.steps) steps")
71+
.foregroundStyle(.blue)
72+
}
73+
.frame(height: 300)
1374
}
1475
}
15-
16-
#Preview {
17-
StepChartView()
18-
}

InfiniLink/Core/Exercise/Views/ExerciseDetailView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ struct ExerciseDetailView: View {
103103
}
104104
if heartPoints.count > 1 {
105105
Section("Heart Rate") {
106-
HeartChartView(heartPoints: heartPoints)
107-
.frame(height: geo.size.width / 1.6)
106+
HeartChartView(showHeader: false)
108107
}
109108
.listRowBackground(Color.clear)
110109
}

InfiniLink/Core/HeartView.swift

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
//
77

88
import SwiftUI
9-
import Accelerate
109
import CoreData
11-
import SwiftUICharts
1210

1311
struct HeartView: View {
1412
@ObservedObject var bleManager = BLEManager.shared
@@ -41,15 +39,15 @@ struct HeartView: View {
4139
return NSLocalizedString("Now", comment: "")
4240
}
4341
func timestamp(for heartPoint: HeartDataPoint?) -> String? {
44-
guard let timeInterval = heartPoint?.timestamp?.timeIntervalSinceNow else { return "" }
42+
guard let timeInterval = heartPoint?.timestamp?.timeIntervalSinceNow else { return "No data" }
4543

4644
return units(for: Int(abs(timeInterval)))
4745
}
4846

4947
var body: some View {
5048
GeometryReader { geo in
51-
ScrollView {
52-
VStack(spacing: 20) {
49+
List {
50+
Group {
5351
Section {
5452
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) {
5553
HStack {
@@ -59,18 +57,15 @@ struct HeartView: View {
5957
)
6058
DetailHeaderSubItemView(
6159
title: "Avg",
62-
value: {
63-
let meanValue = heartPointValues.isEmpty ? 0 : vDSP.mean(heartPointValues)
64-
return heartRate(for: Double(meanValue))
65-
}()
66-
)
60+
value: heartRate(for: Double(heartPointValues.compactMap({ Int($0) }).reduce(0, +) / heartPointValues.count)))
6761
DetailHeaderSubItemView(
6862
title: "Max",
6963
value: heartRate(for: heartPointValues.max() ?? 0)
7064
)
7165
}
7266
}
7367
}
68+
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
7469
Section {
7570
Picker("Range", selection: $dataSelection) {
7671
ForEach(0...3, id: \.self) { index in
@@ -88,13 +83,10 @@ struct HeartView: View {
8883
}
8984
.pickerStyle(.segmented)
9085
}
91-
Section {
92-
HeartChartView(heartPoints: chartManager.heartPoints())
93-
.frame(height: geo.size.width / 1.8)
94-
.padding(.vertical)
95-
}
86+
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
87+
HeartChartView()
9688
}
97-
.padding()
89+
.listRowBackground(Color.clear)
9890
}
9991
}
10092
.navigationTitle("Heart Rate")

0 commit comments

Comments
 (0)