1

Ajouter la frise des top couleurs annuelles

This commit is contained in:
Richard Dern 2025-12-03 15:28:53 +01:00
parent ed3e4354ec
commit ff2fa1819a
4 changed files with 84 additions and 2 deletions

View File

@ -187,6 +187,7 @@ Le script lit `data/intermediate/colors_by_set.csv` et produit deux agrégats :
Le script lit les agrégats de l'étape 14 et produit `figures/step15/colors_translucent_share.png` (part des pièces translucides par année et nombre de couleurs distinctes), `figures/step15/colors_heatmap_linear.png` (heatmap année × couleur en quantités brutes) et `figures/step15/colors_heatmap_log.png` (heatmap avec échelle log1p).
Une troisième variante normalise les quantités par année : `figures/step15/colors_heatmap_share.png`. Dans cette vue, chaque colonne (année) est ramenée à une part relative (01) du total de pièces de l'année. Cela met en évidence la structure de palette indépendamment du volume : deux années restent comparables même si leur nombre total de pièces diffère fortement, mais l'information de volume absolu n'apparaît plus (à privilégier pour les comparaisons de proportions, pas pour mesurer la rareté volumique).
Une frise `figures/step15/colors_top5_swatches.png` montre, pour chaque année, les 5 couleurs les plus utilisées (pastilles verticales par année).
Toutes les vues héritent du filtrage des couleurs ignorées et des pièces techniques/structurelles appliqué en amont.
### Étape 16 : couleurs de peau des minifigs

View File

