@nozich/git-mood 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/svg.rs ADDED
@@ -0,0 +1,190 @@
1
+ use std::collections::HashMap;
2
+ use chrono::{Duration, Local, Datelike};
3
+ use serde::{Deserialize, Serialize};
4
+ use crate::sentiment::Mood;
5
+
6
+ #[derive(Debug, Serialize, Deserialize, Clone, Default)]
7
+ pub struct DayStats {
8
+ pub commits_count: usize,
9
+ pub positive: usize,
10
+ pub negative: usize,
11
+ pub neutral: usize,
12
+ pub refactor: usize,
13
+ pub weekend: usize,
14
+ }
15
+
16
+ impl DayStats {
17
+ pub fn determine_mood(&self) -> Option<Mood> {
18
+ if self.commits_count == 0 {
19
+ return None;
20
+ }
21
+
22
+ // 1. If 30% or more commits are negative, mark the day negative (Red)
23
+ if self.negative > 0 && (self.negative as f64 / self.commits_count as f64) >= 0.3 {
24
+ return Some(Mood::Negative);
25
+ }
26
+
27
+ // 2. If 50% or more are refactoring, mark refactor (Yellow)
28
+ if self.refactor > 0 && (self.refactor as f64 / self.commits_count as f64) >= 0.5 {
29
+ return Some(Mood::Refactor);
30
+ }
31
+
32
+ // 3. If 50% or more are weekend commits, mark weekend (Purple)
33
+ if self.weekend > 0 && (self.weekend as f64 / self.commits_count as f64) >= 0.5 {
34
+ return Some(Mood::Weekend);
35
+ }
36
+
37
+ // 4. If positive >= neutral, mark positive (Turquoise)
38
+ if self.positive > 0 && self.positive >= self.neutral {
39
+ return Some(Mood::Positive);
40
+ }
41
+
42
+ // 5. Default is Neutral (Green)
43
+ Some(Mood::Neutral)
44
+ }
45
+ }
46
+
47
+ pub fn generate_svg(log_data: &HashMap<String, DayStats>) -> String {
48
+ let today = Local::now().date_naive();
49
+ let weekday = today.weekday().num_days_from_sunday() as i64; // Sunday = 0, Saturday = 6
50
+
51
+ // Find Sunday of 52 weeks ago
52
+ let start_date = today - Duration::days(weekday + 52 * 7);
53
+
54
+ let box_size = 10;
55
+ let gap = 2;
56
+ let margin_left = 35;
57
+ let margin_top = 45;
58
+
59
+ let mut grid_svg = String::new();
60
+ let mut current_date = start_date;
61
+
62
+ let mut total_commits = 0;
63
+ let mut mood_counts = HashMap::new();
64
+
65
+ // Iterate through all days from start_date to today
66
+ while current_date <= today {
67
+ let date_str = current_date.format("%Y-%m-%d").to_string();
68
+
69
+ let stats = log_data.get(&date_str);
70
+ let mood = stats.and_then(|s| {
71
+ total_commits += s.commits_count;
72
+ s.determine_mood()
73
+ });
74
+
75
+ if let Some(m) = mood {
76
+ *mood_counts.entry(m).or_insert(0) += 1;
77
+ }
78
+
79
+ let days_since_start = (current_date - start_date).num_days();
80
+ let col = days_since_start / 7;
81
+ let row = days_since_start % 7;
82
+
83
+ let x = margin_left + col * (box_size + gap);
84
+ let y = margin_top + row * (box_size + gap);
85
+
86
+ let color = match mood {
87
+ Some(Mood::Positive) => Mood::Positive.to_color_code(),
88
+ Some(Mood::Negative) => Mood::Negative.to_color_code(),
89
+ Some(Mood::Refactor) => Mood::Refactor.to_color_code(),
90
+ Some(Mood::Weekend) => Mood::Weekend.to_color_code(),
91
+ Some(Mood::Neutral) => Mood::Neutral.to_color_code(),
92
+ None => "#161b22", // Gray
93
+ };
94
+
95
+ grid_svg.push_str(&format!(
96
+ r#"<rect class="day" x="{}" y="{}" width="{}" height="{}" rx="2" fill="{}" data-date="{}" />"#,
97
+ x, y, box_size, box_size, color, date_str
98
+ ));
99
+ grid_svg.push('\n');
100
+
101
+ current_date += Duration::days(1);
102
+ }
103
+
104
+ // Determine dominant mood
105
+ let dominant_mood = mood_counts
106
+ .iter()
107
+ .max_by_key(|&(_, count)| count)
108
+ .map(|(mood, _)| *mood)
109
+ .unwrap_or(Mood::Neutral);
110
+
111
+ let dominant_mood_str = match dominant_mood {
112
+ Mood::Positive => "Turquoise (Highly Productive)",
113
+ Mood::Negative => "Red (Bug Squashing / Stressed)",
114
+ Mood::Refactor => "Yellow (Cleanups & Refactoring)",
115
+ Mood::Weekend => "Purple (Weekend Warrior)",
116
+ Mood::Neutral => "Green (Steady Progress)",
117
+ };
118
+
119
+ let dominant_mood_color = dominant_mood.to_color_code();
120
+
121
+ // Build the final SVG with a beautiful widget wrapper
122
+ format!(
123
+ r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 710 170" width="100%" height="100%">
124
+ <style>
125
+ .bg {{ fill: #0d1117; rx: 8px; }}
126
+ .title {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 14px; fill: #adbac7; font-weight: 600; }}
127
+ .subtitle {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 11px; fill: #768390; }}
128
+ .legend-text {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 10px; fill: #768390; }}
129
+ .day {{ transition: transform 0.1s ease; cursor: pointer; }}
130
+ .day:hover {{ transform: scale(1.2); transform-origin: center; stroke: #539bf5; stroke-width: 1px; }}
131
+ </style>
132
+ <rect class="bg" width="710" height="170" />
133
+
134
+ <!-- Header Info -->
135
+ <text x="20" y="25" class="title">git-mood // Contribution Analytics</text>
136
+ <text x="500" y="25" class="subtitle" text-anchor="end">Dominant Mood: <tspan fill="{}">{}</tspan></text>
137
+ <text x="700" y="25" class="subtitle" text-anchor="end">Total commits tracked: {}</text>
138
+
139
+ <!-- Contribution Grid -->
140
+ {}
141
+
142
+ <!-- Legend -->
143
+ <g transform="translate(480, 145)">
144
+ <rect x="0" y="0" width="8" height="8" rx="1" fill="#161b22" />
145
+ <rect x="25" y="0" width="8" height="8" rx="1" fill="#00cdac" />
146
+ <rect x="50" y="0" width="8" height="8" rx="1" fill="#00f2fe" />
147
+ <rect x="75" y="0" width="8" height="8" rx="1" fill="#ff0844" />
148
+ <rect x="100" y="0" width="8" height="8" rx="1" fill="#f6d365" />
149
+ <rect x="125" y="0" width="8" height="8" rx="1" fill="#7f00ff" />
150
+
151
+ <text x="140" y="8" class="legend-text">Mood Map</text>
152
+ </g>
153
+
154
+ <!-- Day labels (S M T W T F S) -->
155
+ <g class="legend-text" transform="translate(15, 54)">
156
+ <text x="0" y="0">S</text>
157
+ <text x="0" y="24">T</text>
158
+ <text x="0" y="48">T</text>
159
+ <text x="0" y="72">S</text>
160
+ </g>
161
+ </svg>"##,
162
+ dominant_mood_color, dominant_mood_str, total_commits, grid_svg
163
+ )
164
+ }
165
+
166
+ #[cfg(test)]
167
+ mod tests {
168
+ use super::*;
169
+
170
+ #[test]
171
+ fn test_svg_generation() {
172
+ let mut log_data = HashMap::new();
173
+ log_data.insert(
174
+ "2026-06-18".to_string(),
175
+ DayStats {
176
+ commits_count: 5,
177
+ positive: 4,
178
+ negative: 0,
179
+ neutral: 1,
180
+ refactor: 0,
181
+ weekend: 0,
182
+ },
183
+ );
184
+
185
+ let svg = generate_svg(&log_data);
186
+ assert!(svg.contains("git-mood // Contribution Analytics"));
187
+ assert!(svg.contains("Dominant Mood"));
188
+ assert!(svg.contains("2026-06-18"));
189
+ }
190
+ }