1

Reviens à l’état fbf20e2 sans palettes par set

This commit is contained in:
2025-12-02 16:23:56 +01:00
parent 909a1eae71
commit c0700a8829
11 changed files with 0 additions and 816 deletions

View File

@@ -1,89 +0,0 @@
"""Palette dominante par set (hors minifigs)."""
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Sequence
import matplotlib.pyplot as plt
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.stats import read_rows
PLACEHOLDER_COLOR = "#e0e0e0"
def load_swatches(path: Path) -> List[dict]:
"""Charge le CSV des couleurs dominantes par set."""
return read_rows(path)
def group_swatches(rows: Sequence[dict], top_n: int = 5) -> List[dict]:
"""Groupe les couleurs par set et complète avec des placeholders si besoin."""
grouped: Dict[str, List[dict]] = defaultdict(list)
meta: Dict[str, dict] = {}
for row in rows:
grouped[row["set_num"]].append(row)
meta[row["set_num"]] = {"name": row["name"], "year": int(row["year"])}
result: List[dict] = []
for set_num, colors in grouped.items():
sorted_colors = sorted(colors, key=lambda r: int(r["rank"]))
while len(sorted_colors) < top_n:
sorted_colors.append(
{
"set_num": set_num,
"name": meta[set_num]["name"],
"year": str(meta[set_num]["year"]),
"rank": str(len(sorted_colors) + 1),
"color_rgb": "",
"color_name": "N/A",
"quantity_non_minifig": "0",
}
)
result.append(
{
"set_num": set_num,
"name": meta[set_num]["name"],
"year": meta[set_num]["year"],
"colors": sorted_colors[:top_n],
}
)
result.sort(key=lambda r: (r["year"], r["name"], r["set_num"]))
return result
def plot_set_color_swatches(swatches_path: Path, destination_path: Path) -> None:
"""Trace la palette de 5 couleurs dominantes par set (hors minifigs)."""
rows = load_swatches(swatches_path)
if not rows:
return
grouped = group_swatches(rows, top_n=5)
set_labels = [f"{item['year']} {item['name']}" for item in grouped]
y_positions = list(range(len(grouped)))
height = max(4, len(grouped) * 0.4)
fig, ax = plt.subplots(figsize=(12, height))
for y, item in zip(y_positions, grouped):
for idx, color in enumerate(item["colors"]):
rgb = color["color_rgb"].strip()
face_color = f"#{rgb}" if rgb else PLACEHOLDER_COLOR
ax.scatter(
idx,
y,
s=500,
color=face_color,
edgecolor="#0d0d0d",
linewidth=0.6,
)
ax.set_yticks(y_positions)
ax.set_yticklabels(set_labels)
ax.set_xticks([])
ax.invert_yaxis()
ax.set_xlim(-0.6, 4.6)
ax.set_title("Top 5 couleurs principales par set (hors minifigs)")
ax.grid(False)
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=160)
plt.close(fig)

View File

@@ -1,93 +0,0 @@
"""Visualisation des palettes perceptuelles (top 5) par set."""
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Sequence
import matplotlib.pyplot as plt
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.stats import read_rows
PLACEHOLDER_COLOR = "#e0e0e0"
def load_swatches(path: Path) -> List[dict]:
"""Charge le CSV des palettes perceptuelles."""
return read_rows(path)
def group_swatches(rows: Sequence[dict], top_n: int = 5) -> List[dict]:
"""Groupe les couleurs par set et complète avec placeholders si besoin."""
grouped: Dict[str, List[dict]] = defaultdict(list)
meta: Dict[str, dict] = {}
for row in rows:
grouped[row["set_num"]].append(row)
meta[row["set_num"]] = {"name": row["name"], "year": int(row["year"])}
result: List[dict] = []
for set_num, colors in grouped.items():
sorted_colors = sorted(colors, key=lambda r: int(r["rank"]))
while len(sorted_colors) < top_n:
sorted_colors.append(
{
"set_num": set_num,
"name": meta[set_num]["name"],
"year": str(meta[set_num]["year"]),
"rank": str(len(sorted_colors) + 1),
"color_rgb": "",
"color_name": "N/A",
"share_non_minifig": "0",
"quantity_non_minifig": "0",
}
)
result.append(
{
"set_num": set_num,
"name": meta[set_num]["name"],
"year": meta[set_num]["year"],
"colors": sorted_colors[:top_n],
}
)
result.sort(key=lambda r: (r["year"], r["set_num"], r["name"]))
return result
def plot_set_color_swatches_perceptual(swatches_path: Path, destination_path: Path) -> None:
"""Trace les 5 couleurs perceptuelles par set avec taille proportionnelle à la part."""
rows = load_swatches(swatches_path)
if not rows:
return
grouped = group_swatches(rows, top_n=5)
set_labels = [f"{item['year']} {item['name']}" for item in grouped]
y_positions = list(range(len(grouped)))
height = max(4, len(grouped) * 0.4)
fig, ax = plt.subplots(figsize=(12, height))
for y, item in zip(y_positions, grouped):
for idx, color in enumerate(item["colors"]):
rgb = color["color_rgb"].strip()
face_color = f"#{rgb}" if rgb else PLACEHOLDER_COLOR
share = float(color.get("share_non_minifig", "0"))
size = 450 + 900 * share
ax.scatter(
idx,
y,
s=size,
color=face_color,
edgecolor="#0d0d0d",
linewidth=0.6,
alpha=0.95,
)
ax.set_yticks(y_positions)
ax.set_yticklabels(set_labels)
ax.set_xticks([])
ax.invert_yaxis()
ax.set_xlim(-0.6, 4.6)
ax.set_title("Top 5 couleurs perceptuelles par set (hors minifigs, pièces techniques exclues)")
ax.grid(False)
ensure_parent_dir(destination_path)
fig.tight_layout()
fig.savefig(destination_path, dpi=160)
plt.close(fig)

