927437a8d8
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>
250 lines
8.6 KiB
Python
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)
|