---
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: <str normalizado>, sheet: <str>, anchor: <str>}. En error: {status: 'error', error: '<mensaje>'}."
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).
