@@ -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+
470538if __name__ == "__main__" :
471539 main ()
0 commit comments