Skip to content

Commit 21cc3c1

Browse files
authored
Merge pull request #19 from vectordotdev/feat/contributor-heatmap
Add top contributor heatmap (last 12 months, PRs)
2 parents 7b79e8a + 45ab867 commit 21cc3c1

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

scripts/util/plot.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ def main():
167167
exclude_labels=args.exclude_labels
168168
)
169169

170+
contributor_csv = os.path.join(args.input_dir, f"{prefix}.contributor_monthly.csv")
171+
if os.path.exists(contributor_csv):
172+
output_path = os.path.join(OUTPUT_DIR, f"{prefix}.contributors_top10_12m.png")
173+
plot_contributor_heatmap(contributor_csv, table, output_path)
174+
170175
# Discussion trends
171176
disc_prefix = f"{env['REPO_OWNER']}_{env['REPO_NAME']}_discussions"
172177
disc_csv = os.path.join(args.input_dir, f"{disc_prefix}.monthly_summary.csv")
@@ -467,5 +472,68 @@ def plot_label_state_counts(path, table, output_path, top_n, exclude_labels=None
467472
logging.warning(f"[{table}] Could not generate label count chart: {e}")
468473

469474

475+
def plot_contributor_heatmap(path, table, output_path, top_n=10, window_months=12):
476+
try:
477+
df = pd.read_csv(path)
478+
if df.empty:
479+
logging.warning(f"[{table}] Contributor CSV is empty: {path}")
480+
return
481+
482+
df = df[~df["user_login"].str.endswith("[bot]", na=False)]
483+
if df.empty:
484+
logging.warning(f"[{table}] No non-bot contributors in {path}")
485+
return
486+
487+
months_all = sorted(df["month"].dropna().unique())
488+
window = months_all[-window_months:]
489+
df = df[df["month"].isin(window)]
490+
if df.empty:
491+
logging.warning(f"[{table}] No contributor data in last {window_months} months")
492+
return
493+
494+
totals = df.groupby("user_login")["count"].sum().sort_values(ascending=False)
495+
top_users = totals.head(top_n).index.tolist()
496+
df = df[df["user_login"].isin(top_users)]
497+
498+
pivot = (
499+
df.pivot_table(index="user_login", columns="month", values="count", fill_value=0)
500+
.reindex(index=top_users, columns=window, fill_value=0)
501+
)
502+
503+
last_month = window[-1]
504+
pivot = pivot.assign(_total=totals.reindex(pivot.index).values)
505+
pivot = pivot.sort_values(by=[last_month, "_total"], ascending=False).drop(columns="_total")
506+
507+
fig, ax = plt.subplots(figsize=(12, 5))
508+
im = ax.imshow(pivot.values, aspect="auto", cmap="YlOrRd")
509+
510+
ax.set_xticks(np.arange(len(window)))
511+
ax.set_xticklabels(window, rotation=45, ha="right")
512+
ax.set_yticks(np.arange(len(pivot.index)))
513+
ax.set_yticklabels(pivot.index)
514+
515+
vmax = pivot.values.max() if pivot.values.size else 0
516+
for i in range(pivot.shape[0]):
517+
for j in range(pivot.shape[1]):
518+
v = pivot.values[i, j]
519+
if v > 0:
520+
color = "white" if v > vmax * 0.5 else "black"
521+
ax.text(j, i, int(v), ha="center", va="center", color=color, fontsize=9)
522+
523+
cbar = fig.colorbar(im, ax=ax)
524+
cbar.set_label(f"{table} opened")
525+
526+
ax.set_title(f"Top {top_n} {table} contributors (last {window_months} months)", fontsize=16)
527+
set_axis_labels(ax, "Month", "Contributor")
528+
ax.grid(False)
529+
530+
plt.tight_layout()
531+
plt.savefig(output_path)
532+
logging.info(f"Saved plot to {output_path}")
533+
plt.close()
534+
except Exception as e:
535+
logging.warning(f"[{table}] Could not generate contributor heatmap: {e}")
536+
537+
470538
if __name__ == "__main__":
471539
main()

src/commands/generate_summaries.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub fn run(db: &str, config: &Config, exclude_labels: Option<&str>) -> Result<()
2828
export_overall_totals(&conn, out_dir, config, table, exc)?;
2929
}
3030

31+
export_contributor_monthly(&conn, out_dir, config, "pull_requests", exc)?;
32+
3133
// Generate discussion summaries if the table exists
3234
let has_discussions: bool = conn
3335
.prepare("SELECT 1 FROM discussions LIMIT 1")
@@ -317,6 +319,31 @@ fn export_overall_totals(
317319
)
318320
}
319321

322+
fn export_contributor_monthly(
323+
conn: &Connection,
324+
out_dir: &Path,
325+
config: &Config,
326+
table: &str,
327+
exclude_labels: Option<&[String]>,
328+
) -> Result<()> {
329+
let (wc, params) = build_where(table, exclude_labels, &["user_login IS NOT NULL"]);
330+
let query = format!(
331+
"SELECT substr({table}.created_at, 1, 7) AS month,
332+
{table}.user_login AS user_login,
333+
COUNT(*) AS count
334+
FROM {table}
335+
{wc}
336+
GROUP BY month, user_login
337+
ORDER BY month, count DESC"
338+
);
339+
write_query_to_csv(
340+
conn,
341+
&query,
342+
&to_rusqlite_params(&params),
343+
&csv_path(out_dir, config, table, "contributor_monthly"),
344+
)
345+
}
346+
320347
fn export_discussion_monthly_summary(
321348
conn: &Connection,
322349
out_dir: &Path,

0 commit comments

Comments
 (0)