1

Ajoute le genre des personnages et colore les graphiques

This commit is contained in:
2025-12-02 11:37:13 +01:00
parent 230b9db239
commit f5c1fa6333
12 changed files with 300 additions and 43 deletions

View File

@@ -1,20 +1,35 @@
"""Diagramme de longévité des personnages (bornes d'apparition)."""
from pathlib import Path
from typing import List
from typing import Dict, List
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
from lib.filesystem import ensure_parent_dir
from lib.milestones import load_milestones
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_spans(path: Path) -> List[dict]:
"""Charge le CSV des bornes min/max par personnage."""
return read_rows(path)
def plot_character_spans(spans_path: Path, destination_path: Path) -> None:
def plot_character_spans(spans_path: Path, destination_path: Path, milestones_path: Path | None = None) -> None:
"""Trace un diagramme en barres représentant la longévité des personnages."""
rows = load_spans(spans_path)
if not rows:
@@ -23,18 +38,22 @@ def plot_character_spans(spans_path: Path, destination_path: Path) -> None:
starts = [int(row["start_year"]) for row in rows]
ends = [int(row["end_year"]) for row in rows]
counts = [int(row["total_minifigs"]) 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)))
widths = [end - start + 1 for start, end in zip(starts, ends)]
min_year = min(starts)
max_year = max(ends)
height = max(5, len(rows) * 0.3)
milestones = load_milestones(milestones_path) if milestones_path else []
fig, ax = plt.subplots(figsize=(12, height))
bars = ax.barh(
positions,
widths,
left=starts,
color="#1f77b4",
color=colors,
edgecolor="#0d0d0d",
linewidth=0.6,
)
@@ -57,6 +76,55 @@ def plot_character_spans(spans_path: Path, destination_path: Path) -> None:
color="#0d0d0d",
)
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")
if milestones:
milestones_in_range = sorted(
[m for m in milestones if min_year <= m["year"] <= max_year],
key=lambda m: (m["year"], m["description"]),
)
milestone_offsets: Dict[int, int] = {}
offset_step = 0.2
max_offset = 0
y_bottom, y_top = ax.get_ylim()
text_y = y_top - (y_top - y_bottom) * 0.01
for milestone in milestones_in_range:
year = milestone["year"]
count_for_year = milestone_offsets.get(year, 0)
milestone_offsets[year] = count_for_year + 1
horizontal_offset = offset_step * (count_for_year // 2 + 1)
max_offset = max(max_offset, count_for_year)
if count_for_year % 2 == 1:
horizontal_offset *= -1
text_x = year + horizontal_offset
ax.axvline(year, color="#d62728", linestyle="--", linewidth=1, alpha=0.65)
ax.text(
text_x,
text_y,
milestone["description"],
rotation=90,
verticalalignment="top",
horizontalalignment="center",
fontsize=8,
color="#d62728",
)
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=160)

View File

@@ -4,11 +4,25 @@ 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)
@@ -24,11 +38,13 @@ def plot_minifigs_per_character(counts_path: Path, destination_path: Path) -> No
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="#1f77b4", edgecolor="#0d0d0d", linewidth=0.6)
bars = ax.barh(positions, counts, color=colors, edgecolor="#0d0d0d", linewidth=0.6)
ax.set_yticks(positions)
ax.set_yticklabels(characters)
ax.invert_yaxis()
@@ -40,6 +56,23 @@ def plot_minifigs_per_character(counts_path: Path, destination_path: Path) -> No
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()