"""Visualisation de la réutilisation des têtes de minifigs.""" import csv from pathlib import Path from typing import List import matplotlib.pyplot as plt from matplotlib.offsetbox import AnnotationBbox, OffsetImage from PIL import Image from lib.filesystem import ensure_parent_dir from lib.rebrickable.resources import sanitize_name def load_head_reuse(path: Path) -> List[dict]: """Charge le CSV head_reuse.""" rows: List[dict] = [] with path.open() as csv_file: reader = csv.DictReader(csv_file) for row in reader: rows.append(row) return rows def format_label(row: dict) -> str: """Formate le label affiché sur l'axe vertical.""" character = row["known_character"] if character != "": return f"{row['part_num']} — {character}" return row["part_num"] def load_head_image(row: dict, resources_dir: Path) -> Image.Image | None: """Charge l'image d'une tête si disponible localement.""" set_id = row.get("sample_set_id", "").strip() character = row.get("known_character", "").strip() if set_id == "" or character == "": return None path = resources_dir / set_id / sanitize_name(character) / "head.jpg" if not path.exists(): return None return Image.open(path) def plot_head_reuse( path: Path, destination_path: Path, top: int | None = None, resources_dir: Path = Path("figures/rebrickable"), show_images: bool = True, ) -> None: """Trace un bar chart horizontal mettant en avant les têtes exclusives ou rares.""" rows = load_head_reuse(path) rows.sort(key=lambda r: (int(r["total_sets"]), int(r["other_sets"]), r["part_num"])) selected = rows if top is None else rows[:top] labels = [format_label(r) for r in selected] filtered_counts = [int(r["filtered_sets"]) for r in selected] other_counts = [int(r["other_sets"]) for r in selected] positions = list(range(len(selected))) fig, ax = plt.subplots(figsize=(13, 0.5 * len(selected) + 1.5)) ax.barh(positions, filtered_counts, color="#1f78b4", label="Sets filtrés") ax.barh(positions, other_counts, left=filtered_counts, color="#b2df8a", label="Autres sets") ax.set_yticks(positions) ax.set_yticklabels(labels) ax.set_xlabel("Nombre de sets contenant la tête") ax.grid(axis="x", linestyle="--", alpha=0.4) ax.legend() if show_images: max_count = max((f + o) for f, o in zip(filtered_counts, other_counts)) pad = max_count * 0.15 if max_count > 0 else 1.0 ax.set_xlim(left=-pad, right=max_count + pad * 0.2) for row, pos in zip(selected, positions): image = load_head_image(row, resources_dir) if image is None: continue target_height = 24 ratio = target_height / image.height resized = image.resize((int(image.width * ratio), target_height)) imagebox = OffsetImage(resized) ab = AnnotationBbox( imagebox, (-pad * 0.4, pos), xycoords=("data", "data"), box_alignment=(0.5, 0.5), frameon=False, ) ax.add_artist(ab) fig.subplots_adjust(left=0.42) fig.tight_layout() ensure_parent_dir(destination_path) fig.savefig(destination_path, dpi=150) plt.close(fig)