1

97 lines
3.3 KiB
Python

"""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)