githubstats is a data pipeline and interactive visualization for GitHub programming language popularity, derived from real developer activity on GH Archive.
The pipeline downloads every public GitHub event for a given month, filters out bots and automated noise, resolves language compositions per repository, and computes multiple weighted language activity ratings. The results are consumed by a single-page web UI (index.html) that renders an interactive trend chart (via Apache ECharts) and a ranked table with sparklines.
The project covers January 2015 through the present (~11 years of monthly data). A key data discontinuity exists: GitHub removed language data from event payloads in October 2025. Before that date, language attribution uses the single primary language field; from October 2025 onward, the GraphQL API is required for full multi-language breakdown.
- Backend: Rust (Edition 2024), async with
tokio, HTTP withreqwest, JSON withserde_json, CLI withclap - Frontend: Pure HTML + CSS + vanilla JavaScript, no build step; Apache ECharts 5 loaded from CDN
- CI: GitHub Actions —
cargo fmt --check+cargo clippy -D warningson every push/PR
githubstats/
├── src/bin/ # Five standalone CLI binaries (the entire backend)
│ ├── github_archive_loader.rs # Step 1: GH Archive → aggregated CSV
│ ├── filter_archive.rs # Step 2: filtered CSV (bot/noise removal)
│ ├── github_language_loader.rs # Step 3: repo slugs → GitHub GraphQL → language JSONL
│ ├── produce_statistics.rs # Step 4: CSV + languages → 5 per-month rating files
│ └── pack_statistics.rs # Step 5: per-month files → 5 combined all-months files
├── data/ # All intermediate and final data (83 GB, partially gitignored)
│ ├── archive-YYYYMM-filtered.csv # Gitignored; ~0.1–1 GB each, 138 months
│ ├── languages-YYYY-MM.jsonl # Gitignored; repo → language breakdown per month
│ ├── language-ratings-YYYY-MM-<type>.jsonl # Per-month ratings, one file per type/month
│ └── language-ratings-all-<type>.jsonl # Combined files consumed by the frontend
├── docs/ # Reference docs and sample event payloads
│ ├── GITHUB_EVENT_TYPES.md
│ └── events/{2024,2026}/ # Sample pre/post API-change payloads
├── index.html # Frontend SPA — no build step, open directly in browser
├── Cargo.toml # Package name is `fetch_month` (historical); defines all 5 binaries
Where to put things:
- New pipeline steps → new file under
src/bin/ - Shared utilities → if significant, extract to
src/lib.rs(no lib crate exists yet; keep code self-contained per binary unless sharing is clearly warranted) - New data files →
data/, following the existing naming conventions (see below) - Reference documentation →
docs/ - Frontend changes →
index.htmlonly (no build tooling)
| File | Pattern |
|---|---|
| Raw archive (gitignored) | data/archive-YYYYMM.csv |
| Filtered archive (gitignored) | data/archive-YYYYMM-filtered.csv |
| Language lookup (gitignored) | data/languages-YYYY-MM.jsonl |
| Per-month rating (committed) | data/language-ratings-YYYY-MM-<type>.jsonl |
| Combined all-months rating (committed) | data/language-ratings-all-<type>.jsonl |
Valid <type> values: pr-count, issue-count, push-count, developer-activity, active-repos, star-count
# Build all binaries
cargo build --release
# Lint (matches CI exactly)
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
# Run tests
cargo testIndividual pipeline step examples are documented in README.md.
Self-contained binaries: Each binary under src/bin/ is fully self-contained. There is intentionally no shared library crate. Prefer this pattern — duplicate small utilities rather than introducing premature abstractions.
Staged concurrent pipeline (github_archive_loader): Uses bounded async-channel MPMC channels between stages. Network I/O is async; CPU-intensive work (gzip decompression, JSON parsing) runs on tokio::task::spawn_blocking. This pattern must be preserved to avoid blocking the async executor.
stderr for diagnostics, stdout for data (github_language_loader): All progress output goes to stderr; all data output goes to stdout as clean JSONL. This enables shell piping and must be maintained for any binary that reads/writes data streams.
Sequential filter chain (filter_archive): Each filter is a pure function Vec<Row> → Vec<Row> named filter_<noun>. The main function reads as a pipeline. Follow this pattern when adding new filters. Every filter must log a [filter_name] N removed (X.X%), M remaining line to stderr.
Retry with exponential back-off: Both HTTP clients implement manual retry (no middleware). New HTTP calls should follow the same pattern.
anyhow::Resultfor all fallible functions;.context("...")at every I/O site; nounwrap()in production paths- Every binary opens with a
//!module doc comment documenting: purpose, pipeline stage, input/output format, usage example, and design decisions - Inline comments explain why, not what — especially for non-obvious decisions (API quirks, exploit mitigations, concurrency choices)
- Progress lines use bracketed prefixes:
[stage_name],[retry N/M],[rate-limit], etc. - CLI struct is always named
Argsand derived withclap::Parser - Constants at the top of each file with explanatory comments
| Variable | Required by | Purpose |
|---|---|---|
GITHUB_TOKEN |
github_language_loader |
GitHub PAT for GraphQL API (public repo read access sufficient) |
After making any change to index.html, verify it visually using the integrated browser tool:
- Start a local HTTP server in the background (do not use
file://URLs — ECharts and fetch-based data loading require HTTP):python3 -m http.server 8080 --directory . &
- Navigate the browser tool to
http://localhost:8080/index.html. - Take a snapshot or screenshot and confirm the page renders correctly (chart visible, table populated, no console errors).
- Check the browser console for JavaScript errors (
playwright_browser_console_messagesat levelerror). - Kill the server when done:
kill %1 # or: pkill -f "http.server 8080"
If port 8080 is already in use, try 8081, 8082, etc.
- Do not add a frontend build step or JavaScript framework — the zero-tooling approach is intentional
- Do not add
unwrap()orexpect()in production code paths - Do not block the tokio async executor with CPU-heavy work — use
spawn_blocking - Do not change the data file naming conventions without updating
pack_statistics.rsandindex.html
Never run git commit, git push, or create pull requests unless the user explicitly requests it. Always leave changes staged or unstaged for the user to review before committing.