github_heatmap/heatmap/
mod.rs1mod contribution_week;
2mod contribution;
3
4pub use contribution_week::ContributionWeek;
5pub use contribution::Contribution;
6use scraper::{Selector, Html, ElementRef};
7use crate::{ColorValues, HeatmapError};
8
9const WEEK_SELECTOR: &str = "svg.js-calendar-graph-svg g g";
10const DAY_SELECTOR: &str = "rect.ContributionCalendar-day";
11const DAYS_IN_WEEK: usize = 7;
12
13#[derive(Debug, Eq, PartialEq)]
20pub struct Heatmap {
21 pub contribution_weeks: Vec<ContributionWeek>,
24}
25
26impl Heatmap {
27 pub fn from_document(document: &Html) -> Result<Self, HeatmapError> {
47 let contribution_week_selector = Selector::parse(WEEK_SELECTOR).unwrap();
48 let day_selector = Selector::parse(DAY_SELECTOR).unwrap();
49 let mut contribution_weeks = vec![];
50
51 for el in document.select(&contribution_week_selector) {
52 let week = Self::get_contribution_week(&el, &day_selector)?;
53 contribution_weeks.push(week);
54 }
55
56 match &contribution_weeks.is_empty() {
57 false => Ok(Heatmap { contribution_weeks }),
58 true => Err(HeatmapError::QueryElement {
59 alias: "heatmap".to_string(),
60 selector: WEEK_SELECTOR.to_string()
61 })
62 }
63 }
64
65 pub fn render(&self, color: &ColorValues) {
72 for day in 0..DAYS_IN_WEEK {
73 let week: String = self.contribution_weeks
74 .iter()
75 .map(|week| match &week.contributions[day] {
76 Some(day) => day.render(color),
77 None => String::from(" ")
78 })
79 .collect();
80
81 println!("{week}");
82 };
83 }
84
85 fn get_contribution_week(el: &ElementRef, selector: &Selector) -> Result<ContributionWeek, HeatmapError> {
86 let day_els: Vec<_> = el.select(selector).collect();
87
88 if day_els.is_empty() {
89 return Err(HeatmapError::QueryElement {
90 alias: "heatmap node".to_string(),
91 selector: DAY_SELECTOR.to_string()
92 });
93 }
94
95 ContributionWeek::from_days(&day_els)
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn constructs_heatmap() {
105 let fragment = Html::parse_fragment(r#"
106 <svg class="js-calendar-graph-svg">
107 <g>
108 <g>
109 <rect y='45' data-level='1' class="ContributionCalendar-day" />
110 <rect y='60' data-level='2' class="ContributionCalendar-day" />
111 <rect y='75' data-level='3' class="ContributionCalendar-day" />
112 <rect y='90' data-level='4' class="ContributionCalendar-day" />
113 </g>
114 <g>
115 <rect y='0' data-level='1' class="ContributionCalendar-day" />
116 <rect y='15' data-level='2' class="ContributionCalendar-day" />
117 <rect y='30' data-level='3' class="ContributionCalendar-day" />
118 <rect y='45' data-level='4' class="ContributionCalendar-day" />
119 <rect y='60' data-level='4' class="ContributionCalendar-day" />
120 <rect y='75' data-level='4' class="ContributionCalendar-day" />
121 <rect y='90' data-level='4' class="ContributionCalendar-day" />
122 </g>
123 </g>
124 </svg>
125 "#);
126
127 let heatmap = Heatmap::from_document(&fragment).unwrap();
128 let expected = Heatmap {
129 contribution_weeks: vec![
130 ContributionWeek {
131 contributions: vec![
132 None,
133 None,
134 None,
135 Some(Contribution { heat_level: 1 }),
136 Some(Contribution { heat_level: 2 }),
137 Some(Contribution { heat_level: 3 }),
138 Some(Contribution { heat_level: 4 }),
139 ]
140 },
141 ContributionWeek {
142 contributions: vec![
143 Some(Contribution { heat_level: 1 }),
144 Some(Contribution { heat_level: 2 }),
145 Some(Contribution { heat_level: 3 }),
146 Some(Contribution { heat_level: 4 }),
147 Some(Contribution { heat_level: 4 }),
148 Some(Contribution { heat_level: 4 }),
149 Some(Contribution { heat_level: 4 }),
150 ]
151 }
152 ]
153 };
154
155 assert_eq!(heatmap, expected)
156 }
157
158 #[test]
159 fn gets_contribution_week() {
160 let fragment = Html::parse_fragment(r#"
161 <g>
162 <rect y='0' data-level='1' class="ContributionCalendar-day" />
163 <rect y='15' data-level='2' class="ContributionCalendar-day" />
164 <rect y='30' data-level='3' class="ContributionCalendar-day" />
165 <rect y='45' data-level='4' class="ContributionCalendar-day" />
166 <rect y='60' data-level='4' class="ContributionCalendar-day" />
167 <rect y='75' data-level='4' class="ContributionCalendar-day" />
168 <rect y='90' data-level='4' class="ContributionCalendar-day" />
169 </g>
170 "#);
171
172 let el = fragment.root_element();
173 let selector = Selector::parse(DAY_SELECTOR).unwrap();
174 let contribution_week = Heatmap::get_contribution_week(&el, &selector).unwrap();
175
176 let expected = ContributionWeek {
177 contributions: vec![
178 Some(Contribution { heat_level: 1 }),
179 Some(Contribution { heat_level: 2 }),
180 Some(Contribution { heat_level: 3 }),
181 Some(Contribution { heat_level: 4 }),
182 Some(Contribution { heat_level: 4 }),
183 Some(Contribution { heat_level: 4 }),
184 Some(Contribution { heat_level: 4 }),
185 ]
186 };
187
188 assert_eq!(contribution_week, expected)
189 }
190
191 #[test]
192 fn error_if_cannot_parse_contribution_week() {
193 let fragment = Html::parse_fragment(r#"
194 <rect y='0' data-level='1' class="InvalidClass" />
195 <rect y='15' data-level='2' class="InvalidClass" />
196 <rect y='30' data-level='3' class="InvalidClass" />
197 <rect y='45' data-level='4' class="InvalidClass" />
198 <rect y='60' data-level='4' class="InvalidClass" />
199 <rect y='75' data-level='4' class="InvalidClass" />
200 <rect y='90' data-level='4' class="InvalidClass" />
201 "#);
202
203 let el = fragment.root_element();
204 let selector = Selector::parse(DAY_SELECTOR).unwrap();
205 let contribution_week = Heatmap::get_contribution_week(&el, &selector);
206 let expected = Err(HeatmapError::QueryElement {
207 alias: "heatmap node".to_string(),
208 selector: DAY_SELECTOR.to_string(),
209 });
210
211 assert_eq!(contribution_week, expected)
212 }
213}
214
215