2026-01-07 23:03:00 +01:00
|
|
|
# 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.
|
|
|
|
|
|
|
|
|
|
"""Common styling and utilities for graph generation."""
|
|
|
|
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
import seaborn as sns
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Modern vibrant color palette
|
|
|
|
|
COLORS = {
|
2026-01-07 23:15:37 +01:00
|
|
|
"total": "#0066CC", # Vibrant blue
|
|
|
|
|
"pass": "#10B981", # Modern green (Tailwind emerald)
|
|
|
|
|
"fail": "#EF4444", # Modern red (Tailwind red)
|
|
|
|
|
"error": "#F59E0B", # Modern amber
|
|
|
|
|
"skip": "#8B5CF6", # Modern purple (Tailwind violet)
|
|
|
|
|
"size": "#6366F1", # Modern indigo
|
|
|
|
|
"multisize": "#10B981", # Modern green (Tailwind emerald)
|
|
|
|
|
"default": "#10B981", # Modern green (Tailwind emerald)
|
2026-01-07 23:03:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_theme():
|
|
|
|
|
"""Set up modern Seaborn theme with enhanced settings."""
|
|
|
|
|
sns.set_theme(style="ticks", context="talk", palette="muted")
|
2026-01-07 23:15:37 +01:00
|
|
|
plt.rcParams.update(
|
|
|
|
|
{
|
|
|
|
|
"font.family": "sans-serif",
|
2026-01-07 23:21:30 +01:00
|
|
|
"font.sans-serif": [
|
|
|
|
|
"Inter",
|
|
|
|
|
"SF Pro Display",
|
|
|
|
|
"Segoe UI",
|
|
|
|
|
"DejaVu Sans",
|
|
|
|
|
"Arial",
|
|
|
|
|
],
|
2026-01-07 23:15:37 +01:00
|
|
|
"axes.facecolor": "#FAFAFA",
|
|
|
|
|
"figure.facecolor": "white",
|
|
|
|
|
"axes.edgecolor": "#CCCCCC",
|
|
|
|
|
"axes.linewidth": 0.8,
|
|
|
|
|
"xtick.color": "#555555",
|
|
|
|
|
"ytick.color": "#555555",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-01-07 23:03:00 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_smoothing(df, group_col, value_col, window=15):
|
|
|
|
|
"""Apply rolling average smoothing to data.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
df: DataFrame with data to smooth
|
|
|
|
|
group_col: Column name to group by (or None for no grouping)
|
|
|
|
|
value_col: Column name containing values to smooth
|
|
|
|
|
window: Rolling window size (default: 15)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Series with smoothed values
|
|
|
|
|
"""
|
|
|
|
|
if group_col:
|
|
|
|
|
return df.groupby(group_col)[value_col].transform(
|
|
|
|
|
lambda x: x.rolling(window=window, min_periods=1, center=True).mean()
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
return df[value_col].rolling(window=window, min_periods=1, center=True).mean()
|
|
|
|
|
|
|
|
|
|
|
2026-01-07 23:15:37 +01:00
|
|
|
def style_axes(ax, xlabel="Date", ylabel="Value"):
|
2026-01-07 23:03:00 +01:00
|
|
|
"""Apply modern styling to axes.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ax: Matplotlib axes object
|
|
|
|
|
xlabel: Label for x-axis
|
|
|
|
|
ylabel: Label for y-axis
|
|
|
|
|
"""
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.set_xlabel(xlabel, fontsize=15, fontweight="600", labelpad=15, color="#374151")
|
|
|
|
|
ax.set_ylabel(ylabel, fontsize=15, fontweight="600", labelpad=15, color="#374151")
|
2026-01-07 23:03:00 +01:00
|
|
|
|
|
|
|
|
# Modern grid styling
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.grid(
|
|
|
|
|
True,
|
|
|
|
|
alpha=0.35,
|
|
|
|
|
linestyle="-",
|
|
|
|
|
linewidth=1,
|
|
|
|
|
color="#E5E7EB",
|
|
|
|
|
which="major",
|
|
|
|
|
zorder=0,
|
|
|
|
|
)
|
|
|
|
|
ax.grid(
|
|
|
|
|
True,
|
|
|
|
|
alpha=0.15,
|
|
|
|
|
linestyle=":",
|
|
|
|
|
linewidth=0.5,
|
|
|
|
|
color="#F3F4F6",
|
|
|
|
|
which="minor",
|
|
|
|
|
zorder=0,
|
|
|
|
|
)
|
2026-01-07 23:03:00 +01:00
|
|
|
ax.set_axisbelow(True)
|
|
|
|
|
ax.minorticks_on()
|
|
|
|
|
|
|
|
|
|
# Format x-axis dates
|
2026-01-07 23:15:37 +01:00
|
|
|
plt.xticks(rotation=45, ha="right")
|
2026-01-07 23:03:00 +01:00
|
|
|
ax.margins(x=0.01)
|
|
|
|
|
ax.set_ylim(bottom=0)
|
|
|
|
|
|
|
|
|
|
# Modern spine styling
|
|
|
|
|
sns.despine(ax=ax, top=True, right=True, left=False, bottom=False, offset=8)
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.spines["left"].set_linewidth(2)
|
|
|
|
|
ax.spines["bottom"].set_linewidth(2)
|
|
|
|
|
ax.spines["left"].set_color("#9CA3AF")
|
|
|
|
|
ax.spines["bottom"].set_color("#9CA3AF")
|
2026-01-07 23:03:00 +01:00
|
|
|
|
|
|
|
|
# Modern background
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.patch.set_facecolor("#FAFAFA")
|
2026-01-07 23:03:00 +01:00
|
|
|
ax.patch.set_alpha(0.6)
|
|
|
|
|
|
|
|
|
|
# Add subtle shadow effect
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.add_patch(
|
|
|
|
|
plt.Rectangle(
|
|
|
|
|
(0, 0),
|
|
|
|
|
1,
|
|
|
|
|
1,
|
|
|
|
|
transform=ax.transAxes,
|
|
|
|
|
facecolor="none",
|
|
|
|
|
edgecolor="#E5E7EB",
|
|
|
|
|
linewidth=3,
|
|
|
|
|
zorder=-1,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-01-07 23:03:00 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_title(ax, title, subtitle=None):
|
|
|
|
|
"""Add modern title and optional subtitle to axes.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ax: Matplotlib axes object
|
|
|
|
|
title: Main title text
|
|
|
|
|
subtitle: Optional subtitle text
|
|
|
|
|
"""
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.text(
|
|
|
|
|
0.5,
|
|
|
|
|
1.12,
|
|
|
|
|
title,
|
|
|
|
|
ha="center",
|
|
|
|
|
va="bottom",
|
|
|
|
|
transform=ax.transAxes,
|
|
|
|
|
fontsize=26,
|
|
|
|
|
fontweight="bold",
|
|
|
|
|
family="sans-serif",
|
|
|
|
|
color="#1a1a1a",
|
|
|
|
|
)
|
2026-01-07 23:03:00 +01:00
|
|
|
|
|
|
|
|
if subtitle:
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.text(
|
|
|
|
|
0.5,
|
|
|
|
|
1.06,
|
|
|
|
|
subtitle,
|
|
|
|
|
ha="center",
|
|
|
|
|
va="bottom",
|
|
|
|
|
transform=ax.transAxes,
|
|
|
|
|
fontsize=13,
|
|
|
|
|
color="#6B7280",
|
|
|
|
|
style="italic",
|
|
|
|
|
alpha=0.9,
|
|
|
|
|
)
|
2026-01-07 23:03:00 +01:00
|
|
|
|
|
|
|
|
|
2026-01-07 23:15:37 +01:00
|
|
|
def style_legend(ax, handles, labels, ncol=1, loc="upper left"):
|
2026-01-07 23:03:00 +01:00
|
|
|
"""Apply modern styling to legend.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ax: Matplotlib axes object
|
|
|
|
|
handles: Legend handles
|
|
|
|
|
labels: Legend labels
|
|
|
|
|
ncol: Number of columns (default: 1)
|
|
|
|
|
loc: Legend location (default: 'upper left')
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Legend object
|
|
|
|
|
"""
|
2026-01-07 23:15:37 +01:00
|
|
|
legend = ax.legend(
|
|
|
|
|
handles,
|
|
|
|
|
labels,
|
|
|
|
|
loc=loc,
|
|
|
|
|
frameon=True,
|
|
|
|
|
fancybox=False,
|
|
|
|
|
shadow=False,
|
|
|
|
|
ncol=ncol,
|
|
|
|
|
bbox_to_anchor=(0, 1.04) if "left" in loc else (1, 1.04),
|
|
|
|
|
fontsize=13,
|
|
|
|
|
edgecolor="#D1D5DB",
|
|
|
|
|
framealpha=0.98,
|
|
|
|
|
borderpad=1.2,
|
|
|
|
|
labelspacing=0.8,
|
|
|
|
|
columnspacing=2,
|
|
|
|
|
)
|
|
|
|
|
legend.get_frame().set_facecolor("#FFFFFF")
|
2026-01-07 23:03:00 +01:00
|
|
|
legend.get_frame().set_linewidth(2)
|
2026-01-07 23:15:37 +01:00
|
|
|
legend.get_frame().set_boxstyle("round,pad=0.5")
|
2026-01-07 23:03:00 +01:00
|
|
|
return legend
|
|
|
|
|
|
|
|
|
|
|
2026-05-01 17:46:36 +02:00
|
|
|
GNU_COREUTILS_RELEASES = [
|
|
|
|
|
("9.0", "2021-09-24"),
|
|
|
|
|
("9.1", "2022-04-15"),
|
|
|
|
|
("9.2", "2023-03-20"),
|
|
|
|
|
("9.3", "2023-04-18"),
|
|
|
|
|
("9.4", "2023-08-29"),
|
|
|
|
|
("9.5", "2024-03-28"),
|
|
|
|
|
("9.6", "2025-01-17"),
|
|
|
|
|
("9.7", "2025-04-09"),
|
|
|
|
|
("9.8", "2025-09-22"),
|
|
|
|
|
("9.9", "2025-11-10"),
|
|
|
|
|
("9.10", "2026-02-04"),
|
|
|
|
|
("9.11", "2026-04-23"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_gnu_release_markers(ax, x_min, x_max, y_max, releases=GNU_COREUTILS_RELEASES):
|
|
|
|
|
"""Draw vertical bars at GNU coreutils release dates within the data range."""
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
x_min = pd.to_datetime(x_min, utc=True)
|
|
|
|
|
x_max = pd.to_datetime(x_max, utc=True)
|
|
|
|
|
for version, date_str in releases:
|
|
|
|
|
rel_date = pd.to_datetime(date_str, utc=True)
|
|
|
|
|
if x_min <= rel_date <= x_max:
|
|
|
|
|
ax.axvline(
|
|
|
|
|
x=rel_date,
|
|
|
|
|
color="#6B7280",
|
|
|
|
|
linestyle="--",
|
|
|
|
|
linewidth=1.2,
|
|
|
|
|
alpha=0.6,
|
|
|
|
|
zorder=2,
|
|
|
|
|
)
|
|
|
|
|
ax.text(
|
|
|
|
|
rel_date,
|
2026-05-01 18:07:49 +02:00
|
|
|
y_max * 0.02,
|
2026-05-01 17:46:36 +02:00
|
|
|
f"v{version}",
|
|
|
|
|
rotation=90,
|
|
|
|
|
fontsize=10,
|
|
|
|
|
color="#374151",
|
|
|
|
|
ha="right",
|
2026-05-01 18:07:49 +02:00
|
|
|
va="bottom",
|
2026-05-01 17:46:36 +02:00
|
|
|
alpha=0.85,
|
|
|
|
|
zorder=4,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-01-07 23:03:00 +01:00
|
|
|
def add_reference_lines(ax, y_max):
|
|
|
|
|
"""Add horizontal reference lines at key values.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ax: Matplotlib axes object
|
|
|
|
|
y_max: Maximum y value for calculating reference positions
|
|
|
|
|
"""
|
|
|
|
|
if y_max > 100:
|
2026-01-07 23:15:37 +01:00
|
|
|
ax.axhline(
|
|
|
|
|
y=y_max * 0.75,
|
|
|
|
|
color="#D1D5DB",
|
|
|
|
|
linestyle="--",
|
|
|
|
|
linewidth=0.8,
|
|
|
|
|
alpha=0.3,
|
|
|
|
|
zorder=0,
|
|
|
|
|
)
|
|
|
|
|
ax.axhline(
|
|
|
|
|
y=y_max * 0.5,
|
|
|
|
|
color="#D1D5DB",
|
|
|
|
|
linestyle="--",
|
|
|
|
|
linewidth=0.8,
|
|
|
|
|
alpha=0.3,
|
|
|
|
|
zorder=0,
|
|
|
|
|
)
|