"""Rendu HTML de la heatmap de co-occurrence taxonomique — Sprint 75. A.I.4 chantier 1 du plan d'évolution 2026. Suite directe ``picarones/core/taxonomy_cooccurrence.py``. Pattern identique aux autres rendus (Sprints 41/43/62/67/72/74) : **server-side**, pas de JavaScript, anti-injection systématique. Sortie typique -------------- - ``build_taxonomy_cooccurrence_html(data, labels)`` produit un bloc complet : titre + note d'usage + heatmap SVG + table des paires les plus co-occurrentes. - ``""`` retourné si ``data is None`` ou si la matrice est vide (rapport adaptatif). """ from __future__ import annotations from html import escape as _e from typing import Optional def _color_for_jaccard(j: float) -> str: """Gradient blanc → bleu profond pour Jaccard ∈ [0, 1]. Interpolation entre #ffffff (j=0) et #1e3a8a (j=1). """ f = max(0.0, min(1.0, j)) r = int(255 + (30 - 255) * f) g = int(255 + (58 - 255) * f) b = int(255 + (138 - 255) * f) return f"#{r:02x}{g:02x}{b:02x}" def _text_color_for_bg(j: float) -> str: """Texte blanc si fond foncé, noir sinon (lisibilité).""" return "#fff" if j > 0.55 else "#222" def _build_heatmap_svg( classes: list[str], matrix: dict[str, dict[str, float]], *, cell_size: int = 36, label_left: int = 130, label_top: int = 80, ) -> str: """Construit la heatmap SVG. Cellule = carré coloré ``_color_for_jaccard``, valeur Jaccard affichée en chiffres si > 0,05. Étiquettes des classes en colonne (haut) et en ligne (gauche). """ n = len(classes) if n == 0: return "" width = label_left + n * cell_size + 10 height = label_top + n * cell_size + 10 parts = [ f'") return "".join(parts) def _build_top_pairs_table( top_pairs: list, labels: dict, ) -> str: """Construit la table HTML des paires les plus co-occurrentes.""" if not top_pairs: return "" pair_label = labels.get("taxocooc_pair_label", "Paire") jaccard_label = labels.get("taxocooc_jaccard_label", "Jaccard") parts = [ '
| ' f'{_e(pair_label)} | ', f'' f'{_e(jaccard_label)} | ', '
|---|---|
'
f'{_e(ca)} ↔ {_e(cb)} | '
f'{j:.2f} | ' f'