1

Aère le graphique des nouveaux personnages

This commit is contained in:
2025-12-03 22:29:19 +01:00
parent f9854a6949
commit f9e1555ecb
7 changed files with 240 additions and 1 deletions

View File

@@ -1,12 +1,13 @@
"""Graphique du nombre de minifigs par personnage."""
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.plots.gender_palette import GENDER_COLORS, GENDER_LABELS
from lib.rebrickable.stats import read_rows
@@ -21,6 +22,11 @@ def load_presence(path: Path) -> List[dict]:
return read_rows(path)
def load_new_characters(path: Path) -> List[dict]:
"""Charge le CSV des personnages introduits par année."""
return read_rows(path)
def load_variations_and_totals(path: Path) -> List[dict]:
"""Charge le CSV comparatif variations/total par personnage."""
return read_rows(path)
@@ -199,3 +205,73 @@ def plot_character_year_presence(presence_path: Path, destination_path: Path) ->
fig.tight_layout()
fig.savefig(destination_path, dpi=160)
plt.close(fig)
def plot_new_characters_per_year(
counts_path: Path,
milestones_path: Path,
destination_path: Path,
start_year: int,
end_year: int,
) -> None:
"""Trace un diagramme en barres du nombre de nouveaux personnages introduits par an."""
rows = load_new_characters(counts_path)
if not rows:
return
counts = {int(row["year"]): int(row["new_characters"]) for row in rows}
years = list(range(start_year, end_year + 1))
values = [counts.get(year, 0) for year in years]
fig_width = max(8.5, len(years) * 0.45 + 2.5)
fig, ax = plt.subplots(figsize=(fig_width, 5.4))
bars = ax.bar(years, values, color="#1f77b4", edgecolor="#0d0d0d", linewidth=0.7)
ax.set_xlabel("Année")
ax.set_ylabel("Nouveaux personnages")
ax.set_title("Personnages introduits par an (hors figurants)")
ax.grid(axis="y", linestyle="--", alpha=0.3)
ax.set_xticks(years)
ax.set_xticklabels(years, rotation=45, ha="right")
ax.set_xlim(start_year - 0.6, end_year + 0.6)
y_max = max(values) if values else 0
upper_limit = 20
ax.set_ylim(0, upper_limit)
for bar, value in zip(bars, values):
if value == 0:
continue
ax.text(bar.get_x() + bar.get_width() / 2, value + 0.05, str(value), ha="center", va="bottom", fontsize=8)
milestones = load_milestones(milestones_path)
if milestones:
milestones_in_range = sorted(
[m for m in milestones if start_year <= m["year"] <= end_year],
key=lambda m: (m["year"], m["description"]),
)
offset_step = 0.25
offset_map: Dict[int, int] = {}
top_limit = ax.get_ylim()[1]
label_y = top_limit * 0.96
for milestone in milestones_in_range:
year = milestone["year"]
count_for_year = offset_map.get(year, 0)
offset_map[year] = count_for_year + 1
horizontal_offset = offset_step * (count_for_year // 2 + 1)
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, zorder=1)
ax.text(
text_x,
label_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)
plt.close(fig)