Skip to content

Commit 63c813c

Browse files
authored
Merge pull request #32 from vectordotdev/feat/unique-contributors-presentation
Refresh unique-contributors chart for presentation use
2 parents e9b4bb9 + 396a1e1 commit 63c813c

10 files changed

Lines changed: 223 additions & 36 deletions
-29.2 KB
Loading
49.2 KB
Loading
-29.6 KB
Loading
52.7 KB
Loading
-27.7 KB
Loading
39.1 KB
Loading

scripts/util/plot.py

Lines changed: 171 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import hashlib
33
import logging
44
import os
5+
from pathlib import Path
56

67
import matplotlib.pyplot as plt
78
import numpy as np
@@ -249,6 +250,14 @@ def main():
249250
output_path = os.path.join(OUTPUT_DIR, f"{prefix}.unique_contributors.png")
250251
plot_unique_contributors(contributor_csv, table, output_path)
251252

253+
output_path = os.path.join(OUTPUT_DIR, f"{prefix}.unique_contributors_yearly.png")
254+
plot_yearly_contributors(contributor_csv, table, output_path)
255+
256+
# Same data as the yearly chart, also written as a markdown
257+
# table into trends/{repo}.md between AUTO markers.
258+
trends_md = Path(SCRIPT_DIR).resolve().parents[1] / "trends" / f"{env['REPO_NAME']}.md"
259+
update_yearly_contributors_table(contributor_csv, table, trends_md)
260+
252261
# Discussion trends
253262
disc_prefix = f"{env['REPO_OWNER']}_{env['REPO_NAME']}_discussions"
254263
disc_csv = os.path.join(args.input_dir, f"{disc_prefix}.monthly_summary.csv")
@@ -686,20 +695,7 @@ def plot_unique_contributors(path, table, output_path, window_months=12):
686695
new_counts = per_month.get(True, pd.Series(0, index=window))
687696
returning_counts = per_month.get(False, pd.Series(0, index=window))
688697

689-
def window_stats(n_months):
690-
recent = window[-n_months:]
691-
sub = df_window[df_window["month"].isin(recent)]
692-
total = sub["user_login"].nunique()
693-
new_users = sub.loc[sub["is_new"], "user_login"].nunique()
694-
return total, new_users, total - new_users
695-
696-
s12 = window_stats(12)
697-
s6 = window_stats(6)
698-
s1 = window_stats(1)
699-
700-
fig, (ax, ax_stats) = plt.subplots(
701-
1, 2, figsize=(14, 6), gridspec_kw={"width_ratios": [3.2, 1]}
702-
)
698+
fig, ax = plt.subplots(figsize=(12, 6))
703699

704700
x = np.arange(len(window))
705701
ax.bar(x, returning_counts.values, color="#8E5CE6", label="Returning")
@@ -715,39 +711,178 @@ def window_stats(n_months):
715711
ax.set_xticklabels(window, rotation=45, ha="right")
716712
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
717713
set_axis_labels(ax, "Month", "Unique contributors")
718-
ax.set_title(f"Unique {pretty_table(table)} contributors (last {len(window)} months)", fontsize=16)
714+
ax.set_title(
715+
f"Unique {pretty_table(table)} contributors (last {len(window)} months)",
716+
fontsize=16,
717+
)
719718
ax.legend(loc="upper left")
720719

721-
ax_stats.axis("off")
722-
ax_stats.set_title("Window totals", fontsize=14)
720+
plt.tight_layout()
721+
plt.savefig(output_path)
722+
logging.info(f"Saved plot to {output_path}")
723+
plt.close()
724+
except Exception as e:
725+
logging.warning(f"[{table}] Could not generate unique-contributors plot: {e}")
723726

724-
def fmt(label, stats):
725-
total, new_users, returning_users = stats
726-
return (
727-
f"{label}\n"
728-
f" total: {total}\n"
729-
f" new: {new_users}\n"
730-
f" returning: {returning_users}\n"
731-
)
732727

