1
etude_lego_jurassic_world/lib/plots/global_minifig_heads.py

106 lines
4.3 KiB
Python

"""Visualisation des couleurs de têtes de minifigs sur le catalogue complet."""
from pathlib import Path
from typing import Dict, Iterable, List, Tuple
import matplotlib.pyplot as plt
from lib.color_sort import sort_hex_colors_lab
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.stats import read_rows
def load_global_heads(heads_path: Path) -> List[dict]:
"""Charge l'agrégat global des têtes par année."""
return read_rows(heads_path)
def order_colors(rows: Iterable[dict]) -> List[Tuple[str, str, str]]:
"""Retourne toutes les couleurs triées par teinte (Yellow en premier)."""
colors_set = {(row["color_name"], row["color_rgb"], row["is_translucent"]) for row in rows}
yellow_colors = [color for color in colors_set if color[0] == "Yellow"]
remaining = [color for color in colors_set if color[0] != "Yellow"]
ordered_hex = sort_hex_colors_lab([color[1] for color in remaining])
hex_rank = {hex_value: index for index, hex_value in enumerate(ordered_hex)}
sorted_remaining = sorted(remaining, key=lambda c: (hex_rank[c[1]], c[2], c[0]))
return yellow_colors + sorted_remaining
def build_color_labels(colors: Iterable[Tuple[str, str, str]]) -> Dict[Tuple[str, str, str], str]:
"""Construit des libellés uniques (suffixe opaque/translucide si besoin)."""
name_counts: Dict[str, int] = {}
for name, _, _ in colors:
name_counts[name] = name_counts.get(name, 0) + 1
labels: Dict[Tuple[str, str, str], str] = {}
for name, color_rgb, is_trans in colors:
if name_counts[name] > 1:
suffix = " translucide" if is_trans == "true" else " opaque"
labels[(name, color_rgb, is_trans)] = f"{name}{suffix}"
else:
labels[(name, color_rgb, is_trans)] = name
return labels
def build_share_matrix(
rows: Iterable[dict], colors: List[Tuple[str, str, str]]
) -> Tuple[List[int], List[Tuple[str, str, str]], List[Dict[Tuple[str, str, str], float]]]:
"""Construit les parts par année pour toutes les couleurs triées."""
years = sorted({int(row["year"]) for row in rows})
shares_by_year: List[Dict[Tuple[str, str, str], float]] = []
rows_by_year: Dict[int, List[dict]] = {year: [] for year in years}
for row in rows:
rows_by_year[int(row["year"])].append(row)
for year in years:
year_rows = rows_by_year[year]
total = sum(int(r["quantity"]) for r in year_rows)
shares: Dict[Tuple[str, str, str], float] = {color: 0.0 for color in colors}
for r in year_rows:
key = (r["color_name"], r["color_rgb"], r["is_translucent"])
shares[key] = shares.get(key, 0.0) + (int(r["quantity"]) / total if total > 0 else 0.0)
shares_by_year.append(shares)
return years, colors, shares_by_year
def plot_global_head_shares(
heads_path: Path,
destination_path: Path,
top_limit: int = 12,
) -> None:
"""Trace les parts des couleurs de têtes de minifigs par année (catalogue complet)."""
rows = load_global_heads(heads_path)
colors = order_colors(rows)
labels_map = build_color_labels(colors)
years, colors, shares_by_year = build_share_matrix(rows, colors)
fig, ax = plt.subplots(figsize=(14, 6))
bottoms = [0.0] * len(years)
y_positions = list(range(len(years)))
for color in colors:
name, color_rgb, is_trans = color
values = [shares[color] for shares in shares_by_year]
edge = "#f2f2f2" if is_trans == "true" else "#0d0d0d"
ax.bar(
years,
values,
bottom=bottoms,
color=f"#{color_rgb}",
edgecolor=edge,
label=labels_map[color],
linewidth=0.7,
)
bottoms = [b + v for b, v in zip(bottoms, values)]
ax.set_ylim(0, 1.05)
ax.set_ylabel("Part des couleurs (têtes de minifigs, catalogue complet)")
ax.set_xlabel("Année")
if len(years) > 15:
step = max(1, len(years) // 10)
ax.set_xticks(years[::step])
else:
ax.set_xticks(years)
ax.set_title("Répartition des couleurs de têtes de minifigs par année (catalogue complet)")
ax.grid(True, axis="y", linestyle="--", alpha=0.25)
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=170)
plt.close(fig)