feat(eda): render quality global — DPI 220, tablas anchas como imagen, layout side_by_side, índice clicable
Mejoras transversales del motor AutomaticEDA (PDF + PPTX) sobre el modelo de bloques: 1. DPI alto global: toda figura/imagen embebida se rasteriza a 220 dpi (antes 150, y en PDF la página se guardaba a ~100 dpi re-rasterizando los imshow). En PDF se aplica savefig.dpi=220 a la página; el texto sigue vectorial y seleccionable. Permite ampliar en el móvil sin pixelar. Imagen embebida medida: ~1081px (antes ~492px). 2. Tabla ancha → imagen de alta resolución: cuando un DataTable tiene demasiadas columnas para ser legible como texto (criterio _table_fits_as_text), se dibuja entera como una imagen nítida (nueva función render_table_as_figure_py_datascience: cabecera sombreada + zebra) escalada para caber completa, de modo que el lector hace zoom y la lee sin perder datos. Las tablas que sí caben siguen como texto seleccionable / tabla nativa. Aplica en PDF y PPTX. El df.head de 19 columnas del dataset sintético ya no se corta: sale como imagen. 3. Group.layout: nuevo hint retrocompatible (default "stack"). "side_by_side" coloca la tabla a la izquierda (~55%) y la figura a la derecha (~45%) en la misma slide PPTX (cae a apilado si no hay par tabla+figura o no caben); en PDF se trata como "stack" (el ancho A5 móvil no admite dos columnas). Pensado para que el capítulo cat_distr ponga el gráfico al lado de la tabla en PPT. 4. Portada con índice clicable: la lista de capítulos pasa de "Este informe incluye..." (markdown) a un Heading "Índice" + un TocEntry por capítulo. El renderer registra el inicio de cada capítulo y cablea cada entrada como salto real (PDF: link GOTO PyMuPDF; PPTX: salto a slide nativo), reutilizando el mecanismo del glosario clicable. Modelo: Group gana `layout`; nuevo bloque TocEntry; normalizers y __init__ actualizados. Contrato: documentado en docs/automatic_eda_contract.md §11.4 (incluye el contrato exacto del campo layout para el agente de cat_distr). Tests: nuevo render_quality_test.py (13 golden: DPI alto real, tabla ancha→imagen PDF/PPTX, narrow→texto, side_by_side PPTX dos columnas / PDF apilado, índice clicable PDF+PPTX, retrocompatibilidad layout por defecto). render_features_test actualizado al índice nuevo. Suite: 188 passed (módulo) + 38 passed/1 skipped (acceptance + pipeline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -160,11 +160,21 @@ class Group:
|
||||
a chapter can give each unit its own page — e.g. one categorical column per
|
||||
page (see CAT DISTR). It is purely additive: the default False keeps the plain
|
||||
keep-together behaviour for every existing chapter.
|
||||
|
||||
``layout`` is a hint for how the group's children are arranged:
|
||||
``"stack"`` (default) keeps the historical top-to-bottom flow; ``"side_by_side"``
|
||||
asks the PPTX renderer to place the group's table to the LEFT and its figure to
|
||||
the RIGHT of the same slide (table ~55% width, figure ~45%), measuring so both
|
||||
fit and falling back to stacking when they do not. The PDF renderer treats
|
||||
``"side_by_side"`` exactly like ``"stack"`` (the A5 mobile page is too narrow for
|
||||
two readable columns). Unknown values degrade to ``"stack"``. Purely additive:
|
||||
the default keeps every existing chapter unchanged.
|
||||
"""
|
||||
|
||||
blocks: list = field(default_factory=list)
|
||||
title: Optional[str] = None
|
||||
page_break_before: bool = False
|
||||
layout: str = "stack"
|
||||
kind: str = field(default="group", init=False)
|
||||
|
||||
|
||||
@@ -183,6 +193,22 @@ class GlossaryEntry:
|
||||
kind: str = field(default="glossary_entry", init=False)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TocEntry:
|
||||
"""One clickable index (table-of-contents) entry shown on the cover.
|
||||
|
||||
Rendered as a single line — the chapter ``label`` in the accent link colour —
|
||||
that, once the document is laid out, becomes a real click jumping to the first
|
||||
page/slide of the target chapter (PDF link annotation via PyMuPDF; PPTX native
|
||||
slide jump). ``target_id`` is matched against each chapter's ``id`` *and* its
|
||||
``title`` (the cover only knows chapter titles), so either resolves. If the
|
||||
target cannot be resolved the entry still renders as plain text (never cut)."""
|
||||
|
||||
label: str = ""
|
||||
target_id: str = ""
|
||||
kind: str = field(default="toc_entry", init=False)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Chapter:
|
||||
"""An ordered set of blocks with an id, a title and a generation version."""
|
||||
@@ -207,13 +233,14 @@ _BLOCK_BY_KIND = {
|
||||
"note": Note,
|
||||
"group": Group,
|
||||
"glossary_entry": GlossaryEntry,
|
||||
"toc_entry": TocEntry,
|
||||
}
|
||||
|
||||
|
||||
def as_block(obj: Any):
|
||||
"""Coerce a value into a block dataclass. Unknown values become a Note."""
|
||||
if isinstance(obj, (Heading, Markdown, KVTable, DataTable, Figure, Image,
|
||||
Caption, Note, Group, GlossaryEntry)):
|
||||
Caption, Note, Group, GlossaryEntry, TocEntry)):
|
||||
if isinstance(obj, Group):
|
||||
obj.blocks = as_blocks(obj.blocks)
|
||||
return obj
|
||||
@@ -259,11 +286,15 @@ def as_block(obj: Any):
|
||||
return Group(blocks=as_blocks(obj.get("blocks")),
|
||||
title=obj.get("title"),
|
||||
page_break_before=bool(
|
||||
obj.get("page_break_before", False)))
|
||||
obj.get("page_break_before", False)),
|
||||
layout=_safe_str(obj.get("layout")) or "stack")
|
||||
if cls is GlossaryEntry:
|
||||
return GlossaryEntry(key=_safe_str(obj.get("key")),
|
||||
label=_safe_str(obj.get("label")),
|
||||
definition=_safe_str(obj.get("definition")))
|
||||
if cls is TocEntry:
|
||||
return TocEntry(label=_safe_str(obj.get("label")),
|
||||
target_id=_safe_str(obj.get("target_id")))
|
||||
except Exception: # noqa: BLE001 — never raise on a malformed block.
|
||||
return Note(text=_safe_str(obj))
|
||||
return Note(text=_safe_str(obj))
|
||||
|
||||
Reference in New Issue
Block a user