Spaces:
Sleeping
Sleeping
| """Rendu HTML « Précision sur séquences numériques » — Sprint 86. | |
| Suite directe ``picarones/core/numerical_sequences.py`` | |
| (Sprint 85) + câblage runner Sprint 86. | |
| Pattern identique aux autres rendus : server-side, pas de JS, | |
| anti-injection systématique. | |
| Vue | |
| --- | |
| Tableau moteur × catégorie (year / roman / foliation / currency | |
| / regnal) × score strict ; une ligne par moteur, une cellule | |
| colorée par cellule. Une seconde ligne donne le score ``value`` | |
| (en plus petit). Catégorie omise si **aucun** moteur n'a de | |
| GT exploitable pour elle. | |
| Adaptative : ``""`` si aucun moteur n'a de | |
| ``aggregated_numerical_sequences``. | |
| """ | |
| from __future__ import annotations | |
| from html import escape as _e | |
| from typing import Optional | |
| from picarones.core.numerical_sequences import CATEGORIES | |
| def _color_for_score(score: float) -> str: | |
| """Gradient rouge → jaune → vert.""" | |
| f = max(0.0, min(1.0, score)) | |
| if f < 0.5: | |
| t = f / 0.5 | |
| r = 235 | |
| g = int(70 + (200 - 70) * t) | |
| b = 70 | |
| else: | |
| t = (f - 0.5) / 0.5 | |
| r = int(235 + (60 - 235) * t) | |
| g = int(200 + (160 - 200) * t) | |
| b = int(70 + (90 - 70) * t) | |
| return f"#{r:02x}{g:02x}{b:02x}" | |
| def _category_columns_with_signal(rows: list[dict]) -> list[str]: | |
| """Ne garde que les catégories où ≥ 1 moteur a un n_total > 0.""" | |
| visible: list[str] = [] | |
| for cat in CATEGORIES: | |
| for r in rows: | |
| agg = r.get("aggregated_numerical_sequences") or {} | |
| cat_data = (agg.get("per_category") or {}).get(cat) or {} | |
| if (cat_data.get("n_total") or 0) > 0: | |
| visible.append(cat) | |
| break | |
| return visible | |
| def build_numerical_sequences_html( | |
| engines: list[dict], | |
| labels: Optional[dict[str, str]] = None, | |
| ) -> str: | |
| """Construit la section HTML séquences numériques. | |
| Returns | |
| ------- | |
| str | |
| ``""`` si aucun moteur n'a de signal. | |
| """ | |
| rows = [ | |
| e for e in engines | |
| if isinstance(e.get("aggregated_numerical_sequences"), dict) | |
| ] | |
| if not rows: | |
| return "" | |
| visible_cats = _category_columns_with_signal(rows) | |
| if not visible_cats: | |
| return "" | |
| labels = labels or {} | |
| title = labels.get( | |
| "numseq_title", "Précision sur séquences numériques", | |
| ) | |
| note = labels.get( | |
| "numseq_note", | |
| "Score strict (forme préservée) — la valeur entre " | |
| "parenthèses est le score sur la valeur (XIV ↔ 14 " | |
| "accepté). Foliotation : recto/verso non interchangeables.", | |
| ) | |
| col_engine = labels.get("numseq_engine", "Moteur") | |
| col_global = labels.get("numseq_global", "Global") | |
| cat_label = { | |
| "year": labels.get("numseq_cat_year", "Année"), | |
| "roman": labels.get("numseq_cat_roman", "Romain"), | |
| "foliation": labels.get("numseq_cat_foliation", "Foliation"), | |
| "currency": labels.get("numseq_cat_currency", "Montant"), | |
| "regnal": labels.get("numseq_cat_regnal", "Régnal"), | |
| } | |
| parts = [ | |
| '<div class="numseq-section" style="margin:1rem 0">', | |
| f'<h3 style="margin:0 0 .3rem 0">{_e(title)}</h3>', | |
| f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">' | |
| f'{_e(note)}</div>', | |
| '<table style="border-collapse:collapse;width:100%;' | |
| 'font-size:.9rem">', | |
| '<thead><tr>', | |
| f'<th style="padding:.4rem .6rem;text-align:left;' | |
| f'border-bottom:1px solid #ccc;font-weight:600">' | |
| f'{_e(col_engine)}</th>', | |
| f'<th style="padding:.4rem .6rem;text-align:right;' | |
| f'border-bottom:1px solid #ccc;font-weight:600">' | |
| f'{_e(col_global)}</th>', | |
| ] | |
| for cat in visible_cats: | |
| parts.append( | |
| f'<th style="padding:.4rem .6rem;text-align:right;' | |
| f'border-bottom:1px solid #ccc;font-weight:600">' | |
| f'{_e(cat_label.get(cat, cat))}</th>' | |
| ) | |
| parts.append("</tr></thead><tbody>") | |
| for engine in rows: | |
| agg = engine["aggregated_numerical_sequences"] | |
| name = engine.get("name") or "?" | |
| per_cat = agg.get("per_category") or {} | |
| global_strict = float(agg.get("global_strict_score") or 0.0) | |
| global_value = float(agg.get("global_value_score") or 0.0) | |
| n_total = int(agg.get("n_total") or 0) | |
| global_color = _color_for_score(global_strict) | |
| parts.append( | |
| f'<tr>' | |
| f'<td style="padding:.4rem .6rem">{_e(str(name))}</td>' | |
| f'<td style="padding:.4rem .6rem;text-align:right;' | |
| f'background:{global_color};font-family:monospace;' | |
| f'font-weight:600">' | |
| f'{global_strict * 100:.1f}%' | |
| f'<span style="font-size:.75rem;font-weight:400;' | |
| f'opacity:.75"> ({global_value * 100:.0f}%, ' | |
| f'n={n_total})</span></td>' | |
| ) | |
| for cat in visible_cats: | |
| cat_data = per_cat.get(cat) or {} | |
| n = int(cat_data.get("n_total") or 0) | |
| if n == 0: | |
| parts.append( | |
| '<td style="padding:.4rem .6rem;text-align:right;' | |
| 'opacity:.4">—</td>' | |
| ) | |
| continue | |
| strict = float(cat_data.get("strict_score") or 0.0) | |
| value = float(cat_data.get("value_score") or 0.0) | |
| color = _color_for_score(strict) | |
| parts.append( | |
| f'<td style="padding:.4rem .6rem;text-align:right;' | |
| f'background:{color};font-family:monospace">' | |
| f'{strict * 100:.0f}%' | |
| f'<span style="font-size:.75rem;opacity:.75"> ' | |
| f'({value * 100:.0f}%, n={n})</span></td>' | |
| ) | |
| parts.append("</tr>") | |
| parts.append("</tbody></table></div>") | |
| return "".join(parts) | |
| __all__ = ["build_numerical_sequences_html"] | |