@ -1,7 +1,7 @@
"""Visualisations temporelles des palettes de couleurs."""
from pathlib import Path
from typing import Dict, Iterable, List, Tuple
from typing import Dict, Iterable, List, Sequence, Tuple
import matplotlib.pyplot as plt
import numpy as np
@ -115,6 +115,80 @@ def build_heatmap_data(rows: Iterable[dict]) -> Tuple[List[int], List[str], np.n
return years, labels, matrix, swatches
def build_top_colors_by_year(rows: Iterable[dict], limit: int = 5) -> List[dict]:
"""Extrait les principales couleurs par année avec leur part relative."""
totals_by_year: Dict[int, int] = {}
grouped: Dict[int, List[dict]] = {}
for row in rows:
year = int(row["year"])
quantity = int(row["quantity_total"])
totals_by_year[year] = totals_by_year.get(year, 0) + quantity
grouped.setdefault(year, []).append(row)
top_rows: List[dict] = []
for year in sorted(grouped.keys()):
entries = grouped[year]
entries.sort(key=lambda r: (-int(r["quantity_total"]), r["color_name"], r["color_rgb"]))
total = totals_by_year[year]
for rank, entry in enumerate(entries[:limit]):
quantity = int(entry["quantity_total"])
share = quantity / total if total > 0 else 0.0
top_rows.append(
{
"year": year,
"rank": rank,
"color_name": entry["color_name"],
"color_rgb": entry["color_rgb"],
"is_translucent": entry["is_translucent"],
"quantity_total": quantity,
"share": share,
}
)
return top_rows
def plot_top_colors_swatches(
matrix_path: Path,
destination_path: Path,
limit: int = 5,
) -> None:
"""Affiche une frise des top couleurs par année (5 pastilles empilées par année)."""
rows = load_rows(matrix_path)
if not rows:
return
top_rows = build_top_colors_by_year(rows, limit=limit)
years = sorted({row["year"] for row in top_rows})
year_positions = {year: idx for idx, year in enumerate(years)}
x_values = [year_positions[row["year"]] for row in top_rows]
y_values = [limit - row["rank"] - 1 for row in top_rows]
ordered_hex = sort_hex_colors_lab({row["color_rgb"] for row in top_rows})
hex_rank = {hex_value: idx for idx, hex_value in enumerate(ordered_hex)}
sorted_rows = sorted(
top_rows,
key=lambda r: (r["year"], hex_rank[r["color_rgb"]], r["is_translucent"], r["color_name"]),
)
x_values = [year_positions[row["year"]] for row in sorted_rows]
y_values = [sorted_rows[i]["rank"] * 0.6 for i in range(len(sorted_rows))]
colors = [f"#{row['color_rgb']}" for row in sorted_rows]
sizes = [620 for _ in sorted_rows]
edges = ["#f2f2f2" if row["is_translucent"] == "true" else "#111111" for row in sorted_rows]
fig, ax = plt.subplots(figsize=(0.75 * len(years) + 3.5, 0.75 * limit + 0.9))
ax.scatter(x_values, y_values, s=sizes, c=colors, edgecolors=edges, linewidths=1.05)
ax.set_xticks(list(year_positions.values()))
ax.set_xticklabels(years, rotation=45)
ax.set_yticks([])
ax.set_ylabel("")
ax.set_xlim(-0.6, len(years) - 0.4)
ax.set_ylim(-0.6, limit - 0.5)
ax.set_title(f"Top {limit} couleurs par année")
ax.grid(axis="x", linestyle="--", alpha=0.25)
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=160)
plt.close(fig)
def plot_colors_heatmap(
matrix_path: Path,
destination_path: Path,

View File

@ -3,6 +3,7 @@
from pathlib import Path
from lib.plots.colors_timeline import plot_colors_heatmap, plot_translucent_share
from lib.plots.colors_timeline import plot_top_colors_swatches
TIMELINE_PATH = Path("data/intermediate/colors_timeline.csv")
@ -12,6 +13,7 @@ TRANSLUCENT_DESTINATION = Path("figures/step15/colors_translucent_share.png")
HEATMAP_LINEAR_DESTINATION = Path("figures/step15/colors_heatmap_linear.png")
HEATMAP_LOG_DESTINATION = Path("figures/step15/colors_heatmap_log.png")
HEATMAP_SHARE_DESTINATION = Path("figures/step15/colors_heatmap_share.png")
TOP_COLORS_DESTINATION = Path("figures/step15/colors_top5_swatches.png")
def main() -> None:
@ -20,6 +22,7 @@ def main() -> None:
plot_colors_heatmap(MATRIX_PATH, HEATMAP_LINEAR_DESTINATION, use_log_scale=False)
plot_colors_heatmap(MATRIX_PATH, HEATMAP_LOG_DESTINATION, use_log_scale=True)
plot_colors_heatmap(MATRIX_PATH, HEATMAP_SHARE_DESTINATION, normalize_by_year=True)
plot_top_colors_swatches(MATRIX_PATH, TOP_COLORS_DESTINATION, limit=5)
if __name__ == "__main__":

View File

@ -3,7 +3,7 @@
import matplotlib
from pathlib import Path
from lib.plots.colors_timeline import plot_colors_heatmap, plot_translucent_share
from lib.plots.colors_timeline import plot_colors_heatmap, plot_top_colors_swatches, plot_translucent_share
matplotlib.use("Agg")
@ -33,6 +33,7 @@ def test_plot_colors_heatmap(tmp_path: Path) -> None:
destination_linear = tmp_path / "figures" / "step15" / "colors_heatmap_linear.png"
destination_log = tmp_path / "figures" / "step15" / "colors_heatmap_log.png"
destination_share = tmp_path / "figures" / "step15" / "colors_heatmap_share.png"
destination_top = tmp_path / "figures" / "step15" / "colors_top5_swatches.png"
matrix_path.write_text(
"year,color_rgb,is_translucent,color_name,quantity_total\n"
"2020,AAAAAA,false,Gray,5\n"
@ -44,6 +45,7 @@ def test_plot_colors_heatmap(tmp_path: Path) -> None:
plot_colors_heatmap(matrix_path, destination_linear, use_log_scale=False)
plot_colors_heatmap(matrix_path, destination_log, use_log_scale=True)
plot_colors_heatmap(matrix_path, destination_share, normalize_by_year=True)
plot_top_colors_swatches(matrix_path, destination_top, limit=2)
assert destination_linear.exists()
assert destination_linear.stat().st_size > 0
@ -51,3 +53,5 @@ def test_plot_colors_heatmap(tmp_path: Path) -> None:
assert destination_log.stat().st_size > 0
assert destination_share.exists()
assert destination_share.stat().st_size > 0
assert destination_top.exists()
assert destination_top.stat().st_size > 0