"""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 from picarones.report.render_helpers import ( GRADIENT_TARGET_BLUE, build_grid_svg, color_single_gradient, text_color_for_bg, ) def _build_jaccard_heatmap_svg( classes: list[str], matrix: dict[str, dict[str, float]], *, cell_size: int = 36, label_left: int = 130, label_top: int = 80, ) -> str: """Heatmap Jaccard de co-occurrence taxonomique. Délègue à :func:`build_grid_svg` ; reste un wrapper local qui encapsule les conventions spécifiques à la matrice symétrique (valeur affichée seulement si > 0,05, étiquettes rotées). """ if not classes: return "" def cell_value(i: int, j: int) -> float: return matrix.get(classes[i], {}).get(classes[j], 0.0) return build_grid_svg( n_rows=len(classes), n_cols=len(classes), row_label_fn=lambda i: classes[i], col_label_fn=lambda j: classes[j], cell_color_fn=lambda i, j: color_single_gradient( cell_value(i, j), end_rgb=GRADIENT_TARGET_BLUE, ), cell_text_fn=lambda i, j: ( f"{cell_value(i, j):.2f}" if cell_value(i, j) > 0.05 else None ), cell_text_color_fn=lambda i, j: text_color_for_bg(cell_value(i, j)), cell_w=cell_size, cell_h=cell_size, label_left=label_left, label_top=label_top, rotate_col_labels=True, aria_label="Heatmap Jaccard co-occurrence taxonomique", ) 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'