"""Rendu HTML du diagramme miroir taxonomique — Sprint 77. A.I.4 chantier 3 du plan d'évolution 2026. Suite directe ``picarones/core/taxonomy_comparison.py``. Pattern identique aux autres rendus (Sprints 41/43/62/67/72/74/75/76) : **server-side**, pas de JavaScript, anti-injection systématique. Diagramme miroir ---------------- Une ligne par classe taxonomique, divisée en deux barres horizontales : - À **gauche** : barre du moteur A (orientée vers la gauche, du centre vers le bord). - À **droite** : barre du moteur B (orientée vers la droite). - Couleur de la classe selon ``recoverability`` : - vert (#5fa860) : ``recoverable`` - orange (#e0a050) : ``difficult`` - rouge (#d8553b) : ``irrecoverable`` Lecture immédiate : un moteur dont les barres tirent vers la **gauche** sur du vert (case_error, ligature_error) et un moteur qui tire à droite sur du rouge (lacuna) — la décision éditoriale est évidente même si les CER globaux sont identiques. """ from __future__ import annotations from html import escape as _e from typing import Optional _RECOVERABILITY_COLORS = { "recoverable": "#5fa860", "difficult": "#e0a050", "irrecoverable": "#d8553b", } def _build_mirror_chart_svg( data: dict, *, bar_max_width: int = 200, row_height: int = 22, label_width: int = 140, margin_top: int = 50, margin_bottom: int = 20, ) -> str: """Construit le diagramme miroir SVG.""" classes = data["classes"] prop_a = data["proportions_a"] prop_b = data["proportions_b"] recov = data["recoverability"] engine_a = data["engine_a"] engine_b = data["engine_b"] n_rows = len(classes) if n_rows == 0: return "" # Échelle : on normalise à la valeur max de toutes les # proportions (pour que la classe la plus présente atteigne # bar_max_width). max_prop = max( max(prop_a.values(), default=0.0), max(prop_b.values(), default=0.0), ) if max_prop <= 0: max_prop = 1.0 # évite division par zéro (cas dégénéré) width = label_width + 2 * bar_max_width + 40 height = margin_top + n_rows * row_height + margin_bottom center = width // 2 parts = [ f'") return "".join(parts) def _build_recoverability_summary_html( data: dict, labels: dict, ) -> str: """Encart résumé par catégorie de récupérabilité (3 lignes).""" totals = data.get("totals_by_recoverability") or {} if not totals: return "" label_recov = labels.get("taxocomp_recoverable", "Récupérable") label_diff = labels.get("taxocomp_difficult", "Difficile") label_irrec = labels.get("taxocomp_irrecoverable", "Irrécupérable") rows = [ ("recoverable", label_recov), ("difficult", label_diff), ("irrecoverable", label_irrec), ] parts = [ '
| ' f'{_e(labels.get("taxocomp_level_label", "Catégorie"))} | ', '' f'{_e(_e(data["engine_a"]))} | ', '' f'{_e(_e(data["engine_b"]))} | ', '
|---|---|---|
| ' f'' f'{_e(label)} | ' f'{cell["a"] * 100:.1f}% | ' f'{cell["b"] * 100:.1f}% | ' f'