Interactive command-line tool for exploring GitHub Pull Request data using SQL-style queries. Type a one-liner, get a plot or table. The repo includes a snapshot of all open Spring AI PRs so you can start immediately.
The repo includes a bundled PR data snapshot — no GitHub token, no jbang, no JDK needed. Just Python and git.
Prerequisites: Python 3.10+ and git. If python3 --version shows older than 3.10 (or is missing), install with Homebrew: brew install [email protected]
git clone https://github.com/markpollack/prplot.git
cd prplot
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python -m prplot data/spring-ai-open-prs.jsonYou should see:
Loaded 283 PRs
Type 'help' for commands, 'fields' for available fields, 'quit' to exit
Try a few queries at the prplot> prompt (close each plot window to return):
prplot> bar author
prplot> hist age_days
prplot> plot age_days vs comment_count
prplot> identify age_days > 180 AND comment_count = 0
prplot> quitTip: prplot has tab completion — type a few letters and press Tab. Arrow keys scroll through command history.
When done, type quit to exit prplot, then deactivate at your shell prompt to leave the virtual environment. Next time: cd prplot && source venv/bin/activate && python -m prplot data/spring-ai-open-prs.json.
Copy-paste these at the prplot> prompt for deeper exploration:
# PRs created in the last 30 days
prplot> identify created_at_dt > now-30d
# PRs not updated in 6 months
prplot> identify updated_at_dt < now-6M
# Label breakdown
prplot> bar label_names
# Load pre-defined cuts for the Spring AI team
prplot> source cuts/spring-ai-all.prplot
# External contributors with stale PRs
prplot> identify $external AND $stale
# Core team PRs from the last 30 days
prplot> identify $core AND $recent
# Recent external contributor PRs (last week)
prplot> identify $external AND $freshThe bundled data/spring-ai-open-prs.json is a point-in-time snapshot. To get current data, use github-collector via jbang — no Java install or build required.
Step 1: Install jbang
curl -Ls https://sh.jbang.dev | bash -s - app setupAfter running, restart your terminal (or source ~/.bashrc) so that the jbang command is on your PATH. Verify with:
jbang --versionStep 2: Add the github-collector catalog
jbang catalog add --name=github-collector \
https://raw.githubusercontent.com/spring-ai-community/github-collector/main/jbang-catalog.jsonThis registers the collect@github-collector alias. jbang will download and cache the Java code automatically on first run — you don't need Maven or Gradle installed separately.
Important: github-collector requires JDK 17. If your default JDK is newer (e.g. 21 or 25), you may hit compilation errors. Check with
java -versionand switch to 17 if needed (e.g.export JAVA_HOME=$(/usr/libexec/java_home -v 17)on macOS).
Step 3: Set your GitHub token
A personal access token with repo scope is required to read PR data from the GitHub API.
export GITHUB_TOKEN=ghp_your_token_hereAdd this to your shell profile (~/.bashrc, ~/.zshrc, etc.) to persist it.
Re-collect all open PRs from scratch, replacing the bundled snapshot:
jbang collect@github-collector \
--type prs --pr-state open --single-file \
-o data/spring-ai-open-prs.jsonThen analyze:
python -m prplot data/spring-ai-open-prs.jsonOnly fetch PRs that changed since the last collection. This is faster and avoids hitting GitHub API rate limits on large repos:
jbang collect@github-collector \
--type prs --pr-state open --single-file \
--incremental --no-clean \
-o data/spring-ai-open-prs.json--incrementalskips PRs whoseupdated_athasn't changed since the last run.--no-cleankeeps previously collected PRs that are no longer returned (e.g. PRs that were closed since the last full refresh). Use a full refresh periodically to prune these.
github-collector defaults to spring-projects/spring-ai. To analyze a different repo:
jbang collect@github-collector \
--repo owner/repo --type prs --pr-state open \
--single-file -o my-project-prs.jsonRun jbang collect@github-collector --help for all options. See the github-collector README for more examples.
PRs include GitHub labels in the labels field (list of {name, color, description} objects). The enriched label_names field extracts just the name strings for easy querying:
prplot> bar label_names
prplot> identify label_names CONTAINS 'bug'Plot Types:
hist field→ Histograms & distributionsplot x vs y→ Interactive scatter plots with clickable pointsbar category→ Bar charts & breakdownstrend time [by group]→ Time series analysisstats field [by group]→ Statistical summariesidentify WHERE condition→ Find specific PRs in tables
Filtering:
WHERE state = 'open'→ SQL-style conditionsWHERE age_days > 90 AND comment_count > 5→ Multiple conditionsWHERE label_names CONTAINS 'bug'→ Text searchWHERE author LIKE '%spring%'→ Pattern matchingWHERE author NOT IN ('alice', 'bob')→ Negated operatorsWHERE created_at_dt > now-30d→ Relative date filtering
Investigation:
- Click any plot point → See PR# and details in popup
identify comments > 10→ Show matching PRs in rich tables with clickable URLsexport WHERE condition TO file.json→ Save filtered data
prplot> fields
# Shows all available columns: age_days, state, comment_count, label_names, etc.# How old are the PRs?
prplot> hist age_days
# What's the mix of open vs closed?
prplot> bar state
# Which authors are most active?
prplot> bar author# PRs with lots of activity
prplot> hist comment_count where comment_count > 5
# Older PRs only
prplot> hist age_days where age_days > 90
# PRs created in the last 30 days
prplot> hist age_days where created_at_dt > now-30d
# High-activity old PRs
prplot> bar author where age_days > 90 and comment_count > 3# Do older PRs get more comments?
prplot> plot age_days vs comment_count
# Focus on active PRs
prplot> plot age_days vs comment_count where comment_count > 2
# Activity vs age patterns
prplot> plot activity_score vs age_days where age_days > 60# PR creation over time
prplot> trend created_at_dt
# Broken down by author
prplot> trend created_at_dt by author
# Focus on older PRs
prplot> trend created_at_dt where age_days > 90# Summary of comment activity
prplot> stats comment_count
# Broken down by state
prplot> stats comment_count by state
# Age analysis by complexity
prplot> stats age_days by complexity# Find high-activity PRs
prplot> identify comment_count > 10
# Find old open PRs
prplot> identify state = 'open' and age_days > 200
# Find recent activity
prplot> identify age_days < 30 and activity_score > 10
# Find PRs from specific contributors
prplot> identify author in ('sunyuhan1998', 'quaff', 'wilocu')
# Find PRs updated in the last week
prplot> identify updated_at_dt > now-7d
# Results shown in table with clickable "View PR" links# Save current plot
prplot> save pr_analysis.png
# Export filtered data
prplot> export where label_names CONTAINS 'MCP' to mcp_prs.json
# All old open PRs
prplot> export where state = 'open' and age_days > 180 to stale_prs.jsonprplot> hist age_days where state = 'open'
prplot> bar time_bucket where state = 'open'
prplot> export where state = 'open' and age_days > 365 to very_old_prs.jsonprplot> plot comment_count vs age_days where state = 'open'
prplot> hist activity_score where state = 'open'
prplot> stats comment_count by authorprplot> bar label_names
prplot> trend created_at_dt by author where age_days < 180
prplot> stats age_days by author where state = 'open'prplot> bar complexity
prplot> plot complexity vs comment_count where state = 'open'
prplot> stats age_days by complexity where state = 'open'| Command | Description | Example |
|---|---|---|
HIST field |
Histogram of field values | hist age_days |
PLOT x [VS y] |
Scatter plot or line plot | plot comments vs age_days |
TREND field [BY group] |
Time series trend | trend created_at_dt by author |
BAR field [BY group] |
Bar chart | bar author |
STATS field [BY group] |
Statistical summary | stats comments by state |
All plot commands support SQL-style WHERE clauses:
-- Equality and comparison
WHERE state = 'open'
WHERE age_days > 90
WHERE comment_count >= 5
-- Boolean operators
WHERE state = 'open' AND comment_count > 5
WHERE age_days > 180 OR activity_score > 10
-- String matching
WHERE author LIKE '%spring%'
WHERE label_names CONTAINS 'bug'
-- List membership (IN operator)
WHERE state IN ('open', 'closed')
WHERE author IN ('sunyuhan1998', 'quaff', 'wilocu')
-- Negated operators (NOT IN, NOT LIKE, NOT CONTAINS)
WHERE author NOT IN ('tzolov', 'markpollack')
WHERE author NOT LIKE '%bot%'
WHERE label_names NOT CONTAINS 'bug'
-- Relative dates (Grafana-style)
WHERE created_at_dt > now-30d
WHERE updated_at_dt < now-6M
-- Absolute dates (quoted ISO strings)
WHERE created_at_dt > '2025-01-01'
WHERE created_at_dt > '2025-06-15T00:00:00'prplot supports Grafana-style relative date literals for filtering by date fields (created_at_dt, updated_at_dt, closed_at_dt). No quotes needed.
Relative dates (now-<amount><unit>):
| Unit | Meaning | Example |
|---|---|---|
d |
days | now-30d — 30 days ago |
w |
weeks | now-2w — 2 weeks ago |
M |
months | now-6M — 6 months ago |
y |
years | now-1y — 1 year ago |
# PRs created in the last 30 days
prplot> hist age_days where created_at_dt > now-30d
# PRs not updated in 6+ months
prplot> identify updated_at_dt < now-6M
# PRs from the last year
prplot> trend created_at_dt where created_at_dt > now-1yAbsolute dates (quoted ISO strings):
prplot> identify created_at_dt > '2025-01-01'
prplot> hist age_days where created_at_dt > '2025-06-15'| Command | Description |
|---|---|
fields |
Show all available fields with types |
help |
Show command help |
source <file> |
Execute commands from a .prplot file |
save filename.png |
Save current plot to file |
export WHERE ... TO file.json |
Export filtered data |
quit / exit |
Exit the CLI |
The tool automatically enriches your PR data with computed fields:
age_days- Days since PR creationdays_since_update- Days since last updatetime_bucket- Categorized age ("< 1 month", "1-3 months", etc.)created_year,created_month,created_week- Date components
body_length- Length of PR descriptioncomplexity- Estimated complexity ("low", "medium", "high")label_count- Number of GitHub labels
label_names- List of GitHub label name strings
comment_count- Number of commentsactivity_score- Same as comment_countauthor- PR author loginauthor_login- PR author login (alias)
prplot> # Start with basic exploration
prplot> fields
[Shows all available fields with types]
prplot> # Look at PR age distribution
prplot> hist age_days
[Histogram showing distribution of PR ages]
prplot> # Focus on open PRs only
prplot> hist age_days where state = 'open'
[Histogram of open PR ages]
prplot> # Correlation analysis
prplot> plot comment_count vs age_days where state = 'open'
[Scatter plot with trend line]
prplot> # Time trends by author
prplot> trend created_at_dt by author
[Multi-line time series plot]
prplot> # Export data for further analysis
prplot> export where label_names CONTAINS 'MCP' to mcp_prs.json
Exported 23 PRs to mcp_prs.json
prplot> # Save visualization
prplot> save pr_age_analysis.png
Plot saved to pr_age_analysis.pngThe tool expects JSON files with the structure:
{
"prs": [
{
"number": 4396,
"title": "fix: mcp server registration tools failed",
"state": "open",
"created_at": "2025-09-15T16:43:43Z",
"author": {"login": "username", "name": "Full Name"},
"comments": [{"author": {"login": "..."}, "body": "...", "created_at": "..."}],
"labels": [{"name": "bug", "color": "d73a4a", "description": "..."}]
// ... other fields
}
]
}- Field names, commands, and operators auto-complete
- Nested field completion: Type a dict column name +
.for subfield suggestions - Context-aware suggestions based on field types
- Sample values for categorical fields
- Smart completion for nested objects
- Previous commands saved across sessions
- Use arrow keys to navigate history
- History stored in
~/.prplot_history
Save frequently-used cuts and commands in .prplot files and load them with source:
prplot> source cuts/spring-ai-core.prplot
Sourced 2 commands from cuts/spring-ai-core.prplot
prplot> source cuts/spring-ai-triage.prplot
Sourced 6 commands from cuts/spring-ai-triage.prplot
prplot> identify $core AND $freshLines starting with # are comments. Blank lines are skipped. Files can source other files.
Auto-load a file on startup with --init:
python -m prplot --init cuts/spring-ai-all.prplot data/spring-ai-open-prs.jsonThe cuts/ directory ships with ready-to-use cut definitions for Spring AI:
| File | Contents |
|---|---|
cuts/spring-ai-core.prplot |
Core team vs external contributor cuts |
cuts/spring-ai-triage.prplot |
Time-based (fresh/stale/ancient) and activity cuts |
cuts/spring-ai-all.prplot |
Sources both of the above |
- Numeric fields → histograms or scatter plots
- Categorical fields → bar charts
- DateTime fields → trend lines
- Mixed types → appropriate visualizations
- Automatic trend lines for scatter plots
- Summary statistics in histogram text boxes
- Value labels on bar charts
- Data is loaded once at startup and cached in memory
- Pandas DataFrames used for fast filtering and aggregation
- Optimized data types for memory efficiency
- Field indexing for quick lookups
# Basic age distribution
hist age_days
# Comment activity distribution
hist comment_count
# Filter for active PRs only
hist age_days where comment_count > 5
# Focus on older PRs
hist comment_count where age_days > 90 and comment_count > 0# Age vs comment correlation
plot age_days vs comment_count
# Activity score over time
plot age_days vs activity_score
# High-activity subset (click points to see PR#!)
plot age_days vs comment_count where comment_count > 5
# PRs created in the last 6 months
plot age_days vs comment_count where created_at_dt > now-6M# Author distribution
bar author
# Age category breakdown
bar time_bucket
# Authors for active PRs only
bar author where comment_count > 3
# Complexity distribution for older PRs
bar complexity where age_days > 90# PR creation over time
trend created_at_dt
# Creation trends by author
trend created_at_dt by author
# Focus on recent activity
trend created_week where age_days < 120
# Monthly patterns by complexity
trend created_month by complexity where age_days > 30# Overall comment statistics
stats comment_count
# Age breakdown by complexity
stats age_days by complexity
# Activity analysis by author
stats activity_score by author
# Comment patterns for older PRs
stats comment_count by time_bucket where age_days > 60# High-activity PRs
identify comment_count > 10
# Old PRs with ongoing discussion
identify age_days > 200 and comment_count > 3
# Recent high-engagement PRs
identify age_days < 60 and activity_score > 15
# Label-specific investigation
identify label_names contains 'bug' and comment_count > 2
# Alternative WHERE syntax (both forms work identically)
identify age_days where age_days > 90 and comments > 5
# Boolean field queries (case-insensitive)
identify soft_approval_detected = true
identify is_draft = false
# Multiple value matching with IN operator
identify author in ('sunyuhan1998', 'quaff', 'wilocu')
identify state in ('open', 'closed')
# Date-based filtering
identify created_at_dt > now-30d
identify updated_at_dt < now-6M# Export high-activity PRs
export where comment_count > 5 to active_prs.json
# Export old PRs with discussion
export where age_days > 180 and comment_count > 2 to old_active_prs.json
# Export recent PRs
export where created_at_dt > now-30d to recent_prs.json
# Save current plot
save correlation_analysis.png# Type and press TAB for completions:
plot au<TAB> # → author, author_login
hist com<TAB> # → comment_count, comments, complexityComputed Fields Available:
age_days,days_since_update- Time calculationstime_bucket- "< 1 month", "1-3 months", etc.complexity- "low", "medium", "high"comment_count- Number of commentsactivity_score- Same as comment_countlabel_count- Number of labelslabel_names- List of label name stringsauthor- PR author login
pandas- Data manipulationmatplotlib/seaborn- Plottingpyparsing- Query parsingprompt-toolkit- Interactive CLIrich- Beautiful terminal output
MIT License - see LICENSE file for details.