View File

@@ -1,126 +0,0 @@
"""Préparation des palettes dominantes par set (hors minifigs)."""
import csv
from collections import defaultdict
from pathlib import Path
from typing import Dict, Iterable, List, Sequence
import colorsys
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.color_ignores import is_ignored_color_rgb
from lib.rebrickable.stats import read_rows
def load_colors_by_set(path: Path) -> List[dict]:
"""Charge colors_by_set.csv."""
return read_rows(path)
def load_sets_enriched(path: Path) -> Dict[str, dict]:
"""Indexe nom et année par set_num."""
lookup: Dict[str, dict] = {}
with path.open() as csv_file:
reader = csv.DictReader(csv_file)
for row in reader:
lookup[row["set_num"]] = {"name": row["name"], "year": int(row["year"]), "set_id": row["set_id"]}
return lookup
def parse_rgb_hex(value: str) -> tuple[float, float, float]:
"""Parse un code hexadécimal (RRGGBB) en composantes 0-1."""
normalized = value.strip()
if len(normalized) != 6:
return (0.0, 0.0, 0.0)
r = int(normalized[0:2], 16) / 255.0
g = int(normalized[2:4], 16) / 255.0
b = int(normalized[4:6], 16) / 255.0
return (r, g, b)
def hue_bucket(degrees: float) -> int:
"""Regroupe les teintes en grandes familles pour l'affichage."""
if degrees >= 330 or degrees < 30:
return 0 # rouge
if degrees < 90:
return 1 # jaune / orangé
if degrees < 150:
return 2 # vert
if degrees < 270:
return 3 # bleu
return 4 # violet
def color_display_key(row: dict) -> tuple[float, int, float, str]:
"""Clé de tri visuelle : teinte regroupée d'abord, puis luminosité."""
r, g, b = parse_rgb_hex(row["color_rgb"])
h, _s, v = colorsys.rgb_to_hsv(r, g, b)
degrees = h * 360.0
bucket = hue_bucket(degrees)
return (bucket, degrees, v, row["color_name"])
def build_top_colors_by_set(rows: Iterable[dict], sets_lookup: Dict[str, dict], top_n: int = 5) -> List[dict]:
"""Sélectionne les top couleurs hors minifigs pour chaque set."""
colors_by_set: Dict[str, List[dict]] = defaultdict(list)
for row in rows:
quantity = int(row["quantity_non_minifig"])
if quantity <= 0:
continue
if is_ignored_color_rgb(row["color_rgb"]):
continue
set_num = row["set_num"]
set_meta = sets_lookup.get(set_num)
if set_meta is None:
continue
colors_by_set[set_num].append(
{
"set_num": set_num,
"set_id": row["set_id"],
"year": set_meta["year"],
"name": set_meta["name"],
"color_rgb": row["color_rgb"],
"color_name": row["color_name"],
"quantity": quantity,
}
)
results: List[dict] = []
for set_num, color_rows in colors_by_set.items():
sorted_rows = sorted(color_rows, key=lambda r: (-r["quantity"], r["color_name"]))
selected = sorted_rows[:top_n]
ordered = sorted(selected, key=color_display_key)
for rank, color_row in enumerate(ordered, start=1):
results.append(
{
"set_num": color_row["set_num"],
"set_id": color_row["set_id"],
"name": color_row["name"],
"year": str(color_row["year"]),
"rank": str(rank),
"color_rgb": color_row["color_rgb"],
"color_name": color_row["color_name"],
"quantity_non_minifig": str(color_row["quantity"]),
}
)
results.sort(key=lambda r: (int(r["year"]), r["set_num"], r["name"], int(r["rank"])))
return results
def write_top_colors(path: Path, rows: Sequence[dict]) -> None:
"""Écrit le CSV des couleurs dominantes par set."""
ensure_parent_dir(path)
fieldnames = [
"set_num",
"set_id",
"name",
"year",
"rank",
"color_rgb",
"color_name",
"quantity_non_minifig",
]
with path.open("w", newline="") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow(row)

