--- name: add_xlsx_chart kind: function lang: py domain: infra version: "1.0.0" purity: impure signature: "def add_xlsx_chart(xlsx_path: str, sheet_name: str, chart_type: str, data_range: str, cats_range: str = None, anchor: str = 'H2', title: str = '', x_title: str = '', y_title: str = '') -> dict" description: "Anade un grafico nativo de openpyxl a una hoja EXISTENTE de un libro .xlsx existente, refiriendo rangos de celdas ya escritos. chart_type en {bar, line, pie, scatter} (BarChart/LineChart/PieChart/ScatterChart). data_range y cats_range en notacion Excel tipo 'B1:B10' (se convierten a Reference). anchor = celda destino del chart (ej. 'H2'). Acepta titulo del grafico y de los ejes X/Y. Guarda el libro. Es la pieza que completa el grupo excel para generar hojas con graficos: primero escribir datos (write_xlsx_sheets) y luego anadir el chart. Impura: escribe disco y NO lanza: en fallo (hoja/libro inexistente, chart_type invalido, rango invalido) devuelve {status: 'error', error}." tags: [excel, xlsx, chart, openpyxl, spreadsheet, office, onlyoffice, viz, io, infra] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [openpyxl] params: - name: xlsx_path desc: "Ruta al archivo .xlsx EXISTENTE. Esta funcion NO crea el libro: escribe primero los datos con write_xlsx_sheets/upsert_xlsx_sheet. Vacio o inexistente devuelve {status: 'error'} (no lanza)." - name: sheet_name desc: "Nombre de la hoja (ya existente) donde se ancla el grafico y de la que provienen los rangos. Si no existe, devuelve {status: 'error'} con la lista de hojas disponibles." - name: chart_type desc: "Tipo de grafico. Uno de: 'bar', 'line', 'pie', 'scatter' (case-insensitive, se normaliza). Cualquier otro valor devuelve {status: 'error'} con la lista de validos." - name: data_range desc: "Rango de celdas de los valores a graficar, en notacion Excel tipo 'B1:B10'. Se convierte a openpyxl.chart.Reference (1-indexed). Si abarca la cabecera (fila 1), se toma el nombre de la serie de esa primera celda (titles_from_data). Rango invalido devuelve {status: 'error'}." - name: cats_range desc: "Rango de las categorias/etiquetas del eje X (o labels de pie), tipo 'A2:A10'. None (default) = sin categorias explicitas. Para scatter se usa como valores X (xvalues) de la serie." - name: anchor desc: "Celda destino (esquina superior izquierda) del grafico, p.ej. 'H2'. Default 'H2'. Ancla el chart sin desplazar las celdas de datos." - name: title desc: "Titulo del grafico. Vacio (default) = sin titulo." - name: x_title desc: "Titulo del eje X. Vacio (default) = sin titulo. Ignorado por pie (no tiene ejes)." - name: y_title desc: "Titulo del eje Y. Vacio (default) = sin titulo. Ignorado por pie (no tiene ejes)." output: "Dict. En exito: {status: 'ok', chart_type: , sheet: , anchor: }. En error: {status: 'error', error: ''}." tested: true tests: ["test_add_bar_chart_reabre_y_verifica", "test_add_line_chart", "test_add_pie_chart", "test_add_scatter_chart", "test_dos_charts_en_la_misma_hoja", "test_chart_type_invalido_devuelve_error", "test_hoja_inexistente_devuelve_error", "test_libro_inexistente_devuelve_error", "test_data_range_invalido_devuelve_error", "test_xlsx_path_vacio_devuelve_error"] test_file_path: "python/functions/infra/add_xlsx_chart_test.py" file_path: "python/functions/infra/add_xlsx_chart.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from infra.write_xlsx_sheets import write_xlsx_sheets from infra.add_xlsx_chart import add_xlsx_chart # 1) Escribe los datos (cabecera + filas) write_xlsx_sheets("/tmp/ventas_chart.xlsx", { "Ventas": [ ["Mes", "Unidades"], ["Ene", 120], ["Feb", 150], ["Mar", 90], ["Abr", 200], ], }) # 2) Anade un grafico de barras refiriendo los rangos ya escritos res = add_xlsx_chart( xlsx_path="/tmp/ventas_chart.xlsx", sheet_name="Ventas", chart_type="bar", data_range="B1:B5", # incluye la cabecera 'Unidades' -> nombre de la serie cats_range="A2:A5", # meses como categorias del eje X anchor="D2", # esquina superior izquierda del chart title="Unidades por mes", x_title="Mes", y_title="Unidades", ) print(res) # {'status': 'ok', 'chart_type': 'bar', 'sheet': 'Ventas', 'anchor': 'D2'} # Verificar que el chart quedo en la hoja from openpyxl import load_workbook wb = load_workbook("/tmp/ventas_chart.xlsx") print(len(wb["Ventas"]._charts)) # 1 ``` ## Cuando usarla Usala cuando necesites **generar una hoja de Excel con un grafico** a partir de datos que ya escribiste en el libro: dashboards exportables, reports con visualizacion embebida, resumenes que se abren en Excel/OnlyOffice mostrando el chart. Es el ultimo paso del flujo del grupo `excel`: `write_xlsx_sheets` (o `upsert_xlsx_sheet`) escribe los datos, y esta funcion les anade el grafico refiriendo sus rangos. Llamala una vez por grafico (puedes anadir varios a la misma hoja con distintos `anchor`). ## Gotchas - **Impura — escribe en disco.** Reabre el libro, anade el chart y lo GUARDA. No lanza: devuelve `{"status": "error", ...}` ante libro inexistente, hoja inexistente, `chart_type` invalido, rango invalido o openpyxl ausente. - **El libro DEBE existir.** Esta funcion no crea el .xlsx ni escribe datos: los rangos (`data_range`, `cats_range`) deben apuntar a celdas YA escritas. Escribe primero con `write_xlsx_sheets`/`upsert_xlsx_sheet`. - **openpyxl carga el libro entero en memoria** para reabrirlo y reescribirlo. Para libros muy grandes esto consume RAM proporcional al tamano. - **Los `Reference` de openpyxl son 1-indexed** (fila 1, columna 1 = A1). La conversion desde notacion `'B1:B10'` la hace `range_boundaries` internamente; si pasas un rango mal formado, devuelve error en vez de un chart vacio. - **`titles_from_data`**: si `data_range` incluye la fila 1 (cabecera), el nombre de la serie se toma de esa primera celda. Si tu `data_range` empieza en fila 2 (solo datos), la serie queda sin nombre — incluye la cabecera para etiquetarla. - **scatter es distinto**: usa `data_range` como valores Y y `cats_range` como valores X (xvalues) de una unica serie via `Series`. No usa `set_categories` como bar/line/pie. Para scatter, pasa rangos de SOLO datos (sin cabecera) en ambos. - **pie ignora `x_title`/`y_title`** (no tiene ejes). Pasarlos no falla, se ignoran silenciosamente. - **El chart NO se recalcula solo**: openpyxl escribe la definicion del grafico; Excel/LibreOffice lo renderiza al abrir. Si cambias los datos despues, vuelve a llamar a la funcion o edita el rango — el chart referencia celdas, asi que reflejara el valor que tengan al abrir el libro. - **Requiere openpyxl** (ya instalado en `python/.venv`, version 3.1.5).