--- id: render_table_as_figure_py_datascience name: render_table_as_figure kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def render_table_as_figure(header, rows, title=None, note=None, fontsize=9.0, max_cell_chars=40) -> \"matplotlib.figure.Figure\"" description: "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." tags: [eda, table, figure, matplotlib, visualization, rasterize, zoom, render, datascience, impure] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [matplotlib] example: | 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") tested: true tests: - "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" test_file_path: "python/functions/datascience/render_table_as_figure_test.py" file_path: "python/functions/datascience/render_table_as_figure.py" params: - name: header desc: "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: rows desc: "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: title desc: "Título opcional dibujado encima de la tabla, en negrita tinta #1b1b1b, alineado a la izquierda. None o \"\" => sin título. Default None." - name: note desc: "Nota opcional al pie de la figura, en gris #8a8a8a e itálica. None o \"\" => sin nota. Default None." - name: fontsize desc: "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: max_cell_chars desc: "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." output: "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 ```python 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.