Advertisement
SplittyDev

SwiftUI Activity Heatmap

Dec 19th, 2023
894
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Swift 4.75 KB | Source Code | 0 0
  1. //
  2. //  ActivityHeatMap.swift
  3. //
  4. //  Created by Marco Quinten (https://github.com/SplittyDev)
  5. //  Created for FiveSheep OƜ (https://github.com/FiveSheepCo)
  6. //
  7. //  Permission is hereby granted to use this for whatever project you like, whether free or paid.
  8. //
  9.  
  10. import SwiftUI
  11. import Charts
  12.  
  13. /// An activity graph similar to GitHub's contribution graph.
  14. struct ActivityHeatMap: View {
  15.     let entries: [Entry] // TODO: Change this to your own type
  16.    
  17.     struct HeatmapEntry {
  18.         let date: Date
  19.         let active: Bool
  20.         let isToday: Bool
  21.         let isInFuture: Bool
  22.         let gridRow: Int
  23.         let gridCol: Int
  24.        
  25.         var color: Color {
  26.             if active {
  27.                 Color.green
  28.             } else if isToday {
  29.                 Color.blue
  30.             } else if isInFuture {
  31.                 Color.gray.opacity(0.25)
  32.             } else {
  33.                 Color.gray
  34.             }
  35.         }
  36.     }
  37.    
  38.     /// Heatmap entries for the current month.
  39.     var currentMonthEntries: [HeatmapEntry] {
  40.         let now = Date.now
  41.         let calendar = Calendar.current
  42.         var results = [HeatmapEntry]()
  43.        
  44.         // Determine the first and last day of the current month
  45.         let firstOfMonth = calendar.date(from: calendar.dateComponents(
  46.             [.calendar, .timeZone, .era, .year, .quarter, .month],
  47.             from: now
  48.         ))!
  49.         let lastOfMonth = calendar.date(
  50.             byAdding: DateComponents(month: 1, day: -1),
  51.             to: firstOfMonth
  52.         )!
  53.        
  54.         // Loop over the selected date range
  55.         var currentDate = firstOfMonth
  56.         while currentDate <= lastOfMonth {
  57.            
  58.             // TODO: Update this logic to use your own `Entry` type
  59.             let entry = entries.firstIndex { $0.timestamp.dateOnly == currentDate.dateOnly }
  60.            
  61.             // Determine properties of the given day
  62.             let firstDayOfMonthWeekday = calendar.component(.weekday, from: firstOfMonth) - calendar.firstWeekday
  63.             let adjustedFirstDayOfMonthWeekday = (firstDayOfMonthWeekday >= 0)
  64.                 ? firstDayOfMonthWeekday
  65.                 : (firstDayOfMonthWeekday + 7)
  66.             let daysSinceFirstOfMonth = calendar.dateComponents(
  67.                 [.day], from: firstOfMonth, to: currentDate
  68.             ).day!
  69.             let weekNumber = (daysSinceFirstOfMonth + adjustedFirstDayOfMonthWeekday) / 7
  70.             let dayOfWeek = calendar.component(.weekday, from: currentDate) - calendar.firstWeekday
  71.             let adjustedDayOfWeek = (dayOfWeek >= 0) ? dayOfWeek : (dayOfWeek + 7)
  72.            
  73.             // Create heatmap entry
  74.             let heatmapEntry = HeatmapEntry(
  75.                 date: currentDate,
  76.                 active: entry != nil,
  77.                 isToday: currentDate.dateOnly == Date.now.dateOnly,
  78.                 isInFuture: currentDate > Date.now,
  79.                 gridRow: weekNumber,
  80.                 gridCol: adjustedDayOfWeek
  81.             )
  82.             results.append(heatmapEntry)
  83.            
  84.             // Advance date by one day
  85.             currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
  86.         }
  87.        
  88.         return results
  89.     }
  90.    
  91.     var body: some View {
  92.         GeometryReader { geometry in
  93.             let calendar = Calendar.current
  94.            
  95.             // Calculate the height of a single point on the graph
  96.             let maxWeekdays = Double(currentMonthEntries.map { calendar.component(.weekday, from: $0.date) }.max() ?? 1)
  97.             let dotHeight = geometry.size.height / maxWeekdays
  98.            
  99.             HStack {
  100.                 Spacer()
  101.                
  102.                 Chart {
  103.                     ForEach(currentMonthEntries, id: \.date) { entry in
  104.                         RectangleMark(
  105.                             x: .value(String.empty, entry.gridRow),
  106.                             y: .value(String.empty, entry.gridCol),
  107.                             width: .fixed(dotHeight),
  108.                             height: .fixed(dotHeight)
  109.                         )
  110.                         .foregroundStyle(entry.color)
  111.                         .cornerRadius(2)
  112.                     }
  113.                 }
  114.                 .frame(width: dotHeight * 7, height: dotHeight * 7, alignment: .center)
  115.                 .chartXAxis(.hidden)
  116.                 .chartYAxis(.hidden)
  117.                
  118.                 Spacer()
  119.             }
  120.         }
  121.     }
  122. }
  123.  
  124. #Preview {
  125.     let entries = [] // TODO: Somehow create your entries here
  126.     return ActivityHeatMap(entries: entries)
  127.             .padding(16)
  128.             .frame(width: 250, height: 150)
  129.             .background(Color.secondarySystemBackground)
  130.             .clipShape(RoundedRectangle(cornerRadius: 8))
  131. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement