// // 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 } }