"""Visualisation des pièces les plus rares observées dans les sets filtrés.""" 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 def load_part_rarity(path: Path) -> List[dict]: """Charge le CSV des pièces rares.""" 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 l’étiquette de l’axe vertical.""" return f"{row['part_num']} — {row['part_name']}" def load_part_image(row: dict, resources_dir: Path) -> Image.Image | None: """Charge l'image associée à une pièce si elle est disponible.""" path = resources_dir / row["sample_set_id"] / "rare_parts" / f"{row['part_num']}.jpg" if not path.exists(): return None return Image.open(path) def plot_part_rarity( path: Path, destination_path: Path, resources_dir: Path = Path("figures/rebrickable"), show_images: bool = True, ) -> None: """Trace un bar chart horizontal des pièces les plus rares avec leurs visuels.""" rows = load_part_rarity(path) selected = rows labels = [format_label(row) for row in selected] filtered_counts = [int(row["filtered_quantity"]) for row in selected] other_counts = [int(row["other_sets_quantity"]) for row in selected] positions = list(range(len(selected))) fig, ax = plt.subplots(figsize=(13, 0.55 * len(selected) + 1.4)) 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("Occurrences de la pièce (rechanges incluses)") 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)) if selected else 0 pad = max_count * 0.15 if max_count > 0 else 1.0 ax.set_xlim(left=-pad, right=max_count + pad * 0.3) for row, pos in zip(selected, positions): image = load_part_image(row, resources_dir) if image is None: continue target_height = 28 ratio = target_height / image.height resized = image.resize((int(image.width * ratio), target_height)) imagebox = OffsetImage(resized) ab = AnnotationBbox( imagebox, (-pad * 0.45, 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)