You've already forked etude_lego_jurassic_world
Ajoute les agrégats et visualisations globales des couleurs de têtes
This commit is contained in:
86
lib/plots/minifig_skin_tones.py
Normal file
86
lib/plots/minifig_skin_tones.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Visualisation de la part des têtes jaunes sur le catalogue global."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from lib.filesystem import ensure_parent_dir
|
||||
from lib.milestones import load_milestones
|
||||
from lib.rebrickable.stats import read_rows
|
||||
|
||||
|
||||
def compute_yellow_share(rows: List[dict]) -> List[dict]:
|
||||
"""Calcule la part de la couleur Yellow par année."""
|
||||
aggregated: Dict[str, Dict[str, int]] = {}
|
||||
for row in rows:
|
||||
year = row["year"]
|
||||
if year not in aggregated:
|
||||
aggregated[year] = {"yellow": 0, "total": 0}
|
||||
aggregated[year]["total"] += int(row["quantity"])
|
||||
if row["color_name"].lower() == "yellow" or row["color_rgb"].upper() == "FFFF00":
|
||||
aggregated[year]["yellow"] += int(row["quantity"])
|
||||
results = []
|
||||
for year in sorted(aggregated.keys(), key=int):
|
||||
total = aggregated[year]["total"]
|
||||
yellow = aggregated[year]["yellow"]
|
||||
share = yellow / total if total > 0 else 0
|
||||
results.append({"year": int(year), "yellow_share": share, "total": total})
|
||||
return results
|
||||
|
||||
|
||||
def plot_yellow_share(heads_path: Path, milestones_path: Path, destination_path: Path) -> None:
|
||||
"""Trace l'évolution de la part de têtes jaunes dans le catalogue complet."""
|
||||
rows = read_rows(heads_path)
|
||||
milestones = load_milestones(milestones_path)
|
||||
series = compute_yellow_share(rows)
|
||||
years = [item["year"] for item in series]
|
||||
shares = [item["yellow_share"] for item in series]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(13, 5.5))
|
||||
ax.plot(years, shares, color="#f2c300", marker="o", linewidth=2.4, label="Part Yellow")
|
||||
ax.fill_between(years, shares, color="#f2c300", alpha=0.18)
|
||||
ax.set_ylim(0, min(1.0, max(shares + [0.01]) * 1.1))
|
||||
ax.set_ylabel("Part de têtes Yellow")
|
||||
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("Evolution de l'usage des têtes Yellow (catalogue complet)")
|
||||
ax.grid(True, linestyle="--", alpha=0.3)
|
||||
if milestones:
|
||||
min_year = min(years)
|
||||
max_year = max(years)
|
||||
milestones_in_range = sorted(
|
||||
[m for m in milestones if min_year <= m["year"] <= max_year],
|
||||
key=lambda m: (m["year"], m["description"]),
|
||||
)
|
||||
offset_map: Dict[int, int] = {}
|
||||
offset_step = 0.35
|
||||
top_limit = ax.get_ylim()[1] * 1.05
|
||||
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,
|
||||
top_limit,
|
||||
milestone["description"],
|
||||
rotation=90,
|
||||
verticalalignment="top",
|
||||
horizontalalignment="center",
|
||||
fontsize=8,
|
||||
color="#d62728",
|
||||
)
|
||||
ax.set_ylim(ax.get_ylim()[0], top_limit * (1 + max(offset_map.values(), default=0) * 0.02))
|
||||
ensure_parent_dir(destination_path)
|
||||
fig.tight_layout()
|
||||
fig.savefig(destination_path, dpi=170)
|
||||
plt.close(fig)
|
||||
Reference in New Issue
Block a user