1

Compare commits

..

2 Commits

Author SHA1 Message Date
9bbebbd530 Ajouter la frise des top couleurs annuelles 2025-12-03 15:28:53 +01:00
1663a75461 Trier les heatmaps de couleurs par teinte 2025-12-03 15:05:51 +01:00
9 changed files with 88 additions and 3 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). 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 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. 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 ### Étape 16 : couleurs de peau des minifigs

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

@@ -1,12 +1,13 @@
"""Visualisations temporelles des palettes de couleurs.""" """Visualisations temporelles des palettes de couleurs."""
from pathlib import Path 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 matplotlib.pyplot as plt
import numpy as np import numpy as np
from lib.filesystem import ensure_parent_dir from lib.filesystem import ensure_parent_dir
from lib.color_sort import sort_hex_colors_lab
from lib.milestones import load_milestones from lib.milestones import load_milestones
from lib.rebrickable.stats import read_rows from lib.rebrickable.stats import read_rows
@@ -86,9 +87,11 @@ def build_heatmap_data(rows: Iterable[dict]) -> Tuple[List[int], List[str], np.n
for row in rows: for row in rows:
key = (row["color_name"], row["color_rgb"], row["is_translucent"]) key = (row["color_name"], row["color_rgb"], row["is_translucent"])
color_totals[key] = color_totals.get(key, 0) + int(row["quantity_total"]) color_totals[key] = color_totals.get(key, 0) + int(row["quantity_total"])
ordered_hex = sort_hex_colors_lab({color_rgb for _, color_rgb, _ in color_totals.keys()})
hex_rank = {hex_value: index for index, hex_value in enumerate(ordered_hex)}
sorted_colors = sorted( sorted_colors = sorted(
color_totals.items(), color_totals.items(),
key=lambda item: (-item[1], item[0][0], item[0][1]), key=lambda item: (hex_rank[item[0][1]], item[0][2], item[0][0]),
) )
color_keys = [color for color, _ in sorted_colors] color_keys = [color for color, _ in sorted_colors]
labels = [f"{name} ({'trans' if is_trans == 'true' else 'opaque'})" for name, _, is_trans in color_keys] labels = [f"{name} ({'trans' if is_trans == 'true' else 'opaque'})" for name, _, is_trans in color_keys]
@@ -112,6 +115,80 @@ def build_heatmap_data(rows: Iterable[dict]) -> Tuple[List[int], List[str], np.n
return years, labels, matrix, swatches 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( def plot_colors_heatmap(
matrix_path: Path, matrix_path: Path,
destination_path: Path, destination_path: Path,

View File

@@ -3,6 +3,7 @@
from pathlib import Path 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_translucent_share
from lib.plots.colors_timeline import plot_top_colors_swatches
TIMELINE_PATH = Path("data/intermediate/colors_timeline.csv") 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_LINEAR_DESTINATION = Path("figures/step15/colors_heatmap_linear.png")
HEATMAP_LOG_DESTINATION = Path("figures/step15/colors_heatmap_log.png") HEATMAP_LOG_DESTINATION = Path("figures/step15/colors_heatmap_log.png")
HEATMAP_SHARE_DESTINATION = Path("figures/step15/colors_heatmap_share.png") HEATMAP_SHARE_DESTINATION = Path("figures/step15/colors_heatmap_share.png")
TOP_COLORS_DESTINATION = Path("figures/step15/colors_top5_swatches.png")
def main() -> None: 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_LINEAR_DESTINATION, use_log_scale=False)
plot_colors_heatmap(MATRIX_PATH, HEATMAP_LOG_DESTINATION, use_log_scale=True) 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_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__": if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@
import matplotlib import matplotlib
from pathlib import Path 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") 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_linear = tmp_path / "figures" / "step15" / "colors_heatmap_linear.png"
destination_log = tmp_path / "figures" / "step15" / "colors_heatmap_log.png" destination_log = tmp_path / "figures" / "step15" / "colors_heatmap_log.png"
destination_share = tmp_path / "figures" / "step15" / "colors_heatmap_share.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( matrix_path.write_text(
"year,color_rgb,is_translucent,color_name,quantity_total\n" "year,color_rgb,is_translucent,color_name,quantity_total\n"
"2020,AAAAAA,false,Gray,5\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_linear, use_log_scale=False)
plot_colors_heatmap(matrix_path, destination_log, use_log_scale=True) plot_colors_heatmap(matrix_path, destination_log, use_log_scale=True)
plot_colors_heatmap(matrix_path, destination_share, normalize_by_year=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.exists()
assert destination_linear.stat().st_size > 0 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_log.stat().st_size > 0
assert destination_share.exists() assert destination_share.exists()
assert destination_share.stat().st_size > 0 assert destination_share.stat().st_size > 0
assert destination_top.exists()
assert destination_top.stat().st_size > 0