130 lines
3.5 KiB
Swift
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
|
|
}
|
|
}
|