diff --git a/gitgen.py b/gitgen.py
index 80b75d7..6e6eac2 100644
--- a/gitgen.py
+++ b/gitgen.py
@@ -149,6 +149,8 @@ class GitRepoScanner:
return dict(contributions)
def generate_contribution_graph_html(self) -> str:
+ import math
+
contributions = self.get_all_contributions()
monthly_commits = defaultdict(int)
@@ -171,28 +173,89 @@ class GitRepoScanner:
'year_month': year_month
})
- # Find max for scaling
- max_commits = max((m['count'] for m in months_data), default=1)
- if max_commits == 0:
- max_commits = 1
+ max_commits = max((m['count'] for m in months_data), default=1) or 1
+
+ def nice_interval(max_val):
+ raw = max_val / 5
+ magnitude = 10 ** math.floor(math.log10(raw)) if raw >= 1 else 1
+ for step in [1, 2, 5, 10]:
+ if raw <= step * magnitude:
+ return step * magnitude
+ return magnitude * 10
+
+ interval = nice_interval(max_commits)
+ num_intervals = math.ceil(max_commits / interval)
+ chart_max = interval * num_intervals
+
+ # SVG dimensions
+ svg_w = 760
+ svg_h = 300
+ pad_left = 40
+ pad_right = 30
+ pad_top = 30
+ pad_bottom = 40
+ plot_w = svg_w - pad_left - pad_right
+ plot_h = svg_h - pad_top - pad_bottom
+
+ bar_count = len(months_data)
+ bar_gap = 12
+ bar_w = (plot_w - bar_gap * (bar_count - 1)) / bar_count
+
+ svg_parts = [
+ f'<svg class="commit-chart-svg" viewBox="0 0 {svg_w} {svg_h}" '
+ f'xmlns="http://www.w3.org/2000/svg">'
+ ]
- graph_html = '<div class="monthly-chart">\n'
+ # Leader lines + y-axis labels
+ for i in range(num_intervals + 1):
+ val = i * interval
+ y = pad_top + plot_h - (val / chart_max) * plot_h
+ svg_parts.append(
+ f'<line x1="{pad_left}" y1="{y:.2f}" x2="{svg_w - pad_right}" y2="{y:.2f}" '
+ f'stroke="#e2e8f0" stroke-width="1"/>'
+ )
+ svg_parts.append(
+ f'<text x="{pad_left - 4}" y="{y:.2f}" text-anchor="end" dominant-baseline="middle" '
+ f'font-size="11" fill="#888">{val}</text>'
+ )
- for month in months_data:
- height_percent = (month['count'] / max_commits) * 100 if max_commits > 0 else 0
+ # Bars
+ for idx, month in enumerate(months_data):
+ x = pad_left + idx * (bar_w + bar_gap)
+ bar_h = (month['count'] / chart_max) * plot_h
+ bar_y = pad_top + plot_h - bar_h
date_obj = datetime.datetime.strptime(month['year_month'], '%Y-%m')
formatted_date = date_obj.strftime('%b %Y')
- graph_html += f'''<div class="month-col">
- <div class="col-bar" style="height:{height_percent}%" title="{month['count']} commits in {formatted_date}"></div>
- <div class="col-label">{month['label']}</div>
- <div class="col-count">{month['count']}</div>
- </div>\n'''
+ svg_parts.append(
+ f'<rect x="{x:.2f}" y="{bar_y:.2f}" width="{bar_w:.2f}" height="{bar_h:.2f}" '
+ f'rx="3" fill="#40c463">'
+ f'<title>{month["count"]} commits in {formatted_date}</title>'
+ f'</rect>'
+ )
+
+ label_y = pad_top + plot_h + 16
+ count_y = pad_top + plot_h + 30
+ cx = x + bar_w / 2
- graph_html += '</div>'
+ svg_parts.append(
+ f'<text x="{cx:.2f}" y="{label_y}" text-anchor="middle" '
+ f'font-size="12" fill="#666">{month["label"]}</text>'
+ )
+ svg_parts.append(
+ f'<text x="{cx:.2f}" y="{count_y}" text-anchor="middle" '
+ f'font-size="11" fill="#aaa">{month["count"]}</text>'
+ )
- return graph_html
+ svg_parts.append('</svg>')
+
+ return (
+ '<div class="graph-container">\n'
+ '<h2>Activity</h2>\n' +
+ '\n'.join(svg_parts) +
+ '\n</div>\n'
+ )
def get_working_tree(self, repo_path: Path) -> List[Dict]:
if repo_path in self.tree_cache:
@@ -435,7 +498,6 @@ class HTMLGenerator:
</div>
<div class="graph-container">
- <h2>Activity</h2>
{graph_html}
</div>
diff --git a/main.css b/main.css
index 1e1a19d..3226870 100644
--- a/main.css
+++ b/main.css
@@ -533,53 +533,16 @@ pre code {
/* Commit Chart */
.graph-container h2 {
text-align:center;
+ margin-bottom:0.8rem;
}
- .monthly-chart {
- display: flex;
- align-items: flex-end;
- justify-content: space-around;
- gap: 10px;
- max-width: 800px;
- height: 300px;
- margin: 20px auto;
- padding: 20px;
+ .commit-chart-svg {
+ width: 800px;
+ height: auto;
+ display: block;
background: #fff;
border-radius: 8px;
- }
-
- .month-col {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- height: 100%;
- }
-
- .col-bar {
- width: 100%;
- max-width: 40px;
- background: #40c463;
- border-radius: 4px 4px 0 0;
- min-height: 2px;
- margin-top: auto;
- transition: background 0.2s;
- }
-
- .col-bar:hover {
- background: #30a14e;
- }
-
- .col-label {
- font-size: 12px;
- color: #666;
- font-weight: 500;
- margin-top: 5px;
- }
-
- .col-count {
- font-size: 11px;
- color: #888;
+ margin:0 auto;
}
@media (max-width: 768px) {