gitfetch_rs/display/
graph.rs1use super::colors::get_ansi_color;
2use crate::config::ColorConfig;
3use chrono::{Datelike, NaiveDate};
4use serde_json::Value;
5
6pub struct ContributionGraph {
7 weeks: Vec<Week>,
8}
9
10#[derive(Clone)]
11pub struct Week {
12 pub contribution_days: Vec<Day>,
13}
14
15#[derive(Clone)]
16pub struct Day {
17 pub contribution_count: u32,
18 #[allow(dead_code)]
19 pub date: String,
20}
21
22impl ContributionGraph {
23 pub fn from_json(data: &Value) -> Self {
24 let weeks = data
25 .as_array()
26 .map(|arr| {
27 arr
28 .iter()
29 .map(|week| Week {
30 contribution_days: week["contributionDays"]
31 .as_array()
32 .map(|days| {
33 days
34 .iter()
35 .map(|day| Day {
36 contribution_count: day["contributionCount"].as_u64().unwrap_or(0) as u32,
37 date: day["date"].as_str().unwrap_or("").to_string(),
38 })
39 .collect()
40 })
41 .unwrap_or_default(),
42 })
43 .collect()
44 })
45 .unwrap_or_default();
46
47 Self { weeks }
48 }
49
50 pub fn from_grid(grid: Vec<Vec<u8>>) -> Self {
51 if grid.is_empty() || grid[0].is_empty() {
53 return Self { weeks: Vec::new() };
54 }
55
56 let num_columns = grid[0].len();
57 let mut weeks = Vec::new();
58
59 for col_idx in 0..num_columns {
60 let mut week_days = Vec::new();
61 for row_idx in 0..7 {
62 let intensity = if row_idx < grid.len() && col_idx < grid[row_idx].len() {
63 grid[row_idx][col_idx]
64 } else {
65 0
66 };
67
68 week_days.push(Day {
69 contribution_count: intensity as u32,
70 date: format!("2023-01-{:02}", col_idx + 1),
71 });
72 }
73
74 weeks.push(Week {
75 contribution_days: week_days,
76 });
77 }
78
79 Self { weeks }
80 }
81
82 pub fn render(
83 &self,
84 width: Option<usize>,
85 height: Option<usize>,
86 custom_box: &str,
87 colors: &ColorConfig,
88 show_date: bool,
89 spaced: bool,
90 ) -> Vec<String> {
91 let mut lines = Vec::new();
92
93 let num_weeks = width.unwrap_or(52);
95 let recent_weeks = self.get_recent_weeks(num_weeks);
96
97 if show_date {
98 let month_line = self.build_month_line(&recent_weeks);
99 lines.push(month_line);
100 }
101
102 let num_days = height.unwrap_or(7).min(7);
104
105 for day_idx in 0..num_days {
106 let mut row = String::from(" ");
107 for week in &recent_weeks {
108 if let Some(day) = week.contribution_days.get(day_idx) {
109 let block = if spaced {
110 self.get_contribution_block_spaced(day.contribution_count, custom_box, colors)
111 } else {
112 self.get_contribution_block(day.contribution_count, colors)
113 };
114 row.push_str(&block);
115 }
116 }
117 row.push_str("\x1b[0m");
118 lines.push(row);
119 }
120
121 lines
122 }
123
124 fn get_contribution_block(&self, count: u32, colors: &ColorConfig) -> String {
125 let color = match count {
126 0 => &colors.level_0,
127 1..=2 => &colors.level_1,
128 3..=6 => &colors.level_2,
129 7..=12 => &colors.level_3,
130 _ => &colors.level_4,
131 };
132
133 let bg_color = get_ansi_color(color).unwrap_or_default();
135 let bg_ansi = if !bg_color.is_empty() && bg_color.starts_with("\x1b[38;2;") {
136 bg_color.replace("\x1b[38;2;", "\x1b[48;2;")
138 } else {
139 bg_color
140 };
141 format!("{} \x1b[0m", bg_ansi)
142 }
143
144 fn get_contribution_block_spaced(
145 &self,
146 count: u32,
147 custom_box: &str,
148 colors: &ColorConfig,
149 ) -> String {
150 let color = match count {
151 0 => &colors.level_0,
152 1..=2 => &colors.level_1,
153 3..=6 => &colors.level_2,
154 7..=12 => &colors.level_3,
155 _ => &colors.level_4,
156 };
157
158 let ansi_color = get_ansi_color(color).unwrap_or_default();
161 format!("{}{} ", ansi_color, custom_box)
162 }
163
164 fn get_recent_weeks(&self, limit: usize) -> Vec<Week> {
165 if self.weeks.len() <= limit {
166 self.weeks.clone()
167 } else {
168 self.weeks[self.weeks.len() - limit..].to_vec()
169 }
170 }
171
172 fn build_month_line(&self, weeks: &[Week]) -> String {
173 if weeks.is_empty() {
174 return String::new();
175 }
176
177 let months = [
178 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
179 ];
180
181 let mut month_line = String::new();
182
183 for (idx, week) in weeks.iter().enumerate() {
184 if week.contribution_days.is_empty() {
185 continue;
186 }
187
188 let first_day = &week.contribution_days[0];
189 if let Ok(date) = NaiveDate::parse_from_str(&first_day.date, "%Y-%m-%d") {
190 let current_month = date.month() as usize;
191
192 if idx == 0 {
193 month_line.push_str(months[current_month - 1]);
194 } else {
195 if let Some(prev_week) = weeks.get(idx - 1) {
196 if !prev_week.contribution_days.is_empty() {
197 if let Ok(prev_date) =
198 NaiveDate::parse_from_str(&prev_week.contribution_days[0].date, "%Y-%m-%d")
199 {
200 let prev_month = prev_date.month() as usize;
201 if current_month != prev_month {
202 let target_width = (idx + 1) * 2;
203 let current_width = month_line.len();
204 let month_name = months[current_month - 1];
205 if target_width > current_width + month_name.len() {
207 let needed_space = target_width - current_width - month_name.len();
208 month_line.push_str(&" ".repeat(needed_space));
209 month_line.push_str(month_name);
210 } else {
211 month_line.push(' ');
212 month_line.push_str(month_name);
213 }
214 }
215 }
216 }
217 }
218 }
219 }
220 }
221
222 format!(" {}", month_line)
223 }
224
225 #[allow(dead_code)]
226 pub fn calculate_total_contributions(&self) -> u32 {
227 self
228 .weeks
229 .iter()
230 .flat_map(|w| &w.contribution_days)
231 .map(|d| d.contribution_count)
232 .sum()
233 }
234
235 #[allow(dead_code)]
236 pub fn calculate_streaks(&self) -> (u32, u32) {
237 let mut all_contributions: Vec<u32> = self
238 .weeks
239 .iter()
240 .flat_map(|w| &w.contribution_days)
241 .map(|d| d.contribution_count)
242 .collect();
243
244 all_contributions.reverse();
245
246 let mut current_streak = 0;
247 for &count in &all_contributions {
248 if count > 0 {
249 current_streak += 1;
250 } else {
251 break;
252 }
253 }
254
255 let mut max_streak = 0;
256 let mut temp_streak = 0;
257 for &count in &all_contributions {
258 if count > 0 {
259 temp_streak += 1;
260 max_streak = max_streak.max(temp_streak);
261 } else {
262 temp_streak = 0;
263 }
264 }
265
266 (current_streak, max_streak)
267 }
268}