"""Graphiques sur la taille moyenne des sets (pièces par set).""" from pathlib import Path from typing import Dict, Iterable, List, Tuple import matplotlib.pyplot as plt from lib.filesystem import ensure_parent_dir from lib.milestones import load_milestones from lib.rebrickable.stats import read_rows def compute_average_parts_per_set(rows: Iterable[dict]) -> List[Tuple[int, float]]: """Calcule la moyenne annuelle de pièces par set.""" per_year: Dict[int, Dict[str, int]] = {} for row in rows: year = int(row["year"]) per_year[year] = per_year.get(year, {"parts": 0, "sets": 0}) per_year[year]["parts"] += int(row["num_parts"]) per_year[year]["sets"] += 1 results: List[Tuple[int, float]] = [] for year in sorted(per_year): totals = per_year[year] results.append((year, totals["parts"] / totals["sets"])) return results def compute_rolling_mean(series: List[Tuple[int, float]], window: int) -> List[Tuple[int, float]]: """Calcule la moyenne glissante sur une fenêtre donnée.""" values = [value for _, value in series] years = [year for year, _ in series] rolling: List[Tuple[int, float]] = [] for index in range(len(values)): if index + 1 < window: rolling.append((years[index], 0.0)) else: window_values = values[index - window + 1 : index + 1] rolling.append((years[index], sum(window_values) / window)) return rolling def plot_parts_per_set( enriched_sets_path: Path, milestones_path: Path, destination_path: Path, rolling_window: int = 3, ) -> None: """Génère un graphique de la moyenne annuelle et glissante des pièces par set.""" sets_rows = read_rows(enriched_sets_path) milestones = load_milestones(milestones_path) annual_series = compute_average_parts_per_set(sets_rows) rolling_series = compute_rolling_mean(annual_series, rolling_window) years = [year for year, _ in annual_series] annual_values = [value for _, value in annual_series] rolling_values = [value for _, value in rolling_series] fig, ax = plt.subplots(figsize=(12, 6)) ax.plot(years, annual_values, marker="o", color="#2ca02c", label="Moyenne annuelle (pièces/set)") ax.plot( years, rolling_values, marker="^", color="#9467bd", label=f"Moyenne glissante {rolling_window} ans (pièces/set)", ) ax.set_xlabel("Année") ax.set_ylabel("Pièces par set") ax.set_title("Évolution de la taille moyenne des sets (thèmes filtrés)") ax.grid(True, linestyle="--", alpha=0.3) ax.set_xlim(min(years) - 0.4, max(years) + 0.4) ax.set_xticks(list(range(min(years), max(years) + 1))) ax.tick_params(axis="x", labelrotation=45) peak = max(max(annual_values), max(rolling_values)) top_limit = peak * 2 milestones_in_range = sorted( [m for m in milestones if min(years) <= m["year"] <= max(years)], key=lambda m: (m["year"], m["description"]), ) milestone_offsets: Dict[int, int] = {} offset_step = 0.4 max_offset = 0 for milestone in milestones_in_range: year = milestone["year"] count_for_year = milestone_offsets.get(year, 0) milestone_offsets[year] = count_for_year + 1 horizontal_offset = offset_step * (count_for_year // 2 + 1) max_offset = max(max_offset, count_for_year) if count_for_year % 2 == 1: horizontal_offset *= -1 text_x = year + horizontal_offset ax.axvline(year, color="#d62728", linestyle="--", linewidth=1, alpha=0.65) ax.text( text_x, top_limit, milestone["description"], rotation=90, verticalalignment="top", horizontalalignment="center", fontsize=8, color="#d62728", ) ax.set_ylim(0, top_limit * (1 + max_offset * 0.02)) ax.legend(loc="upper left", bbox_to_anchor=(1.12, 1)) ensure_parent_dir(destination_path) fig.tight_layout() fig.savefig(destination_path, dpi=150) plt.close(fig)