733-
text = "\n".join([
734-
fmt("Last 12 months", s12),
735-
fmt("Last 6 months", s6),
736-
fmt("Last month", s1),
737-
])
738-
ax_stats.text(
739-
0.0, 0.95, text,
740-
ha="left", va="top",
741-
family="monospace", fontsize=11,
742-
transform=ax_stats.transAxes,
728+
def _yearly_contributor_rows(path):
729+
"""Compute per-year (year, label, total, new, returning, partial) rows
730+
from contributor_monthly.csv. Excludes bots. `partial` is True when
731+
the year hasn't fully elapsed yet (typically just the current year)."""
732+
df = pd.read_csv(path)
733+
if df.empty:
734+
return []
735+
df = df[~df["user_login"].map(is_bot_login)]
736+
df = df.dropna(subset=["month", "user_login"])
737+
if df.empty:
738+
return []
739+
740+
df["month"] = df["month"].astype(str)
741+
df["year"] = df["month"].str[:4]
742+
first_year_by_user = df.groupby("user_login")["month"].min().str[:4]
743+
744+
today_year = pd.Timestamp.utcnow().year
745+
rows = []
746+
for year, group in df.groupby("year"):
747+
users = group["user_login"].unique()
748+
new_c = sum(first_year_by_user[u] == year for u in users)
749+
total = len(users)
750+
months_seen = group["month"].nunique()
751+
partial = int(year) == today_year and months_seen < 12
752+
label = f"{year} (YTD, {months_seen}mo)" if partial else f"{year}"
753+
rows.append((year, label, total, new_c, total - new_c, partial))
754+
rows.sort(key=lambda r: r[0])
755+
return rows
756+
757+
758+
def plot_yearly_contributors(path, table, output_path, max_years=6):
759+
"""Horizontal stacked bars: one row per year (last `max_years`),
760+
split into new (green) vs returning (purple), with totals annotated."""
761+
try:
762+
rows = _yearly_contributor_rows(path)
763+
if not rows:
764+
return
765+
rows = rows[-max_years:]
766+
767+
labels = [r[1] for r in rows]
768+
new_vals = np.array([r[3] for r in rows])
769+
ret_vals = np.array([r[4] for r in rows])
770+
partials = [r[5] for r in rows]
771+
totals = new_vals + ret_vals
772+
773+
fig, ax = plt.subplots(figsize=(10, max(3.5, 0.7 * len(rows) + 1.5)))
774+
y = np.arange(len(labels))
775+
# Per-row alpha: partial (YTD) years are muted to avoid suggesting
776+
# they're directly comparable to full years.
777+
alphas = [0.35 if p else 1.0 for p in partials]
778+
# Squared edges — rounding stacked segments leaves gaps at the
779+
# junction.
780+
for i, a in enumerate(alphas):
781+
ax.barh(y[i], ret_vals[i], color="#8E5CE6", height=0.55, alpha=a)
782+
ax.barh(y[i], new_vals[i], left=ret_vals[i], color="#36B37E",
783+
height=0.55, alpha=a)
784+
785+
x_pad = max(totals.max() * 0.015, 0.5)
786+
for i, (n, r, t, p) in enumerate(zip(new_vals, ret_vals, totals, partials)):
787+
text_color_inner = "white"
788+
text_color_outer = DARK
789+
if p:
790+
# Inner labels become dark grey on muted bars so they stay
791+
# readable against the lighter fill.
792+
text_color_inner = "#555555"
793+
text_color_outer = "#555555"
794+
if r > 0:
795+
ax.text(r / 2, i, str(int(r)), ha="center", va="center",
796+
color=text_color_inner, fontsize=12, fontweight="bold")
797+
if n > 0:
798+
ax.text(r + n / 2, i, str(int(n)), ha="center", va="center",
799+
color=text_color_inner, fontsize=12, fontweight="bold")
800+
ax.text(t + x_pad, i, f"{int(t)} total",
801+
va="center", fontsize=12, color=text_color_outer,
802+
fontweight="bold",
803+
fontstyle="italic" if p else "normal")
804+
805+
ax.set_yticks(y)
806+
ax.set_yticklabels(labels, fontsize=12)
807+
for tick, p in zip(ax.get_yticklabels(), partials):
808+
if p:
809+
tick.set_fontstyle("italic")
810+
tick.set_color("#555555")
811+
ax.invert_yaxis()
812+
ax.set_xlim(right=totals.max() * 1.28)
813+
814+
ax.set_xticks([])
815+
ax.xaxis.set_visible(False)
816+
for spine in ("top", "right", "bottom"):
817+
ax.spines[spine].set_visible(False)
818+
ax.grid(False)
819+
820+
adj = {"pull_requests": "PR", "issues": "Issue", "discussions": "Discussion"}.get(table, table)
821+
ax.set_title(
822+
f"Unique {adj} contributors by year",
823+
fontsize=18, pad=14,
824+
)
825+
from matplotlib.patches import Patch
826+
ax.legend(
827+
handles=[
828+
Patch(facecolor="#8E5CE6", label="Returning"),
829+
Patch(facecolor="#36B37E", label="New"),
830+
],
831+
loc="lower right", frameon=False,
743832
)
744833

745834
plt.tight_layout()
746835
plt.savefig(output_path)
747836
logging.info(f"Saved plot to {output_path}")
748837
plt.close()
749838
except Exception as e:
750-
logging.warning(f"[{table}] Could not generate unique-contributors plot: {e}")
839+
logging.warning(f"[{table}] Could not generate yearly contributors chart: {e}")
840+
841+
842+
def update_yearly_contributors_table(path, table, trends_md_path):
843+
"""Compute per-year unique-contributor stats (total / new / returning)
844+
and rewrite the section in trends/{repo}.md between the markers
845+
<!-- AUTO:yearly-contributors:start --> and ...:end -->."""
846+
try:
847+
rows = _yearly_contributor_rows(path)
848+
if not rows:
849+
return
850+
851+
lines = [
852+
"| Year | Unique | New | Returning |",
853+
"|------|--------|-----|-----------|",
854+
]
855+
for _, label, total, new_c, ret_c, _partial in rows:
856+
lines.append(f"| {label} | {total} | {new_c} | {ret_c} |")
857+
table_md = "\n".join(lines)
858+
859+
marker_start = "<!-- AUTO:yearly-contributors:start -->"
860+
marker_end = "<!-- AUTO:yearly-contributors:end -->"
861+
862+
if not trends_md_path.exists():
863+
logging.warning(
864+
f"[{table}] Trends file not found, skipping yearly table: {trends_md_path}"
865+
)
866+
return
867+
content = trends_md_path.read_text()
868+
if marker_start not in content or marker_end not in content:
869+
logging.warning(
870+
f"[{table}] Markers not found in {trends_md_path}; "
871+
f"add a section bracketed by {marker_start} / {marker_end}"
872+
)
873+
return
874+
875+
import re
876+
pattern = re.compile(
877+
re.escape(marker_start) + r".*?" + re.escape(marker_end),
878+
re.DOTALL,
879+
)
880+
replacement = f"{marker_start}\n\n{table_md}\n\n{marker_end}"
881+
content = pattern.sub(replacement, content)
882+
trends_md_path.write_text(content)
883+
logging.info(f"Updated yearly-contributors table in {trends_md_path}")
884+
except Exception as e:
885+
logging.warning(f"[{table}] Could not update yearly-contributors table: {e}")
751886

752887

753888
if __name__ == "__main__":

trends/quickwit.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@
4646

4747
![Unique PR Contributors (new vs returning)](../data/images/quickwit-oss_quickwit_pull_requests.unique_contributors.png)
4848

49+
![Unique PR Contributors by year](../data/images/quickwit-oss_quickwit_pull_requests.unique_contributors_yearly.png)
50+
51+
**Unique PR contributors by year**
52+
53+
<!-- AUTO:yearly-contributors:start -->
54+
55+
| Year | Unique | New | Returning |
56+
|------|--------|-----|-----------|
57+
| 2021 | 12 | 12 | 0 |
58+
| 2022 | 30 | 24 | 6 |
59+
| 2023 | 38 | 27 | 11 |
60+
| 2024 | 36 | 27 | 9 |
61+
| 2025 | 30 | 22 | 8 |
62+
| 2026 (YTD, 4mo) | 27 | 13 | 14 |
63+
64+
<!-- AUTO:yearly-contributors:end -->
65+
4966
*Draft PRs and known bot accounts excluded.*
5067

5168
## Discussions

trends/vector.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@
6666

6767
![Unique PR Contributors (new vs returning)](../data/images/vectordotdev_vector_pull_requests.unique_contributors.png)
6868

69+
![Unique PR Contributors by year](../data/images/vectordotdev_vector_pull_requests.unique_contributors_yearly.png)
70+
71+
**Unique PR contributors by year**
72+
73+
<!-- AUTO:yearly-contributors:start -->
74+
75+
| Year | Unique | New | Returning |
76+
|------|--------|-----|-----------|
77+
| 2018 | 2 | 2 | 0 |
78+
| 2019 | 45 | 43 | 2 |
79+
| 2020 | 87 | 74 | 13 |
80+
| 2021 | 123 | 94 | 29 |
81+
| 2022 | 142 | 109 | 33 |
82+
| 2023 | 153 | 110 | 43 |
83+
| 2024 | 177 | 130 | 47 |
84+
| 2025 | 191 | 144 | 47 |
85+
| 2026 (YTD, 4mo) | 104 | 68 | 36 |
86+
87+
<!-- AUTO:yearly-contributors:end -->
88+
6989
*Draft PRs and known bot accounts excluded.*
7090

7191
## Discussions

trends/vrl.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@
4646

4747
![Unique PR Contributors (new vs returning)](../data/images/vectordotdev_vrl_pull_requests.unique_contributors.png)
4848

49+
![Unique PR Contributors by year](../data/images/vectordotdev_vrl_pull_requests.unique_contributors_yearly.png)
50+
51+
**Unique PR contributors by year**
52+
53+
<!-- AUTO:yearly-contributors:start -->
54+
55+
| Year | Unique | New | Returning |
56+
|------|--------|-----|-----------|
57+
| 2023 | 26 | 26 | 0 |
58+
| 2024 | 30 | 26 | 4 |
59+
| 2025 | 29 | 23 | 6 |
60+
| 2026 (YTD, 4mo) | 12 | 9 | 3 |
61+
62+
<!-- AUTO:yearly-contributors:end -->
63+
4964
*Draft PRs and known bot accounts excluded.*
5065

5166
## Discussions

0 commit comments

Comments
 (0)