Hi, I want to recreate a chart from Apple Health and I have code like this. When I scroll - especially the week and month charts, there are performance issues. If I remove .chartScrollPosition(x: $scrollChartPosition), it runs smoothly, but I need to know which part of the chart is currently displayed. Can you help me?
import Charts
import SwiftUI
struct MacroChartView: View {
var selectedRange: ChartRange
var binnedPoints: [MacroBinPoint]
@State private var scrollChartPosition: Date = .now
var body: some View {
VStack {
Text("\(selectedRange.rangeLabel(for: scrollChartPosition))")
Chart(binnedPoints) { point in
BarMark(
x: .value("Date", point.date, unit: selectedRange.binComponent),
y: .value("Calories", point.calories)
)
}
.frame(height: 324)
.chartXVisibleDomain(length: selectedRange.visibleDomainLength())
.chartScrollableAxes(.horizontal)
.chartScrollPosition(x: $scrollChartPosition)
.chartScrollTargetBehavior(.valueAligned(matching: selectedRange.scrollAlignmentComponents))
.chartXAxis {
switch selectedRange {
case .week:
AxisMarks(values: .stride(by: .day)) { date in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
case .month:
AxisMarks(values: .stride(by: .weekOfYear)) { date in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.day())
}
case .halfYear:
AxisMarks(values: .stride(by: .month)) { date in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
case .year:
AxisMarks(values: .stride(by: .month)) { date in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.abbreviated))
}
}
}
}
}
}
enum MeasurementHistoryMode {
case macros
case comparisons
}
enum MacroKindToDisplay {
case protein, fat, carbs
}
enum MacrosDisplayMode: Equatable {
case all
case single(MacroKindToDisplay)
}
enum ChartRange: String, CaseIterable {
case week = "T"
case month = "M"
case halfYear = "6M"
case year = "R"
var binComponent: Calendar.Component {
switch self {
case .week, .month: return .day
case .halfYear: return .weekOfYear
case .year: return .month
}
}
var scrollAlignmentComponents: DateComponents {
switch self {
case .week:
return DateComponents(hour: 0, minute: 0, second: 0)
case .month:
return DateComponents(hour: 0)
case .halfYear:
return DateComponents(weekday: 1)
case .year:
return DateComponents(day: 1)
}
}
func visibleDomainLength() -> Int {
switch self {
case .week:
return 7 * 24 * 60 * 60
case .month:
return 31 * 24 * 60 * 60
case .halfYear:
return 6 * 31 * 24 * 60 * 60
case .year:
return 12 * 31 * 24 * 60 * 60
}
}
func start(for date: Date) -> Date {
let cal = Calendar.current
switch self {
case .week, .month:
return cal.startOfDay(for: date)
case .halfYear:
return cal.dateInterval(of: .weekOfYear, for: date)?.start ?? cal.startOfDay(for: date)
case .year:
return cal.dateInterval(of: .month, for: date)?.start ?? cal.startOfDay(for: date)
}
}
func rangeLabel(for start: Date) -> String {
let end = start.addingTimeInterval(TimeInterval(visibleDomainLength()))
let f = DateFormatter()
f.dateFormat = Calendar.current.isDate(start, inSameDayAs: end) ? "MMM d" : "MMM d"
return Calendar.current.isDate(start, inSameDayAs: end) ? f.string(from: start) : "\(f.string(from: start)) – \(f.string(from: end))"
}
}
struct MacrosPoint: Identifiable {
var id: Date { date }
let date: Date
let calories: Double
let proteinInGrams: Double
let carbsInGrams: Double
let fatInGrams: Double
}
struct MacroBinPoint: Identifiable {
var id: Date { date }
let date: Date
let calories: Double
let proteinKcal: Double
let carbsKcal: Double
let fatKcal: Double
}
func bin(points: [MacrosPoint], for period: ChartRange) -> [MacroBinPoint] {
let grouped = Dictionary(grouping: points) { point in
period.start(for: point.date)
}
let bins = grouped.map { (start, items) -> MacroBinPoint in
var calories = items.reduce(0) { $0 + $1.calories }
var proteinKcal = items.reduce(0) { $0 + $1.proteinInGrams * 4 }
var carbsKcal = items.reduce(0) { $0 + $1.carbsInGrams * 4 }
var fatKcal = items.reduce(0) { $0 + $1.fatInGrams * 9 }
calories /= Double(items.count)
proteinKcal /= Double(items.count)
carbsKcal /= Double(items.count)
fatKcal /= Double(items.count)
return MacroBinPoint(date: start, calories: calories, proteinKcal: proteinKcal, carbsKcal: carbsKcal, fatKcal: fatKcal)
}
.sorted { $0.date < $1.date }
return bins
}
struct ExampleData {
static let macrosPoints: [MacrosPoint] = [
MacrosPoint(date: Date(timeIntervalSince1970: 1687949774), calories: 1895, proteinInGrams: 115, carbsInGrams: 192, fatInGrams: 72),...
]
0
0
47