Files
fn_registry/python/functions/datascience/render_table_as_figure.md
T
egutierrez a74a5a047f 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>
2026-07-01 01:34:21 +02:00

7.7 KiB

id, name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, example, tested, tests, test_file_path, file_path, params, output
id name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports example tested tests test_file_path file_path params output
render_table_as_figure_py_datascience render_table_as_figure function py datascience 1.0.0 impure def render_table_as_figure(header, rows, title=None, note=None, fontsize=9.0, max_cell_chars=40) -> "matplotlib.figure.Figure" Dibuja un bloque tabular (cabecera + filas) como una matplotlib.figure.Figure nítida, lista para rasterizar a DPI alto. Pensada para tablas que NO caben como texto en una página/slide del informe EDA: se rasteriza a alta resolución (el caller usa dpi=220, bbox_inches='tight') y el usuario hace zoom en el móvil para leerla entera sin perder datos. Cabecera sombreada (#eef3f6) y en negrita, filas pares (1-based) con zebra suave (#f6f8fa), tinta oscura (#1b1b1b) sobre blanco, rejilla gris muy fina (#cccccc). Trunca cada celda a max_cell_chars con elipsis y str()-ea cada valor (None -> ""). figsize proporcional al contenido (ancho por nº y longitud de columnas, alto por nº de filas) para que sea legible con zoom. Backend Agg sin pyplot global. Defensiva: header/rows vacíos o None, filas irregulares o cualquier error interno devuelven una Figure placeholder con texto centrado "(tabla no disponible)". NUNCA lanza.
eda
table
figure
matplotlib
visualization
rasterize
zoom
render
datascience
impure
false error_go_core
matplotlib
from datascience.render_table_as_figure import render_table_as_figure header = ["columna", "n_nulos", "%_nulos", "distintos", "tipo", "ejemplo"] rows = [ ["ingresos", 12, "1.2%", 980, "float64", "2345.67"], ["edad", 0, "0.0%", 88, "int64", "37"], ["ciudad", 5, "0.5%", 412, "object", "Madrid"], ] fig = render_table_as_figure(header, rows, title="Resumen de columnas", note="rasteriza a dpi=220 y haz zoom") fig.savefig("/tmp/tabla.png", dpi=220, bbox_inches="tight") true
test_returns_figure_with_table
test_rows_none_does_not_raise
test_header_none_does_not_raise
test_empty_lists_return_placeholder_figure
test_both_none_return_placeholder_figure
test_long_cell_is_truncated
test_none_cells_become_empty_strings
test_can_rasterize_to_png_high_dpi
test_placeholder_can_rasterize
test_ragged_rows_are_padded
python/functions/datascience/render_table_as_figure_test.py python/functions/datascience/render_table_as_figure.py
name desc
header Lista de nombres de columna (puede ser [] o None). Cada nombre se str()-ea, se trunca a max_cell_chars y se pinta en la fila cabecera sombreada en negrita. Si está vacío/None no se dibuja fila de cabecera (solo cuerpo).
name desc
rows Lista de filas; cada fila es una lista de celdas con valores cualesquiera (se str()-ean; None -> ""). Admite None (se trata como []), filas escalares (se envuelven en una celda) y filas de distinta longitud (la rejilla se rectangulariza al ancho máximo, rellenando con celdas vacías). Saltos de línea/tabs en una celda se colapsan a espacios para que no desborde a otras filas.
name desc
title Título opcional dibujado encima de la tabla, en negrita tinta #1b1b1b, alineado a la izquierda. None o "" => sin título. Default None.
name desc
note Nota opcional al pie de la figura, en gris #8a8a8a e itálica. None o "" => sin nota. Default None.
name desc
fontsize Tamaño de fuente base (pt) de las celdas del cuerpo. La cabecera usa fontsize+3 y la nota max(7, fontsize-1). Un valor no numérico o <= 0 cae a 9.0. Default 9.0.
name desc
max_cell_chars Trunca el texto de cada celda a este nº de chars (con … final cuando se recorta) para que el ancho no explote. Un valor no entero cae a 40; <= 0 deja las celdas vacías. Default 40.
Un matplotlib.figure.Figure (figsize proporcional al contenido: ancho ≈ 0.9-1.6" por columna según su texto, total acotado a 3-26"; alto ≈ 0.32" por fila + cabecera + espacio para título/nota, acotado) con un Axes sin ejes que contiene un ax.table(...) NO cerrado. Cabecera fondo #eef3f6 texto #1b1b1b bold; filas pares (1-based) zebra #f6f8fa, impares blanco; tinta #1b1b1b; bordes/rejilla #cccccc lw 0.4; texto alineado a la izquierda. Título encima (bold) y nota debajo (gris itálica) si se pasan. Si header/rows son vacíos o None, o ante cualquier error interno, devuelve una Figure placeholder pequeña con el texto centrado "(tabla no disponible)". NUNCA lanza. El caller la rasteriza (dpi=220, bbox_inches='tight') y la cierra; la función no la muestra ni la guarda.

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from datascience.render_table_as_figure import render_table_as_figure

# Tabla que no cabe como texto en la slide -> se rasteriza y se lee con zoom.
header = ["columna", "n_nulos", "%_nulos", "distintos", "tipo", "ejemplo"]
rows = [
    ["ingresos", 12, "1.2%", 980, "float64", "2345.67"],
    ["edad", 0, "0.0%", 88, "int64", "37"],
    ["ciudad", 5, "0.5%", 412, "object", "Madrid"],
    ["categoria_producto", 0, "0.0%", 1840, "object",
     "un_valor_categorico_muy_largo_que_se_trunca"],
]

fig = render_table_as_figure(
    header,
    rows,
    title="Resumen de columnas",
    note="rasteriza a dpi=220 y haz zoom en el móvil",
    fontsize=9.0,
    max_cell_chars=40,
)

# El renderer del informe lo rasteriza a alta resolución; aquí lo persistimos.
fig.savefig("/tmp/tabla.png", dpi=220, bbox_inches="tight")

Cuando usarla

Úsala en un informe EDA cuando una tabla no cabe como texto en una página o slide y prefieres una imagen nítida que el lector pueda ampliar en el móvil para leerla entera (perfiles de columnas, matrices de conteo, tablas de frecuencias con muchas filas o columnas anchas). Pásale la cabecera y las filas tal cual (los valores se str()-ean por ti) más un title/note opcionales; el llamante la rasteriza a dpi=220 con bbox_inches='tight'. Es la pareja "tabla-como-imagen" de los gráficos build_boxplots_figure / categorical_top_pie_figure: misma paleta y mismo contrato (Agg, sin pyplot, el caller cierra la figura).

Gotchas

  • Impura por matplotlib. Toca la maquinaria de render. Usa el backend Agg y la API orientada a objetos Figure/add_subplot — NUNCA pyplot.* aquí, para no tocar el estado global ni filtrar figuras entre llamadas. pyplot NO es thread-safe; esta función construye el Figure directamente, así que es segura de llamar en bucle desde el renderer.
  • El caller cierra la figura. Devuelve el Figure pero no lo muestra ni lo guarda. Quien la consume debe rasterizarla y luego liberarla (matplotlib.pyplot.close(fig)) para no acumular memoria en lotes grandes.
  • Pensada para rasterizar a DPI alto. El figsize es proporcional al contenido pero la legibilidad real viene del DPI: rasteriza con dpi=220 y bbox_inches='tight'. Una tabla con muchísimas filas crece en alto (capado a ~60") — para miles de filas, parte la tabla o resume antes de pasarla.
  • Truncación de celda visible. Cada celda se recorta a max_cell_chars (default 40) con final y los saltos de línea/tabs se colapsan a espacios, para que ninguna celda desborde a otras filas. Sube max_cell_chars si necesitas ver el valor completo (a costa de ancho).
  • Defensiva, nunca lanza. header/rows vacíos o None, filas escalares, filas de distinta longitud o cualquier error interno se manejan sin propagar: en el peor caso devuelve una Figure placeholder con "(tabla no disponible)". No envuelvas la llamada en try/except por miedo a un raise — no lo hay.