1
etude_lego_jurassic_world/lib/plots/minifig_characters.py

129 lines
4.5 KiB
Python

"""Graphique du nombre de minifigs par personnage."""
from pathlib import Path
from typing import List
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.stats import read_rows
GENDER_COLORS = {
"male": "#4c72b0",
"female": "#c44e52",
"unknown": "#7f7f7f",
}
GENDER_LABELS = {
"male": "Homme",
"female": "Femme",
"unknown": "Inconnu",
"": "Inconnu",
}
def load_counts(path: Path) -> List[dict]:
"""Charge le CSV des comptes par personnage."""
return read_rows(path)
def load_presence(path: Path) -> List[dict]:
"""Charge le CSV de présence par année/personnage."""
return read_rows(path)
def plot_minifigs_per_character(counts_path: Path, destination_path: Path) -> None:
"""Trace un diagramme en barres horizontales du nombre de minifigs par personnage."""
rows = load_counts(counts_path)
characters = [row["known_character"] for row in rows]
counts = [int(row["minifig_count"]) for row in rows]
genders = [row.get("gender", "") for row in rows]
colors = [GENDER_COLORS.get(gender.strip().lower(), GENDER_COLORS["unknown"]) for gender in genders]
positions = list(range(len(rows)))
height = max(6, len(rows) * 0.22)
fig, ax = plt.subplots(figsize=(12, height))
bars = ax.barh(positions, counts, color=colors, edgecolor="#0d0d0d", linewidth=0.6)
ax.set_yticks(positions)
ax.set_yticklabels(characters)
ax.invert_yaxis()
ax.set_xlabel("Nombre de minifigs distinctes")
ax.set_title("Minifigs par personnage (thèmes filtrés)")
ax.grid(True, axis="x", linestyle="--", alpha=0.25)
max_value = max(counts) if counts else 0
ax.set_xlim(0, max_value + 1)
for index, bar in enumerate(bars):
value = counts[index]
ax.text(value + 0.1, bar.get_y() + bar.get_height() / 2, str(value), va="center", fontsize=8)
legend_entries = []
seen = set()
for gender in genders:
normalized = gender.strip().lower()
if normalized in seen:
continue
seen.add(normalized)
legend_entries.append(
Patch(
facecolor=GENDER_COLORS.get(normalized, GENDER_COLORS["unknown"]),
edgecolor="#0d0d0d",
linewidth=0.6,
label=GENDER_LABELS.get(normalized, "Inconnu"),
)
)
if legend_entries:
ax.legend(handles=legend_entries, title="Genre", loc="lower right")
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=160)
plt.close(fig)
def plot_character_year_presence(presence_path: Path, destination_path: Path) -> None:
"""Trace une heatmap indiquant le nombre de minifigs par personnage et par année."""
rows = load_presence(presence_path)
if not rows:
return
years = sorted({int(row["year"]) for row in rows})
characters = sorted(
{row["known_character"] for row in rows},
key=lambda name: (
-sum(int(r["minifig_count"]) for r in rows if r["known_character"] == name),
name,
),
)
matrix = []
for character in characters:
row_values = []
for year in years:
count = next(
(r["minifig_count"] for r in rows if r["known_character"] == character and int(r["year"]) == year),
"0",
)
row_values.append(int(count))
matrix.append(row_values)
height = max(5, len(characters) * 0.35)
fig, ax = plt.subplots(figsize=(12, height))
cax = ax.imshow(matrix, aspect="auto", cmap="Greens", interpolation="nearest")
ax.set_xticks(range(len(years)))
ax.set_xticklabels(years, rotation=45, ha="right")
ax.set_yticks(range(len(characters)))
ax.set_yticklabels(characters)
ax.set_xlabel("Année")
ax.set_ylabel("Personnage")
ax.set_title("Nombre de minifigs par personnage et par année (hors figurants)")
for i, character in enumerate(characters):
for j, year in enumerate(years):
value = matrix[i][j]
if value == 1:
ax.text(j, i, "", ha="center", va="center", color="#0d0d0d", fontsize=7)
elif value > 1:
ax.text(j, i, str(value), ha="center", va="center", color="#0d0d0d", fontsize=7)
fig.colorbar(cax, ax=ax, fraction=0.046, pad=0.04, label="Nombre de minifigs")
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=160)
plt.close(fig)