"""Visualisations des têtes dual-face.""" from pathlib import Path from typing import Iterable, List, Tuple import matplotlib.pyplot as plt import numpy as np from matplotlib.patches import Patch from lib.filesystem import ensure_parent_dir from lib.rebrickable.stats import read_rows def load_rows(path: Path) -> List[dict]: """Charge un CSV en mémoire.""" return read_rows(path) def plot_dual_faces_timeline(by_year_path: Path, destination_path: Path) -> None: """Trace la part annuelle des têtes dual-face.""" rows = load_rows(by_year_path) if not rows: return years = [row["year"] for row in rows] totals = [int(row["total_heads"]) for row in rows] duals = [int(row["dual_heads"]) for row in rows] shares = [float(row["share_dual"]) for row in rows] x = np.arange(len(years)) fig, ax = plt.subplots(figsize=(10, 6)) ax.bar(x, totals, color="#dddddd", alpha=0.4, label="Têtes totales") ax.plot(x, duals, color="#1f77b4", linewidth=2.0, label="Têtes dual-face (volume)") ax.plot(x, [s * max(totals) for s in shares], color="#d62728", linestyle="--", linewidth=1.6, label="Part dual-face (échelle volume)") ax.set_xticks(x) ax.set_xticklabels(years, rotation=45, ha="right") ax.set_ylabel("Volume de têtes") ax.set_title("Têtes de minifigs : volume et part des dual-face par année") ax.grid(True, linestyle="--", alpha=0.3) ax.legend(loc="upper left", frameon=False) ensure_parent_dir(destination_path) fig.tight_layout() fig.savefig(destination_path, dpi=170) plt.close(fig) def select_top_sets(rows: Iterable[dict], limit: int = 15) -> List[dict]: """Sélectionne les sets avec le plus de têtes dual-face.""" sorted_rows = sorted( rows, key=lambda row: (-int(row["dual_heads"]), -float(row["share_dual"]), row["set_num"]), ) return sorted_rows[:limit] def plot_dual_faces_top_sets(by_set_path: Path, destination_path: Path) -> None: """Top des sets contenant des têtes dual-face.""" rows = load_rows(by_set_path) if not rows: return top_rows = select_top_sets(rows) y = np.arange(len(top_rows)) duals = [int(row["dual_heads"]) for row in top_rows] labels = [f"{row['set_num']} · {row['name']} ({row['year']})" for row in top_rows] owned_mask = [row["in_collection"] == "true" for row in top_rows] fig, ax = plt.subplots(figsize=(11, 8)) for pos, value, owned in zip(y, duals, owned_mask): alpha = 0.9 if owned else 0.45 ax.barh(pos, value, color="#9467bd", alpha=alpha) ax.set_yticks(y) ax.set_yticklabels(labels) ax.invert_yaxis() ax.set_xlabel("Nombre de têtes dual-face") ax.set_title("Top des sets avec têtes dual-face") ax.grid(axis="x", linestyle="--", alpha=0.3) legend = [ Patch(facecolor="#9467bd", edgecolor="none", alpha=0.9, label="Set possédé"), Patch(facecolor="#9467bd", edgecolor="none", alpha=0.45, label="Set manquant"), ] ax.legend(handles=legend, loc="lower right", frameon=False) ensure_parent_dir(destination_path) fig.tight_layout() fig.savefig(destination_path, dpi=170) plt.close(fig) def select_top_characters(rows: Iterable[dict], limit: int = 12) -> List[dict]: """Sélectionne les personnages avec le plus de têtes dual-face.""" sorted_rows = sorted( rows, key=lambda row: (-int(row["dual_heads"]), -float(row["share_dual"]), row["known_character"]), ) return sorted_rows[:limit] def plot_dual_faces_characters(by_character_path: Path, destination_path: Path) -> None: """Top des personnages illustrés par des têtes dual-face.""" rows = load_rows(by_character_path) if not rows: return top_rows = select_top_characters(rows) y = np.arange(len(top_rows)) duals = [int(row["dual_heads"]) for row in top_rows] totals = [int(row["total_heads"]) for row in top_rows] shares = [float(row["share_dual"]) for row in top_rows] labels = [row["known_character"] for row in top_rows] fig, ax = plt.subplots(figsize=(11, 8)) ax.barh(y, totals, color="#cccccc", alpha=0.4, label="Têtes totales") ax.barh(y, duals, color="#e15759", alpha=0.9, label="Têtes dual-face") for pos, share in zip(y, shares): ax.text( totals[pos] + 0.1, pos, f"{share*100:.1f}%", va="center", ha="left", fontsize=9, color="#333333", ) ax.set_yticks(y) ax.set_yticklabels(labels) ax.invert_yaxis() ax.set_xlabel("Nombre de têtes") ax.set_title("Personnages dotés de têtes dual-face") ax.grid(axis="x", linestyle="--", alpha=0.3) ax.legend(loc="lower right", frameon=False) ensure_parent_dir(destination_path) fig.tight_layout() fig.savefig(destination_path, dpi=170) plt.close(fig)