Files
fn_registry/python/functions/infra/add_xlsx_chart.py
T
egutierrez 927437a8d8 feat(infra): grupo claude-fleet — FleetView TUI + orquestacion de Claudes
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una
sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI:

- list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals +
  runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId.
- list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables.
- wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console
  (preserva ancho del sidebar), tmux_map_claude_panes.
- launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r,
  mouse on, remain-on-exit off; fija el ancho del sidebar con hooks.
- docs/capabilities/claude-fleet.md + entrada en el INDEX.

Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes
varios de docs e infra de otra sesion, agrupados aqui para no perderlos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:04:41 +02:00

250 lines
8.6 KiB
Python

"""Anade un grafico nativo de openpyxl a una hoja existente de un libro .xlsx.
Funcion impura: abre un libro Excel existente, crea un objeto Chart de openpyxl
(BarChart/LineChart/PieChart/ScatterChart) sobre rangos de celdas YA escritos, lo
ancla en una celda destino y guarda el libro. Es la pieza que faltaba en el grupo
`excel` para producir hojas de Excel con graficos: primero se escriben los datos
(p.ej. con write_xlsx_sheets) y luego se les anade el chart refiriendo sus rangos.
No lanza: cualquier fallo (libro inexistente, hoja inexistente, chart_type
invalido, openpyxl ausente) se devuelve como dict {"status": "error", ...}.
"""
import os
# chart_type -> clase de openpyxl.chart. Se resuelve perezosamente en runtime
# para no fallar al importar el modulo si openpyxl no esta instalado.
_VALID_CHART_TYPES = ("bar", "line", "pie", "scatter")
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:
"""Anade un grafico nativo a una hoja existente de un libro existente.
Args:
xlsx_path: Ruta al archivo .xlsx existente. Debe existir (esta funcion no
crea el libro: escribe primero los datos con otra funcion del grupo).
sheet_name: Nombre de la hoja (ya existente) donde se ancla el grafico y
de la que provienen los rangos.
chart_type: Tipo de grafico. Uno de: "bar", "line", "pie", "scatter".
data_range: Rango de celdas de los valores a graficar, en notacion Excel
tipo "B1:B10". Se convierte a openpyxl.chart.Reference. Si abarca la
cabecera (fila 1), se pasa titles_from_data=True para tomar el nombre
de la serie de esa primera celda.
cats_range: Rango de las categorias/etiquetas del eje X (o labels de pie),
tipo "A2:A10". None (default) = sin categorias explicitas. Ignorado
para scatter (que usa xvalues, ver Gotchas).
anchor: Celda destino (esquina superior izquierda) del grafico, p.ej.
"H2". Default "H2".
title: Titulo del grafico. Vacio (default) = sin titulo.
x_title: Titulo del eje X. Vacio (default) = sin titulo. Ignorado por pie.
y_title: Titulo del eje Y. Vacio (default) = sin titulo. Ignorado por pie.
Returns:
Dict. En exito:
{"status": "ok", "chart_type": <str>, "sheet": <str>, "anchor": <str>}.
En error:
{"status": "error", "error": "<mensaje>"}.
"""
if not xlsx_path:
return {"status": "error", "error": "xlsx_path no puede estar vacio"}
ct = (chart_type or "").strip().lower()
if ct not in _VALID_CHART_TYPES:
return {
"status": "error",
"error": (
f"chart_type invalido: '{chart_type}'. "
f"Validos: {list(_VALID_CHART_TYPES)}"
),
}
abs_path = os.path.abspath(xlsx_path)
if not os.path.exists(abs_path):
return {"status": "error", "error": f"libro no encontrado: {abs_path}"}
try:
from openpyxl import load_workbook
from openpyxl.chart import (
BarChart,
LineChart,
PieChart,
Reference,
ScatterChart,
Series,
)
from openpyxl.utils.cell import range_boundaries
except ImportError: # pragma: no cover - dependencia del entorno
return {
"status": "error",
"error": (
"openpyxl es requerido para add_xlsx_chart. "
"Instalar con: cd python && uv add openpyxl"
),
}
try:
wb = load_workbook(abs_path)
except Exception as exc: # noqa: BLE001 - contrato del grupo: no lanzar
return {"status": "error", "error": f"no se pudo abrir el libro: {exc}"}
if sheet_name not in wb.sheetnames:
return {
"status": "error",
"error": (
f"hoja '{sheet_name}' no existe. "
f"Hojas disponibles: {wb.sheetnames}"
),
}
ws = wb[sheet_name]
# Convierte "B1:B10" -> (min_col, min_row, max_col, max_row) en 1-index.
try:
d_min_col, d_min_row, d_max_col, d_max_row = range_boundaries(data_range)
except Exception as exc: # noqa: BLE001
return {
"status": "error",
"error": f"data_range invalido '{data_range}': {exc}",
}
chart_classes = {
"bar": BarChart,
"line": LineChart,
"pie": PieChart,
"scatter": ScatterChart,
}
try:
chart = chart_classes[ct]()
if title:
chart.title = title
if ct == "scatter":
# Scatter empareja X (cats_range) con Y (data_range) como una serie.
chart.style = 13
if x_title:
chart.x_axis.title = x_title
if y_title:
chart.y_axis.title = y_title
yvalues = Reference(
ws,
min_col=d_min_col,
min_row=d_min_row,
max_col=d_max_col,
max_row=d_max_row,
)
if cats_range:
try:
x_min_col, x_min_row, x_max_col, x_max_row = range_boundaries(
cats_range
)
except Exception as exc: # noqa: BLE001
return {
"status": "error",
"error": f"cats_range invalido '{cats_range}': {exc}",
}
xvalues = Reference(
ws,
min_col=x_min_col,
min_row=x_min_row,
max_col=x_max_col,
max_row=x_max_row,
)
series = Series(yvalues, xvalues, title_from_data=False)
else:
series = Series(yvalues, title_from_data=False)
chart.series.append(series)
else:
# bar/line/pie: add_data + set_categories.
if ct in ("bar", "line"):
if x_title:
chart.x_axis.title = x_title
if y_title:
chart.y_axis.title = y_title
# titles_from_data toma el nombre de serie de la primera fila del
# rango cuando este incluye la cabecera (fila 1).
from_data = d_min_row == 1
data = Reference(
ws,
min_col=d_min_col,
min_row=d_min_row,
max_col=d_max_col,
max_row=d_max_row,
)
chart.add_data(data, titles_from_data=from_data)
if cats_range:
try:
c_min_col, c_min_row, c_max_col, c_max_row = range_boundaries(
cats_range
)
except Exception as exc: # noqa: BLE001
return {
"status": "error",
"error": f"cats_range invalido '{cats_range}': {exc}",
}
cats = Reference(
ws,
min_col=c_min_col,
min_row=c_min_row,
max_col=c_max_col,
max_row=c_max_row,
)
chart.set_categories(cats)
ws.add_chart(chart, anchor)
wb.save(abs_path)
except Exception as exc: # noqa: BLE001 - contrato del grupo: no lanzar
return {"status": "error", "error": f"no se pudo anadir el grafico: {exc}"}
return {
"status": "ok",
"chart_type": ct,
"sheet": sheet_name,
"anchor": anchor,
}
if __name__ == "__main__": # pragma: no cover - smoke manual
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from infra.write_xlsx_sheets import write_xlsx_sheets
tmp = os.path.join(tempfile.gettempdir(), "add_xlsx_chart_demo.xlsx")
write_xlsx_sheets(
tmp,
{
"Ventas": [
["Mes", "Unidades"],
["Ene", 120],
["Feb", 150],
["Mar", 90],
["Abr", 200],
],
},
)
res = add_xlsx_chart(
xlsx_path=tmp,
sheet_name="Ventas",
chart_type="bar",
data_range="B1:B5", # incluye cabecera "Unidades" -> nombre de serie
cats_range="A2:A5", # meses
anchor="D2",
title="Unidades por mes",
x_title="Mes",
y_title="Unidades",
)
print(res)