Ajouter la frise des top couleurs annuelles
This commit is contained in:
parent
ed3e4354ec
commit
ff2fa1819a
@ -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 (0–1) 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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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__":
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user