---
name: render_automatic_eda_pdf
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def render_automatic_eda_pdf(chapters_or_profile, out_path: str, meta: dict = None) -> dict"
description: "Renderiza un documento AutomaticEDA por CAPÍTULOS (modelo de bloques independiente del formato) en un PDF A5 retrato pensado para LEER EN EL MÓVIL. Acepta una lista de capítulos del modelo o directamente un TableProfile del grupo eda (en cuyo caso construye los capítulos canónicos con build_document). El paginador MIDE cada bloque y NUNCA corta nada: el texto se envuelve a líneas completas, las tablas largas se parten por filas REPITIENDO la cabecera, figuras e imágenes se escalan para caber enteras. Cada capítulo empieza en página nueva con pie 'Capítulo · vX.Y.Z' y se escribe un manifiesto automatic_eda_manifest.json junto a la salida para seguimiento por capítulo. dict-no-throw: nunca lanza, devuelve {path, n_pages, chapters, manifest_path, note}. Motor matplotlib PdfPages. Aditivo: NO reemplaza render_eda_pdf."
tags: [eda, pdf, render, report, mobile, automatic-eda, chapters, versioned, no-cut, pagination, matplotlib, datascience, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [os, matplotlib, "datascience.automatic_eda"]
params:
  - name: chapters_or_profile
    desc: "una lista de capítulos del modelo AutomaticEDA (dataclasses Chapter o dicts {id,title,version,blocks}) O un TableProfile dict del grupo eda. Si es un TableProfile, los capítulos canónicos se construyen con build_document(profile, meta['ctx']). Un capítulo es {id,title,version,blocks}; un bloque es uno de: heading, markdown, kv_table, data_table, figure, image, caption, note. Lectura defensiva: cualquier cosa no reconocida se degrada a Note, nunca lanza."
  - name: out_path
    desc: "ruta del archivo PDF de salida. Los directorios padre se crean si faltan. Si está en un directorio no escribible (p.ej. /proc/...) devuelve {path:None, note:<causa>} sin lanzar."
  - name: meta
    desc: "dict opcional. Claves: title (título de portada/pie), ctx (contexto de presentación pasado a los builders de capítulo cuando se da un profile: dataset_name, source_origin, storage, generated_at, description, granularity, quality_criteria, head_rows...), manifest_path (override; por defecto automatic_eda_manifest.json junto a out_path), write_manifest (False para no escribirlo), generated_at."
output: "dict (nunca lanza): {path: str|None, n_pages: int, chapters: list[{id,version,n_pages}], manifest_path: str|None, note: str}. En éxito path es la ruta escrita, n_pages el total de páginas, chapters el desglose por capítulo para el manifiesto. En error fatal path es None y note explica la causa."
tested: true
tests: ["test_golden_profile_genera_pdf_portada_y_overview", "test_edge_tabla_larga_parte_repitiendo_cabecera", "test_edge_celda_larga_no_se_corta", "test_no_corta_texto_markdown", "test_edge_profile_none_y_vacio_un_pagina", "test_error_path_directorio_no_escribible_no_revienta"]
test_file_path: "python/functions/datascience/render_automatic_eda_pdf_test.py"
file_path: "python/functions/datascience/render_automatic_eda_pdf.py"
---

## Ejemplo

```python
from datascience import render_automatic_eda_pdf

# Caso 1: directamente desde un TableProfile del grupo eda.
# profile = profile_table(db, "ventas", backend="duckdb")["profile"]
profile = {
    "table": "ventas", "source": "/data/ventas.csv",
    "n_rows": 1000, "n_cols": 2, "quality_score": 92.5,
    "columns": [
        {"name": "precio", "inferred_type": "numeric", "null_pct": 0.01,
         "null_count": 10,
         "numeric": {"mean": 42.5, "median": 40.0, "min": 1.0, "max": 100.0,
                     "std": 12.3}},
        {"name": "categoria", "inferred_type": "categorical", "null_pct": 0.0,
         "categorical": {"top": [{"value": "neumaticos", "count": 500},
                                 {"value": "aceite", "count": 300}]}},
    ],
}
res = render_automatic_eda_pdf(
    profile, "reports/ventas_aeda.pdf",
    {"title": "EDA — ventas",
     "ctx": {"dataset_name": "Ventas", "source_origin": "ERP export",
             "description": "Líneas de venta del ERP.",
             "granularity": "Cada fila es una línea de venta."}})
print(res["n_pages"], res["chapters"], res["manifest_path"])
# -> 3 [{'id':'portada','version':'1.0.0','n_pages':1},
#       {'id':'overview','version':'1.0.0','n_pages':2}] reports/automatic_eda_manifest.json

# Caso 2: desde capítulos construidos a mano (modelo de bloques).
from datascience.automatic_eda.model import Chapter, Heading, DataTable
ch = Chapter(id="resumen", title="Resumen", version="1.0.0", blocks=[
    Heading("Tabla", 1),
    DataTable(header=["col", "valor"], rows=[["a", "1"], ["b", "2"]]),
])
render_automatic_eda_pdf([ch], "reports/manual.pdf")
```

## Cuando usarla

Cuando quieras el **PDF móvil del nuevo motor AutomaticEDA por capítulos** (portada
+ overview + los capítulos que existan): después de `profile_table(...)`, pásale el
`profile` y obtienes un PDF A5 retrato versionado por capítulo, con manifiesto. Úsala
como capa de presentación PDF del grupo `eda` cuando necesites **garantía de no-corte**
(texto, tablas e imágenes nunca recortados) y **versionado por capítulo** para mejora
continua. Es el reemplazo evolutivo de `render_eda_pdf`: comparte estética Tufte/móvil
pero separa contenido (capítulos/bloques) de formato (renderer), de modo que el mismo
documento se emite también como PPTX (`render_automatic_eda_pptx`). Para añadir un
capítulo nuevo, ver `docs/capabilities/automatic_eda.md`.

## Gotchas

- **Impura**: escribe el PDF en `out_path` (crea los directorios padre) y, salvo
  `meta['write_manifest']=False`, un `automatic_eda_manifest.json` junto a la salida.
  Backend headless `Agg` de matplotlib (corre en agentes/CI sin display).
- **Nunca lanza** (dict-no-throw): un bloque o capítulo que falle se omite y se anota
  en `note`; el PDF se genera igual. Un profile `None`/`{}` produce un PDF de 1 página
  válido. `out_path` no escribible → `{path: None, note: <causa>}`.
- **No corta nada**: el paginador mide cada bloque con una rejilla de caracteres
  (sobre-estima ligeramente, nunca afirma que algo cabe cuando se desbordaría). El
  texto se envuelve a líneas completas (sin cortar a media palabra), las tablas largas
  se parten por filas **repitiendo la cabecera**, las celdas con texto largo se
  envuelven dentro de su columna (la fila crece), y figuras/imágenes se escalan para
  caber enteras (nunca se recortan).
- **Tablas muy anchas**: con muchas columnas (>10) cada columna se estrecha y su texto
  se envuelve en varias líneas (sigue sin perderse). El reparto por columnas-en-grupos
  para tablas muy anchas es una mejora pendiente (ver capability page).
- **head_rows / examples**: el capítulo Overview muestra `df.head` desde
  `ctx['head_rows']`/`profile['head_rows']` y ejemplos no-nulos desde
  `columns[i]['examples']`; si el profile no los trae (hoy no los trae), degrada con un
  placeholder honesto y deriva los ejemplos de los valores reales del perfil (top
  categóricos, min/median/max numéricos). Documentado en el contrato.
- **Registro en el package**: el `## Ejemplo` usa `from datascience import
  render_automatic_eda_pdf` (añadido al `__init__.py`); el test importa el módulo
  directo para no depender de ese registro.
- **Fechas en UI europeas**: la portada formatea la fecha como `DD/MM/AAAA HH:mm`.
