Files
Sylvestre Ledru 184cca774e Track commits and contributors for GNU and uutils coreutils
Add activity_count.py, which walks both git histories and records, per
month since 2021: non-merge commits, active contributors and cumulative
distinct contributors. Authors are deduplicated via mailmap and bots are
excluded. The cumulative count covers each project's full history, not
just the reported range.

activity-graph.py renders the three metrics as stacked panels sharing an
x-axis, in the same style as the other graphs.

The CI job checks out both histories (blobless, full depth), regenerates
the data and commits the refreshed graph, like the other trackers.

Also fix the lint errors the latest ruff reports on the existing files,
which were failing the lint job.
2026-08-02 23:54:00 +02:00

143 lines
3.6 KiB
Python

# This file is part of the uutils coreutils package.
#
# For the full copyright and license information, please view the LICENSE
# file that was distributed with this source code.
"""Render activity-result.json with three stacked panels comparing GNU and
uutils coreutils: commits per month, active contributors per month and
cumulative distinct contributors. All panels share an x-axis so trends line
up vertically.
"""
import sys
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from graph_common import (
add_gnu_release_markers,
apply_smoothing,
setup_theme,
style_axes,
style_legend,
)
if len(sys.argv) <= 1:
print("activity-graph.py: <json file>")
sys.exit()
raw = pd.read_json(sys.argv[1], orient="index", convert_dates=False)
# Flatten {"2021-01": {"gnu": {...}, "uutils": {...}}} into one row per
# (month, project).
rows = []
for month, projects in raw.iterrows():
for project, values in projects.items():
rows.append(
{
"date": pd.to_datetime(month, format="%Y-%m", utc=True),
"project": project,
**{k: int(v) for k, v in values.items()},
}
)
df = pd.DataFrame(rows).sort_values("date")
print(df)
setup_theme()
palette = {
"gnu": "#0066CC",
"uutils": "#10B981",
}
label_map = {
"gnu": "GNU coreutils",
"uutils": "uutils coreutils",
}
def plot_panel(ax, metric, ylabel, smooth=True):
"""Plot one metric for both projects on `ax`."""
data = df[["date", "project", metric]].copy()
if smooth:
# 3-month rolling mean: monthly counts are spiky (release crunches,
# hackathons) and the trend is what matters here.
data["value"] = apply_smoothing(data, "project", metric, window=3)
else:
data["value"] = data[metric]
sns.lineplot(
data=data,
x="date",
y="value",
hue="project",
palette=palette,
hue_order=["gnu", "uutils"],
linewidth=3,
ax=ax,
markers=False,
dashes=False,
alpha=1,
zorder=3,
)
style_axes(ax, xlabel="Date", ylabel=ylabel)
y_max = data["value"].max()
add_gnu_release_markers(ax, df["date"].min(), df["date"].max(), y_max)
handles, labels = ax.get_legend_handles_labels()
labels = [label_map.get(label, label) for label in labels]
style_legend(ax, handles, labels, ncol=2, loc="upper left")
fig, (ax_top, ax_mid, ax_bot) = plt.subplots(
3, 1, figsize=(18, 20), dpi=100, sharex=True
)
plot_panel(ax_top, "commits", "Commits / month")
plot_panel(ax_mid, "authors", "Active contributors / month")
plot_panel(ax_bot, "cumulative_authors", "Distinct contributors (since day 1)", False)
fig.suptitle(
"GNU vs uutils coreutils — Development Activity Since 2021",
fontsize=26,
fontweight="bold",
color="#1a1a1a",
y=0.995,
)
fig.text(
0.5,
0.972,
"Non-merge commits, bots excluded. Top two panels are 3-month rolling averages; "
"the cumulative panel counts every contributor since each project's first commit.",
ha="center",
va="top",
fontsize=13,
color="#6B7280",
style="italic",
alpha=0.9,
)
# Hide the upper panels' x-tick labels — they duplicate the bottom panel's.
for ax in (ax_top, ax_mid):
plt.setp(ax.get_xticklabels(), visible=False)
ax.set_xlabel("")
plt.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig(
"activity-results.svg",
format="svg",
dpi=300,
bbox_inches="tight",
facecolor="white",
edgecolor="none",
metadata={
"Creator": "uutils coreutils tracking",
"Title": "Development Activity Evolution",
},
)