use std::{collections::BTreeMap, fmt::Display}; use chrono::{Datelike, Duration, NaiveDate}; use clap::ValueEnum; use itertools::Itertools; use crate::{get_char, get_color, get_color_map, Commit, DAYS, RESET}; pub struct Heatmap { data: [Vec; 7], since: NaiveDate, until: NaiveDate, commits: Vec, months: Vec<(usize, String)>, highest_count: i32, repos: usize, } impl Heatmap { pub fn new( since: NaiveDate, until: NaiveDate, repos: usize, commits: Vec, split_months: bool, ) -> Self { let mut heatmap = Self { data: [vec![], vec![], vec![], vec![], vec![], vec![], vec![]], since, until, commits, months: vec![], highest_count: 0, repos, }; let mut grouped_commits = BTreeMap::new(); for commit in &heatmap.commits { let commit_day = commit.time.date_naive(); let record = grouped_commits.entry(commit_day).or_insert(0); *record += 1; } let mut current_day = since; let mut day_of_week = current_day.weekday().num_days_from_monday() % 7; if day_of_week != 0 { for i in 0..day_of_week { heatmap.data[i as usize].push(-1); } } while current_day <= until { if split_months { let last_day_of_month = heatmap.last_day_of_month(current_day.year_ce().1 as i32, current_day.month()); // If current day is the last of the month we add 2 weeks worth of empty space so // months are more visible if last_day_of_month == current_day { for i in 0..14 { heatmap.data[(i as usize) % 7].push(-1); } } } let month_name = current_day.format("%b").to_string(); if current_day == since { heatmap.months.push((0, month_name)); } else if current_day.day0() == 0 { heatmap .months .push((heatmap.data[day_of_week as usize].len(), month_name)); } let value = grouped_commits.get(¤t_day); match value { Some(val) => { heatmap.data[day_of_week as usize].push(*val); if *val > heatmap.highest_count { heatmap.highest_count = *val; } } None => heatmap.data[day_of_week as usize].push(0), } // println!("{} {value:?}", current_day); current_day += Duration::days(1); day_of_week = current_day.weekday().num_days_from_monday() % 7; } heatmap } fn last_day_of_month(&self, year: i32, month: u32) -> NaiveDate { NaiveDate::from_ymd_opt(year, month + 1, 1) .unwrap_or_else(|| NaiveDate::from_ymd(year + 1, 1, 1)) .pred_opt() .unwrap_or_default() } fn months_row(&self) -> String { let mut row = " ".to_string(); let mut last_index = 0; for (index, month) in &self.months { let range_size = (index * 2).saturating_sub(last_index * 2).saturating_sub(3); for _i in 0..range_size { row.push(' '); } last_index = *index; row.push_str(month); } row } } impl Display for Heatmap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let start_date = self.since.format("%Y-%b-%d").to_string(); let end_date = self.until.format("%Y-%b-%d").to_string(); let commits = self.commits.len().to_string(); let authors = self.commits.iter().unique_by(|c| &c.author.name).count(); write!(f, "{} - {}\n", start_date, end_date).unwrap(); write!(f, "{} repo(s)\n", self.repos).unwrap(); write!(f, "{} author(s)\n", authors).unwrap(); write!(f, "{} commit(s)\n\n", commits).unwrap(); write!(f, "{}\n", self.months_row()).unwrap(); for (day, row) in DAYS.iter().zip(&self.data) { write!(f, "{day} ").unwrap(); for val in row { match val { x if *x >= 0 => { let color = &get_color_map()[get_color(*val, self.highest_count)]; write!(f, "{color}{}{RESET} ", get_char()).unwrap(); } x if *x < 0 => { write!(f, "{RESET} ").unwrap(); } _ => {} } } write!(f, "\n").unwrap(); } write!(f, "\nLess ").unwrap(); for color in get_color_map() { write!(f, "{color}{}{RESET} ", get_char()).unwrap(); } write!(f, " More\n").unwrap(); Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum HeatmapColors { Green, Red, } #[derive(Clone, Debug, PartialEq, Eq, ValueEnum)] pub enum ColorLogic { ByAmount, ByWeight, }