Files
workoutsplus/WorkoutsPlus/Features/Home/ActivityLog.swift
2024-10-21 15:03:47 +02:00

130 lines
3.5 KiB
Swift

//
// ActivityLog.swift
// WorkoutsPlus
//
// Created by Felix Förtsch on 09.09.24.
//
// https://www.artemnovichkov.com/blog/github-contribution-graph-swift-charts
// https://github.com/artemnovichkov/awesome-swift-charts
import SwiftUI
import Charts
struct ActivityLog: View {
@State var activities: [Activity] = Activity.generate()
var body: some View {
Chart(activities) { contribution in
RectangleMark(
xStart: .value("Start week", contribution.date, unit: .weekOfYear),
xEnd: .value("End week", contribution.date, unit: .weekOfYear),
yStart: .value("Start weekday", weekday(for: contribution.date)),
yEnd: .value("End weekday", weekday(for: contribution.date) + 1)
)
.clipShape(RoundedRectangle(cornerRadius: 4).inset(by: 2))
.foregroundStyle(by: .value("Count", contribution.count))
}
.chartPlotStyle { content in
content
.aspectRatio(aspectRatio, contentMode: .fit)
}
.chartForegroundStyleScale(range: Gradient(colors: colors))
.chartXAxis {
AxisMarks(position: .top, values: .stride(by: .month)) {
AxisValueLabel(format: .dateTime.month())
.foregroundStyle(Color(.label))
}
}
.chartYAxis {
AxisMarks(position: .leading, values: [1, 3, 5]) { value in
if let value = value.as(Int.self) {
AxisValueLabel {
// Symbols from Calendar.current starting with Monday
// Text(shortWeekdaySymbols[value - 1])
}
.foregroundStyle(Color(.label))
}
}
}
.chartYScale(domain: .automatic(includesZero: false, reversed: true))
.chartLegend {
HStack(spacing: 4) {
Text("Less")
ForEach(legendColors, id: \.self) { color in
color
.frame(width: 10, height: 10)
.cornerRadius(2)
}
Text("More")
}
.padding(4)
.foregroundStyle(Color(.label))
.font(.caption2)
}
}
private func weekday(for date: Date) -> Int {
let weekday = Calendar.current.component(.weekday, from: date)
let adjustedWeekday = (weekday == 1) ? 7 : (weekday - 1)
return adjustedWeekday
}
private var aspectRatio: Double {
if activities.isEmpty {
return 1
}
let firstDate = activities.first!.date
let lastDate = activities.last!.date
let firstWeek = Calendar.current.component(.weekOfYear, from: firstDate)
let lastWeek = Calendar.current.component(.weekOfYear, from: lastDate)
return Double(lastWeek - firstWeek + 1) / 7
}
private var colors: [Color] {
(0...10).map { index in
if index == 0 {
return Color(.systemGray5)
}
return Color(.systemGreen).opacity(Double(index) / 10)
}
}
private var legendColors: [Color] {
Array(stride(from: 0, to: colors.count, by: 2).map { colors[$0] })
}
}
#Preview {
ActivityLog()
}
struct Activity: Identifiable {
let date: Date
let count: Int
var id: Date {
date
}
}
extension Activity {
static func generate() -> [Activity] {
var contributions: [Activity] = []
let toDate = Date.now
let fromDate = Calendar.current.date(byAdding: .day, value: -60, to: toDate)!
var currentDate = fromDate
while currentDate <= toDate {
let contribution = Activity(date: currentDate, count: .random(in: 0...10))
contributions.append(contribution)
currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)!
}
return contributions
}
}