oh-my-gitstats 0.1.0__tar.gz
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.
- oh_my_gitstats-0.1.0/LICENSE +21 -0
- oh_my_gitstats-0.1.0/PKG-INFO +173 -0
- oh_my_gitstats-0.1.0/README.md +145 -0
- oh_my_gitstats-0.1.0/pyproject.toml +46 -0
- oh_my_gitstats-0.1.0/setup.cfg +4 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/__init__.py +3 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/charts.py +245 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/cli.py +94 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/collector.py +314 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/constants.py +18 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/data.py +104 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/template.html +265 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats/visualizer.py +84 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats.egg-info/PKG-INFO +173 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats.egg-info/SOURCES.txt +17 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats.egg-info/dependency_links.txt +1 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats.egg-info/entry_points.txt +2 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats.egg-info/requires.txt +4 -0
- oh_my_gitstats-0.1.0/src/oh_my_gitstats.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 amomorning
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oh-my-gitstats
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Git repository commit statistics collector and visualizer
|
|
5
|
+
Author-email: amomorning <amomorning@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/amomorning/oh-my-gitstats
|
|
8
|
+
Project-URL: Repository, https://github.com/amomorning/oh-my-gitstats
|
|
9
|
+
Project-URL: Issues, https://github.com/amomorning/oh-my-gitstats/issues
|
|
10
|
+
Keywords: git,statistics,visualization,commit,heatmap,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: click>=8.0
|
|
24
|
+
Requires-Dist: gitpython>=3.1
|
|
25
|
+
Requires-Dist: pyecharts>=2.0
|
|
26
|
+
Requires-Dist: jinja2>=3.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# π oh-my-gitstats
|
|
34
|
+
|
|
35
|
+
[δΈζ](README.zh-CN.md) | English
|
|
36
|
+
|
|
37
|
+
A Python CLI tool for collecting git commit statistics and visualizing them as interactive HTML charts.
|
|
38
|
+
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
## β¨ Features
|
|
45
|
+
|
|
46
|
+
- π **Batch Collection** - Scan multiple git repositories recursively
|
|
47
|
+
- β‘ **Incremental Sync** - Only fetch new commits since last collection
|
|
48
|
+
- π **Line Charts** - Track changes over time with metric & granularity switching
|
|
49
|
+
- ποΈ **Calendar Heatmaps** - Visualize commit activity with year-based filtering
|
|
50
|
+
- π― **Aggregated & Individual Views** - See combined or per-repo statistics
|
|
51
|
+
- π **VS Code Integration** - Open repo folders directly from the HTML report
|
|
52
|
+
|
|
53
|
+
## π Installation
|
|
54
|
+
|
|
55
|
+
### From PyPI (Recommended)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install oh-my-gitstats
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### From Source (Development)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/amomorning/oh-my-gitstats.git
|
|
65
|
+
cd oh-my-gitstats
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## π Usage
|
|
70
|
+
|
|
71
|
+
### 1οΈβ£ Collect Commit Data
|
|
72
|
+
|
|
73
|
+
Scan a directory for git repositories and export to JSON:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
gitstats collect /path/to/repos --output ./data
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Options:**
|
|
80
|
+
|
|
81
|
+
| Option | Description |
|
|
82
|
+
|--------|-------------|
|
|
83
|
+
| `-o, --output` | Directory to save JSON files (default: `./data`) |
|
|
84
|
+
| `-q, --quiet` | Suppress output messages |
|
|
85
|
+
|
|
86
|
+
### 2οΈβ£ Incremental Sync
|
|
87
|
+
|
|
88
|
+
You may collect repos from multiple locations into the same `data` directory. Re-running `collect` on every location is slow β `sync` reads the existing JSON files and only fetches new commits for each repo:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Collect from multiple locations (one-time)
|
|
92
|
+
gitstats collect /path/to/work-projects --output ./data
|
|
93
|
+
gitstats collect /path/to/personal-projects --output ./data
|
|
94
|
+
|
|
95
|
+
# Later, update all at once β only new commits
|
|
96
|
+
gitstats sync ./data
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Options:**
|
|
100
|
+
|
|
101
|
+
| Option | Description |
|
|
102
|
+
|--------|-------------|
|
|
103
|
+
| `-q, --quiet` | Suppress output messages |
|
|
104
|
+
|
|
105
|
+
### 3οΈβ£ Generate Visualization
|
|
106
|
+
|
|
107
|
+
Create an interactive HTML file from collected data:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
gitstats visualize ./data --output ./output/stats.html
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Options:**
|
|
114
|
+
|
|
115
|
+
| Option | Description |
|
|
116
|
+
|--------|-------------|
|
|
117
|
+
| `-o, --output` | HTML file path (default: `./output/stats.html`) |
|
|
118
|
+
|
|
119
|
+
Granularity and metric can be switched dynamically in the generated HTML β no need to regenerate.
|
|
120
|
+
|
|
121
|
+
## π Output
|
|
122
|
+
|
|
123
|
+
The generated HTML contains:
|
|
124
|
+
|
|
125
|
+
1. **π Line Chart** - Changes over time with metric selector (Lines Changed / Commit Count) and granularity selector (Day/Week/Month). Click legend to toggle projects.
|
|
126
|
+
|
|
127
|
+
2. **ποΈ Aggregate Heatmap** - Combined activity across all repos with year selector (All Years / specific year).
|
|
128
|
+
|
|
129
|
+
3. **π Individual Heatmaps** - Per-repository calendar views in a responsive grid, each with sync status indicator and an "Open Folder" button to open in VS Code.
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
## π JSON Format
|
|
135
|
+
|
|
136
|
+
Each repository generates a JSON file:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"repo_name": "my-project",
|
|
141
|
+
"repo_path": "/absolute/path/to/my-project",
|
|
142
|
+
"last_commit_hash": "a1b2c3d4...",
|
|
143
|
+
"sync_status": "synced",
|
|
144
|
+
"commits": [
|
|
145
|
+
{
|
|
146
|
+
"timestamp": "2024-01-15T10:30:00",
|
|
147
|
+
"additions": 45,
|
|
148
|
+
"deletions": 12
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The `last_commit_hash` field stores the HEAD commit hash at collection time. During `sync`, repositories with a matching hash are skipped instantly β no git operations needed.
|
|
155
|
+
|
|
156
|
+
The `sync_status` field indicates the repository's sync state with its remote:
|
|
157
|
+
|
|
158
|
+
| Status | Description |
|
|
159
|
+
| ------ | ----------- |
|
|
160
|
+
| β
`synced` | In sync with remote |
|
|
161
|
+
| βοΈ `local_changes` | Local has uncommitted changes, remote is up-to-date |
|
|
162
|
+
| β¬οΈ `remote_ahead` | Local is clean, but remote has new commits |
|
|
163
|
+
| β οΈ `diverged` | Local has uncommitted changes and remote has new commits |
|
|
164
|
+
| π `local_only_clean` | No remote configured, local is clean |
|
|
165
|
+
| π§ `local_only_dirty` | No remote configured, local has uncommitted changes |
|
|
166
|
+
|
|
167
|
+
## π§ Requirements
|
|
168
|
+
|
|
169
|
+
- Python 3.9+
|
|
170
|
+
- click
|
|
171
|
+
- gitpython
|
|
172
|
+
- pyecharts
|
|
173
|
+
- jinja2
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# π oh-my-gitstats
|
|
6
|
+
|
|
7
|
+
[δΈζ](README.zh-CN.md) | English
|
|
8
|
+
|
|
9
|
+
A Python CLI tool for collecting git commit statistics and visualizing them as interactive HTML charts.
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## β¨ Features
|
|
17
|
+
|
|
18
|
+
- π **Batch Collection** - Scan multiple git repositories recursively
|
|
19
|
+
- β‘ **Incremental Sync** - Only fetch new commits since last collection
|
|
20
|
+
- π **Line Charts** - Track changes over time with metric & granularity switching
|
|
21
|
+
- ποΈ **Calendar Heatmaps** - Visualize commit activity with year-based filtering
|
|
22
|
+
- π― **Aggregated & Individual Views** - See combined or per-repo statistics
|
|
23
|
+
- π **VS Code Integration** - Open repo folders directly from the HTML report
|
|
24
|
+
|
|
25
|
+
## π Installation
|
|
26
|
+
|
|
27
|
+
### From PyPI (Recommended)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install oh-my-gitstats
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### From Source (Development)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/amomorning/oh-my-gitstats.git
|
|
37
|
+
cd oh-my-gitstats
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## π Usage
|
|
42
|
+
|
|
43
|
+
### 1οΈβ£ Collect Commit Data
|
|
44
|
+
|
|
45
|
+
Scan a directory for git repositories and export to JSON:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
gitstats collect /path/to/repos --output ./data
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Options:**
|
|
52
|
+
|
|
53
|
+
| Option | Description |
|
|
54
|
+
|--------|-------------|
|
|
55
|
+
| `-o, --output` | Directory to save JSON files (default: `./data`) |
|
|
56
|
+
| `-q, --quiet` | Suppress output messages |
|
|
57
|
+
|
|
58
|
+
### 2οΈβ£ Incremental Sync
|
|
59
|
+
|
|
60
|
+
You may collect repos from multiple locations into the same `data` directory. Re-running `collect` on every location is slow β `sync` reads the existing JSON files and only fetches new commits for each repo:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Collect from multiple locations (one-time)
|
|
64
|
+
gitstats collect /path/to/work-projects --output ./data
|
|
65
|
+
gitstats collect /path/to/personal-projects --output ./data
|
|
66
|
+
|
|
67
|
+
# Later, update all at once β only new commits
|
|
68
|
+
gitstats sync ./data
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Options:**
|
|
72
|
+
|
|
73
|
+
| Option | Description |
|
|
74
|
+
|--------|-------------|
|
|
75
|
+
| `-q, --quiet` | Suppress output messages |
|
|
76
|
+
|
|
77
|
+
### 3οΈβ£ Generate Visualization
|
|
78
|
+
|
|
79
|
+
Create an interactive HTML file from collected data:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
gitstats visualize ./data --output ./output/stats.html
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Options:**
|
|
86
|
+
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|--------|-------------|
|
|
89
|
+
| `-o, --output` | HTML file path (default: `./output/stats.html`) |
|
|
90
|
+
|
|
91
|
+
Granularity and metric can be switched dynamically in the generated HTML β no need to regenerate.
|
|
92
|
+
|
|
93
|
+
## π Output
|
|
94
|
+
|
|
95
|
+
The generated HTML contains:
|
|
96
|
+
|
|
97
|
+
1. **π Line Chart** - Changes over time with metric selector (Lines Changed / Commit Count) and granularity selector (Day/Week/Month). Click legend to toggle projects.
|
|
98
|
+
|
|
99
|
+
2. **ποΈ Aggregate Heatmap** - Combined activity across all repos with year selector (All Years / specific year).
|
|
100
|
+
|
|
101
|
+
3. **π Individual Heatmaps** - Per-repository calendar views in a responsive grid, each with sync status indicator and an "Open Folder" button to open in VS Code.
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## π JSON Format
|
|
107
|
+
|
|
108
|
+
Each repository generates a JSON file:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"repo_name": "my-project",
|
|
113
|
+
"repo_path": "/absolute/path/to/my-project",
|
|
114
|
+
"last_commit_hash": "a1b2c3d4...",
|
|
115
|
+
"sync_status": "synced",
|
|
116
|
+
"commits": [
|
|
117
|
+
{
|
|
118
|
+
"timestamp": "2024-01-15T10:30:00",
|
|
119
|
+
"additions": 45,
|
|
120
|
+
"deletions": 12
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The `last_commit_hash` field stores the HEAD commit hash at collection time. During `sync`, repositories with a matching hash are skipped instantly β no git operations needed.
|
|
127
|
+
|
|
128
|
+
The `sync_status` field indicates the repository's sync state with its remote:
|
|
129
|
+
|
|
130
|
+
| Status | Description |
|
|
131
|
+
| ------ | ----------- |
|
|
132
|
+
| β
`synced` | In sync with remote |
|
|
133
|
+
| βοΈ `local_changes` | Local has uncommitted changes, remote is up-to-date |
|
|
134
|
+
| β¬οΈ `remote_ahead` | Local is clean, but remote has new commits |
|
|
135
|
+
| β οΈ `diverged` | Local has uncommitted changes and remote has new commits |
|
|
136
|
+
| π `local_only_clean` | No remote configured, local is clean |
|
|
137
|
+
| π§ `local_only_dirty` | No remote configured, local has uncommitted changes |
|
|
138
|
+
|
|
139
|
+
## π§ Requirements
|
|
140
|
+
|
|
141
|
+
- Python 3.9+
|
|
142
|
+
- click
|
|
143
|
+
- gitpython
|
|
144
|
+
- pyecharts
|
|
145
|
+
- jinja2
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oh-my-gitstats"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Git repository commit statistics collector and visualizer"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "amomorning", email = "amomorning@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["git", "statistics", "visualization", "commit", "heatmap", "cli"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"click>=8.0",
|
|
25
|
+
"gitpython>=3.1",
|
|
26
|
+
"pyecharts>=2.0",
|
|
27
|
+
"jinja2>=3.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/amomorning/oh-my-gitstats"
|
|
32
|
+
Repository = "https://github.com/amomorning/oh-my-gitstats"
|
|
33
|
+
Issues = "https://github.com/amomorning/oh-my-gitstats/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
gitstats = "oh_my_gitstats.cli:main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["setuptools>=61.0"]
|
|
40
|
+
build-backend = "setuptools.build_meta"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
oh_my_gitstats = ["template.html"]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Chart building functions using pyecharts."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from pyecharts import options as opts
|
|
8
|
+
from pyecharts.charts import Line, Calendar
|
|
9
|
+
from pyecharts.commons.utils import JsCode
|
|
10
|
+
|
|
11
|
+
from .constants import METRICS, GRANULARITIES, COLORS
|
|
12
|
+
from .data import aggregate_by_period
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_line_opts(
|
|
16
|
+
all_data: List[Dict[str, Any]],
|
|
17
|
+
granularity: str,
|
|
18
|
+
metric: str
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Build line chart options JSON string.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
all_data: List of repository data.
|
|
24
|
+
granularity: Time aggregation granularity (day/week/month).
|
|
25
|
+
metric: "changes" or "commits".
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
JSON string of chart options.
|
|
29
|
+
"""
|
|
30
|
+
all_periods = set()
|
|
31
|
+
repo_series = {}
|
|
32
|
+
|
|
33
|
+
for repo in all_data:
|
|
34
|
+
aggregated = aggregate_by_period(repo["commits"], granularity, metric)
|
|
35
|
+
repo_series[repo["repo_name"]] = aggregated
|
|
36
|
+
all_periods.update(aggregated.keys())
|
|
37
|
+
|
|
38
|
+
sorted_periods = sorted(all_periods)
|
|
39
|
+
|
|
40
|
+
line = Line()
|
|
41
|
+
line.add_xaxis(sorted_periods)
|
|
42
|
+
|
|
43
|
+
for idx, (repo_name, data) in enumerate(repo_series.items()):
|
|
44
|
+
y_data = [data.get(period, 0) for period in sorted_periods]
|
|
45
|
+
line.add_yaxis(
|
|
46
|
+
series_name=repo_name,
|
|
47
|
+
y_axis=y_data,
|
|
48
|
+
color=COLORS[idx % len(COLORS)],
|
|
49
|
+
is_smooth=True,
|
|
50
|
+
label_opts=opts.LabelOpts(is_show=False),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
y_name = "Lines Changed" if metric == "changes" else "Commits"
|
|
54
|
+
|
|
55
|
+
line.set_global_opts(
|
|
56
|
+
tooltip_opts=opts.TooltipOpts(
|
|
57
|
+
trigger="axis",
|
|
58
|
+
formatter=JsCode(
|
|
59
|
+
"function(params){"
|
|
60
|
+
"var tip=params[0].axisValueLabel;"
|
|
61
|
+
"params.forEach(function(p){"
|
|
62
|
+
"if(p.value[1])tip+='<br/>'+p.marker+p.seriesName+': '+p.value[1];"
|
|
63
|
+
"});"
|
|
64
|
+
"return tip;}"
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
legend_opts=opts.LegendOpts(
|
|
68
|
+
type_="scroll",
|
|
69
|
+
selected_mode="multiple"
|
|
70
|
+
),
|
|
71
|
+
xaxis_opts=opts.AxisOpts(
|
|
72
|
+
type_="category",
|
|
73
|
+
axislabel_opts=opts.LabelOpts(rotate=45)
|
|
74
|
+
),
|
|
75
|
+
yaxis_opts=opts.AxisOpts(name=y_name, type_="value"),
|
|
76
|
+
datazoom_opts=[
|
|
77
|
+
opts.DataZoomOpts(type_="inside"),
|
|
78
|
+
opts.DataZoomOpts(type_="slider")
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return line.dump_options()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_agg_heatmap_opts(
|
|
86
|
+
all_data: List[Dict[str, Any]],
|
|
87
|
+
date_range: tuple[str, str],
|
|
88
|
+
metric: str
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Build aggregate heatmap options JSON string.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
all_data: List of repository data.
|
|
94
|
+
date_range: Tuple of (start_date, end_date).
|
|
95
|
+
metric: "changes" or "commits".
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
JSON string of chart options.
|
|
99
|
+
"""
|
|
100
|
+
daily_data = defaultdict(int)
|
|
101
|
+
for repo in all_data:
|
|
102
|
+
for commit in repo["commits"]:
|
|
103
|
+
ts = datetime.fromisoformat(commit["timestamp"])
|
|
104
|
+
date_str = ts.strftime("%Y-%m-%d")
|
|
105
|
+
if metric == "commits":
|
|
106
|
+
daily_data[date_str] += 1
|
|
107
|
+
else:
|
|
108
|
+
daily_data[date_str] += commit["additions"] + commit["deletions"]
|
|
109
|
+
|
|
110
|
+
calendar_data = [[date, value] for date, value in sorted(daily_data.items())]
|
|
111
|
+
|
|
112
|
+
calendar = Calendar(init_opts=opts.InitOpts(width="100%", height="300px"))
|
|
113
|
+
calendar.add(
|
|
114
|
+
series_name="",
|
|
115
|
+
yaxis_data=calendar_data,
|
|
116
|
+
calendar_opts=opts.CalendarOpts(
|
|
117
|
+
pos_left="100px",
|
|
118
|
+
pos_right="50px",
|
|
119
|
+
pos_top="50px",
|
|
120
|
+
pos_bottom="20px",
|
|
121
|
+
range_=date_range,
|
|
122
|
+
yearlabel_opts=opts.CalendarYearLabelOpts(is_show=True),
|
|
123
|
+
monthlabel_opts=opts.CalendarMonthLabelOpts(is_show=True),
|
|
124
|
+
daylabel_opts=opts.CalendarDayLabelOpts(is_show=True),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
unit = "lines" if metric == "changes" else "commits"
|
|
129
|
+
calendar.set_global_opts(
|
|
130
|
+
visualmap_opts=opts.VisualMapOpts(
|
|
131
|
+
max_=max(daily_data.values()) if daily_data else 100,
|
|
132
|
+
min_=0,
|
|
133
|
+
orient="horizontal",
|
|
134
|
+
is_piecewise=False,
|
|
135
|
+
pos_left="100px",
|
|
136
|
+
pos_bottom="0px",
|
|
137
|
+
),
|
|
138
|
+
tooltip_opts=opts.TooltipOpts(formatter=JsCode(
|
|
139
|
+
f"function(p){{return p.data[0] + ': ' + p.data[1] + ' {unit}'}}"
|
|
140
|
+
))
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return calendar.dump_options()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_ind_heatmap_opts(
|
|
147
|
+
all_data: List[Dict[str, Any]],
|
|
148
|
+
date_range: tuple[str, str],
|
|
149
|
+
metric: str
|
|
150
|
+
) -> List[str]:
|
|
151
|
+
"""Build individual heatmap options JSON string list.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
all_data: List of repository data.
|
|
155
|
+
date_range: Tuple of (start_date, end_date).
|
|
156
|
+
metric: "changes" or "commits".
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of JSON strings, one per repository.
|
|
160
|
+
"""
|
|
161
|
+
options_list = []
|
|
162
|
+
|
|
163
|
+
for repo in all_data:
|
|
164
|
+
daily_data = defaultdict(int)
|
|
165
|
+
for commit in repo["commits"]:
|
|
166
|
+
ts = datetime.fromisoformat(commit["timestamp"])
|
|
167
|
+
date_str = ts.strftime("%Y-%m-%d")
|
|
168
|
+
if metric == "commits":
|
|
169
|
+
daily_data[date_str] += 1
|
|
170
|
+
else:
|
|
171
|
+
daily_data[date_str] += commit["additions"] + commit["deletions"]
|
|
172
|
+
|
|
173
|
+
calendar_data = [[date, value] for date, value in sorted(daily_data.items())]
|
|
174
|
+
|
|
175
|
+
calendar = Calendar(init_opts=opts.InitOpts(width="100%", height="200px"))
|
|
176
|
+
calendar.add(
|
|
177
|
+
series_name="",
|
|
178
|
+
yaxis_data=calendar_data,
|
|
179
|
+
calendar_opts=opts.CalendarOpts(
|
|
180
|
+
pos_left="100px",
|
|
181
|
+
pos_right="50px",
|
|
182
|
+
pos_top="30px",
|
|
183
|
+
pos_bottom="10px",
|
|
184
|
+
range_=date_range,
|
|
185
|
+
yearlabel_opts=opts.CalendarYearLabelOpts(is_show=False),
|
|
186
|
+
monthlabel_opts=opts.CalendarMonthLabelOpts(is_show=True),
|
|
187
|
+
daylabel_opts=opts.CalendarDayLabelOpts(is_show=False),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
max_val = max(daily_data.values()) if daily_data else 100
|
|
192
|
+
unit = "lines" if metric == "changes" else "commits"
|
|
193
|
+
calendar.set_global_opts(
|
|
194
|
+
visualmap_opts=opts.VisualMapOpts(
|
|
195
|
+
max_=max_val,
|
|
196
|
+
min_=0,
|
|
197
|
+
orient="horizontal",
|
|
198
|
+
is_piecewise=False,
|
|
199
|
+
pos_left="100px",
|
|
200
|
+
pos_bottom="0px",
|
|
201
|
+
),
|
|
202
|
+
tooltip_opts=opts.TooltipOpts(formatter=JsCode(
|
|
203
|
+
f"function(p){{return p.data[0] + ': ' + p.data[1] + ' {unit}'}}"
|
|
204
|
+
))
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
options_list.append(calendar.dump_options())
|
|
208
|
+
|
|
209
|
+
return options_list
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_line_js_obj(all_data: List[Dict[str, Any]]) -> str:
|
|
213
|
+
"""Build JS object string for line chart data (all metric x granularity combos)."""
|
|
214
|
+
parts = []
|
|
215
|
+
for metric in METRICS:
|
|
216
|
+
gran_parts = []
|
|
217
|
+
for gran in GRANULARITIES:
|
|
218
|
+
opts_json = build_line_opts(all_data, gran, metric)
|
|
219
|
+
gran_parts.append(f'"{gran}":{opts_json}')
|
|
220
|
+
parts.append(f'"{metric}":{{{",".join(gran_parts)}}}')
|
|
221
|
+
return "{" + ",".join(parts) + "}"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_heatmap_js_obj(
|
|
225
|
+
all_data: List[Dict[str, Any]],
|
|
226
|
+
date_range: tuple[str, str],
|
|
227
|
+
years: List[str]
|
|
228
|
+
) -> str:
|
|
229
|
+
"""Build JS object string for heatmap data (all metric x year combos)."""
|
|
230
|
+
year_ranges: Dict[str, tuple[str, str]] = {"all": date_range}
|
|
231
|
+
for year in years:
|
|
232
|
+
year_ranges[year] = (f"{year}-01-01", f"{year}-12-31")
|
|
233
|
+
|
|
234
|
+
parts = []
|
|
235
|
+
for metric in METRICS:
|
|
236
|
+
year_parts = []
|
|
237
|
+
for year_key, year_range in year_ranges.items():
|
|
238
|
+
agg = build_agg_heatmap_opts(all_data, year_range, metric)
|
|
239
|
+
ind_list = build_ind_heatmap_opts(all_data, year_range, metric)
|
|
240
|
+
ind_str = ",".join(ind_list)
|
|
241
|
+
year_parts.append(
|
|
242
|
+
f'"{year_key}":{{"aggregate":{agg},"individual":[{ind_str}]}}'
|
|
243
|
+
)
|
|
244
|
+
parts.append(f'"{metric}":{{{",".join(year_parts)}}}')
|
|
245
|
+
return "{" + ",".join(parts) + "}"
|