View File

@@ -1,143 +0,0 @@
"""Construction de palettes perceptuelles (top 5) par set hors minifigs."""
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Set
from lib.rebrickable.set_color_swatches import color_display_key, load_sets_enriched, parse_rgb_hex, hue_bucket
from lib.rebrickable.stats import read_rows
def load_colors_by_set(path: Path) -> List[dict]:
"""Charge colors_by_set.csv."""
return read_rows(path)
def compute_shares(rows: Iterable[dict]) -> Dict[str, List[dict]]:
"""Calcule les parts relatives de couleurs hors minifigs pour chaque set."""
by_set: Dict[str, List[dict]] = {}
totals: Dict[str, int] = {}
for row in rows:
quantity = int(row["quantity_non_minifig"])
if quantity <= 0:
continue
set_num = row["set_num"]
totals[set_num] = totals.get(set_num, 0) + quantity
current = by_set.get(set_num)
if current is None:
by_set[set_num] = [row]
else:
current.append(row)
shares: Dict[str, List[dict]] = {}
for set_num, color_rows in by_set.items():
total = totals.get(set_num, 0)
if total == 0:
continue
shares[set_num] = []
for row in color_rows:
share = int(row["quantity_non_minifig"]) / total
shares[set_num].append(
{
"set_num": row["set_num"],
"set_id": row["set_id"],
"name": row.get("name", ""),
"year": row["year"],
"color_rgb": row["color_rgb"],
"color_name": row["color_name"],
"quantity_non_minifig": row["quantity_non_minifig"],
"share_non_minifig": f"{share:.5f}",
}
)
return shares
def select_diverse_palette(rows: List[dict], top_n: int) -> List[dict]:
"""Sélectionne une palette diversifiée : priorité à la part et à la variété de teinte."""
sorted_by_share = sorted(rows, key=lambda r: (-float(r["share_non_minifig"]), r["color_name"]))
selected: List[dict] = []
buckets_used: Set[int] = set()
for row in sorted_by_share:
r, g, b = parse_rgb_hex(row["color_rgb"])
h, _s, _v = __import__("colorsys").rgb_to_hsv(r, g, b)
bucket = hue_bucket(h * 360.0)
if bucket in buckets_used:
continue
selected.append(row)
buckets_used.add(bucket)
if len(selected) == top_n:
break
if len(selected) < top_n:
for row in sorted_by_share:
if row in selected:
continue
selected.append(row)
if len(selected) == top_n:
break
while len(selected) < top_n:
selected.append(
{
"set_num": rows[0]["set_num"] if rows else "",
"set_id": rows[0]["set_id"] if rows else "",
"name": rows[0]["name"] if rows else "",
"year": rows[0]["year"] if rows else "",
"color_rgb": "",
"color_name": "N/A",
"quantity_non_minifig": "0",
"share_non_minifig": "0",
}
)
ordered = sorted(selected, key=color_display_key)
for rank, row in enumerate(ordered, start=1):
row["rank"] = str(rank)
return ordered[:top_n]
def build_perceptual_swatches(rows: Iterable[dict], sets_lookup: Dict[str, dict], top_n: int = 5) -> List[dict]:
"""Construit les palettes perceptuelles (parts relatives + diversité de teinte)."""
shares = compute_shares(rows)
swatches: List[dict] = []
for set_num, color_rows in shares.items():
set_meta = sets_lookup.get(set_num)
if set_meta is None:
continue
selected = select_diverse_palette(color_rows, top_n)
for row in selected:
swatches.append(
{
"set_num": set_num,
"set_id": set_meta["set_id"],
"name": set_meta["name"],
"year": str(set_meta["year"]),
"rank": row["rank"],
"color_rgb": row["color_rgb"],
"color_name": row["color_name"],
"share_non_minifig": row["share_non_minifig"],
"quantity_non_minifig": row["quantity_non_minifig"],
}
)
swatches.sort(key=lambda r: (int(r["year"]), r["set_num"], int(r["rank"])))
return swatches
def write_perceptual_swatches(path: Path, rows: Sequence[dict]) -> None:
"""Écrit le CSV des palettes perceptuelles."""
from lib.filesystem import ensure_parent_dir
ensure_parent_dir(path)
fieldnames = [
"set_num",
"set_id",
"name",
"year",
"rank",
"color_rgb",
"color_name",
"share_non_minifig",
"quantity_non_minifig",
]
with path.open("w", newline="") as csv_file:
import csv
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow(row)