"""Glossary chapter (GLOSARIO) — always the last chapter, clickable terms. Renders one entry per glossary term that the other chapters registered during the document build through ``ctx['glossary'].add(key, label, definition)`` (see ``GlossaryCollector`` in ``model.py``). Each entry is a clickable destination: every in-text appearance a chapter marked with ``[[term:key]]texto[[/term]]`` becomes a real jump to its entry here — PDF link annotations (PyMuPDF) and PPTX native slide jumps, both wired by the renderers. Returns ``None`` when no term was registered (there is nothing to show), so the chapter simply disappears from documents that did not mark any term. Contract: build_(profile, ctx) -> Chapter | None ; CHAPTER_VERSION = "x.y.z". """ from __future__ import annotations from .. import model CHAPTER_VERSION = "1.1.1" CHAPTER_ID = "glosario" CHAPTER_TITLE = "Glosario" # Canonical definitions for cross-cutting terms — the "how to read it" entries # that do not belong to a single chapter. A chapter only needs to *register* the # term (``ctx['glossary'].add(key, label)``) and mark its in-text appearance with # ``[[term:key]]…[[/term]]``; this chapter supplies the full definition here when # the collector carries the term without one. Keeping the prose in a single place # avoids repeating a long paragraph inline in every chapter that names the term # (the explanation moved out of the NUM DISTR and CAT DISTR intros lives here). _BASELINE_TERMS = { "histograma_boxplot": { "label": "Cómo leer el histograma y el boxplot", "definition": ( "Para cada columna numérica se muestra su histograma con tres líneas " "de referencia: la media (línea roja discontinua), la mediana (línea " "verde continua) y la banda ±1σ (zona sombreada que cubre una " "desviación estándar a cada lado de la media). Debajo, alineado al " "mismo eje horizontal, un boxplot de Tukey: la caja abarca del primer " "al tercer cuartil (P25–P75), la línea interior es la mediana y los " "bigotes llegan hasta 1,5·IQR; los puntos rojos señalan que hay " "valores más allá de las vallas (posibles atípicos). Comparar la media " "con la mediana revela la asimetría: si la media supera a la mediana la " "cola larga cae hacia los valores altos (asimetría a la derecha), y al " "revés hacia los bajos."), }, "pagina_categorica": { "label": "Cómo se organiza cada página categórica", "definition": ( "Cada columna categórica ocupa su propia página: muestra sus métricas " "de cardinalidad —incluida la entropía—, una nota que señala " "cardinalidad problemática (columnas que se comportan como " "identificador, con casi todos los valores distintos, o dominadas por " "una sola categoría), la tabla de las categorías más frecuentes (top-k, " "con su conteo y porcentaje) y un gráfico de barras de las categorías " "más comunes (top-k más una barra «Otros» que agrupa la cola). El total " "de filas del dataset se usa como referencia para interpretar los " "conteos."), }, } def _resolve_term(term: dict) -> tuple: """Return (label, definition) for a collected term, completing a missing definition (and, if absent, the label) from the canonical baseline catalog.""" key = model._safe_str(term.get("key")) label = model._safe_str(term.get("label")) definition = model._safe_str(term.get("definition")) base = _BASELINE_TERMS.get(key) if base: if not definition.strip(): definition = model._safe_str(base.get("definition")) if not label.strip() or label == key: label = model._safe_str(base.get("label")) or label return label, definition def build_glosario(profile: dict, ctx: dict): """Build the glossary Chapter from the shared collector, or None if empty.""" ctx = ctx or {} glossary = ctx.get("glossary") if not isinstance(glossary, model.GlossaryCollector) or not glossary: return None blocks = [ model.Heading(text="Glosario de términos", level=1), model.Markdown(text=( "Definición de los términos técnicos que aparecen en el informe. " "Cada término va resaltado en el texto y, al pulsarlo, salta a su " "definición en esta sección.")), ] # One clickable destination per term, alphabetically by *visible* label. The # baseline resolution must happen BEFORE sorting: a term registered bare (no # label) carries its key as label in the collector, so ordering by the # collector's label would place it by its key instead of by the human label # supplied by the baseline catalog. Resolve first, then sort by the final label. resolved = [] for term in glossary.terms(by="order"): label, definition = _resolve_term(term) resolved.append((label, definition, model._safe_str(term.get("key")))) resolved.sort(key=lambda e: model._safe_str(e[0]).lower()) for label, definition, key in resolved: blocks.append(model.GlossaryEntry( key=key, label=label, definition=definition)) return model.Chapter(id=CHAPTER_ID, title=CHAPTER_TITLE, version=CHAPTER_VERSION, blocks=blocks)