chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1 del flow 0008 (kanban_cpp + agent_runner_api + DoD schema). Incluye: - dev/flows/0008-kanban-cpp-and-agent-workflows.md - dev/issues/0112-0119*.md (7 sub-issues) - WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+40
-4
@@ -381,10 +381,16 @@ if(EXISTS ${_PG_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_PG_DIR} ${CMAKE_BINARY_DIR}/apps/primitives_gallery)
|
||||
endif()
|
||||
|
||||
# --- Tables playground (vive dentro de primitives_gallery/playground/tables/) ---
|
||||
if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt)
|
||||
add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables)
|
||||
endif()
|
||||
# --- Tables playground DEPRECATED (issue 0108) ---
|
||||
# Sustituido por apps/tables_qa. El playground legacy queda solo como historia
|
||||
# del split data_table 0107c. NO se builda mas — su self_test (430 checks
|
||||
# contra logica legacy) ya esta cubierto por:
|
||||
# - cpp/tests/ (Catch2 unit tests de la logica pura del registry)
|
||||
# - apps/tables_qa/ (testbed del modulo data_table v2.0.0+)
|
||||
# Para revivirlo (temporal, debugging): descomentar el bloque if(EXISTS ...).
|
||||
# if(EXISTS ${_PG_DIR}/playground/tables/CMakeLists.txt)
|
||||
# add_subdirectory(${_PG_DIR}/playground/tables ${CMAKE_BINARY_DIR}/apps/primitives_gallery/playground/tables)
|
||||
# endif()
|
||||
|
||||
# --- text_editor + file_watcher smoke test (lives in apps/) ---
|
||||
if(NOT DEFINED _TES_DIR)
|
||||
@@ -493,3 +499,33 @@ set(_APP_HUB_LAUNCHER_DIR ${CMAKE_SOURCE_DIR}/../apps/app_hub_launcher)
|
||||
if(EXISTS ${_APP_HUB_LAUNCHER_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_APP_HUB_LAUNCHER_DIR} ${CMAKE_BINARY_DIR}/apps/app_hub_launcher)
|
||||
endif()
|
||||
|
||||
# --- services_monitor (lives in apps/, issue 0096) ---
|
||||
set(_SERVICES_MONITOR_DIR ${CMAKE_SOURCE_DIR}/../apps/services_monitor)
|
||||
if(EXISTS ${_SERVICES_MONITOR_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_SERVICES_MONITOR_DIR} ${CMAKE_BINARY_DIR}/apps/services_monitor)
|
||||
endif()
|
||||
|
||||
# --- app_gestion (lives in apps/, issue 0096) ---
|
||||
set(_APP_GESTION_DIR ${CMAKE_SOURCE_DIR}/../apps/app_gestion)
|
||||
if(EXISTS ${_APP_GESTION_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_APP_GESTION_DIR} ${CMAKE_BINARY_DIR}/apps/app_gestion)
|
||||
endif()
|
||||
|
||||
# --- skill_tree (lives in apps/, issue 0096) ---
|
||||
set(_SKILL_TREE_DIR ${CMAKE_SOURCE_DIR}/../apps/skill_tree)
|
||||
if(EXISTS ${_SKILL_TREE_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_SKILL_TREE_DIR} ${CMAKE_BINARY_DIR}/apps/skill_tree)
|
||||
endif()
|
||||
|
||||
# --- tables_qa (lives in apps/, issue 0096) ---
|
||||
set(_TABLES_QA_DIR ${CMAKE_SOURCE_DIR}/../apps/tables_qa)
|
||||
if(EXISTS ${_TABLES_QA_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_TABLES_QA_DIR} ${CMAKE_BINARY_DIR}/apps/tables_qa)
|
||||
endif()
|
||||
|
||||
# --- process_explorer (lives in apps/, issue 0096) ---
|
||||
set(_PROCESS_EXPLORER_DIR ${CMAKE_SOURCE_DIR}/../apps/process_explorer)
|
||||
if(EXISTS ${_PROCESS_EXPLORER_DIR}/CMakeLists.txt)
|
||||
add_subdirectory(${_PROCESS_EXPLORER_DIR} ${CMAKE_BINARY_DIR}/apps/process_explorer)
|
||||
endif()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "version_generated.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_glfw.h"
|
||||
#include "imgui_impl_opengl3.h"
|
||||
#include "implot.h"
|
||||
@@ -16,9 +17,11 @@
|
||||
#include "core/log_window.h"
|
||||
#include "core/layout_storage.h"
|
||||
#include "gfx/gl_loader.h"
|
||||
#include "app_modules.h"
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
@@ -26,6 +29,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
@@ -224,6 +228,43 @@ static void prune_dead_icon_attached() {
|
||||
}
|
||||
}
|
||||
|
||||
// Pinta la title bar (caption + bordes) en oscuro via DWM, para que el
|
||||
// header del SO no se quede blanco mientras el cliente es dark. DWM la pinta
|
||||
// el OS, no GLFW/ImGui — sin esta llamada queda en blanco aunque el resto
|
||||
// este oscuro.
|
||||
//
|
||||
// Carga dwmapi.dll dinamicamente: evita anadir la dep al toolchain. Si la
|
||||
// DLL/atributo no existe (Win10 < 1809), no hace nada — silencioso.
|
||||
// Attr 20 = DWMWA_USE_IMMERSIVE_DARK_MODE (Win11 / Win10 >= build 18985).
|
||||
// Attr 19 = nombre antiguo (Win10 1809..18984). Probar 20 primero, fallback 19.
|
||||
// Force repaint con SWP_FRAMECHANGED — la ventana ya fue mostrada por GLFW
|
||||
// antes de que lleguemos aqui.
|
||||
typedef HRESULT (WINAPI *PFN_DwmSetWindowAttribute)(HWND, DWORD, LPCVOID, DWORD);
|
||||
static std::unordered_set<HWND> g_dark_titlebar_applied;
|
||||
static void attach_dark_titlebar_to_hwnd(HWND hwnd, bool dark) {
|
||||
if (!hwnd) return;
|
||||
if (g_dark_titlebar_applied.count(hwnd)) return; // idempotent
|
||||
static HMODULE h_dwmapi = LoadLibraryW(L"dwmapi.dll");
|
||||
if (!h_dwmapi) return;
|
||||
static auto p_set = (PFN_DwmSetWindowAttribute)GetProcAddress(h_dwmapi, "DwmSetWindowAttribute");
|
||||
if (!p_set) return;
|
||||
BOOL value = dark ? TRUE : FALSE;
|
||||
HRESULT hr = p_set(hwnd, 20 /* DWMWA_USE_IMMERSIVE_DARK_MODE */, &value, sizeof(value));
|
||||
if (FAILED(hr)) {
|
||||
p_set(hwnd, 19 /* legacy name on Win10 1809..18984 */, &value, sizeof(value));
|
||||
}
|
||||
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||||
g_dark_titlebar_applied.insert(hwnd);
|
||||
}
|
||||
|
||||
static void prune_dead_dark_titlebar() {
|
||||
for (auto it = g_dark_titlebar_applied.begin(); it != g_dark_titlebar_applied.end();) {
|
||||
if (!IsWindow(*it)) it = g_dark_titlebar_applied.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
}
|
||||
|
||||
static void install_sizemove_subclass(GLFWwindow* w) {
|
||||
if (!w) return;
|
||||
install_sizemove_subclass_hwnd(glfwGetWin32Window(w));
|
||||
@@ -391,6 +432,221 @@ const char* framework_description() {
|
||||
return FN_MODULE_FRAMEWORK_DESCRIPTION;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Header badge overlay — identidad por app en viewports secundarios.
|
||||
// ----------------------------------------------------------------------------
|
||||
// Cuando una app C++ tiene N panels y el usuario arrastra varios fuera del
|
||||
// main window, cada panel se convierte en su propio OS viewport. Sin marcas
|
||||
// visuales adicionales, si tienes 3 apps abiertas a la vez no sabes de cual
|
||||
// viene cada panel flotante. Este overlay dibuja un cuadrado redondeado de
|
||||
// ~18px con la inicial de la app en la esquina top-left de la title bar de
|
||||
// cada viewport secundario. Solo en secundarios — el main ya tiene icono
|
||||
// del SO en titlebar/taskbar (attach_app_icon_to_hwnd).
|
||||
//
|
||||
// Filosofia:
|
||||
// - Defaults producen identidad util sin tocar la app (color hash-derivado
|
||||
// desde about.name, glyph = primera letra).
|
||||
// - Apps con icon.accent en su app.md pueden pasar el mismo hex para
|
||||
// coherencia con App Hub.
|
||||
// - ForegroundDrawList: dibuja por encima del titlebar de ImGui sin
|
||||
// necesidad de envolver Begin() ni hookear el render del titulo.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Parsea "#RRGGBB" o "RRGGBB" a ImU32 ABGR. Devuelve 0 si invalido.
|
||||
static ImU32 parse_hex_color_abgr(const char* s) {
|
||||
if (!s || !*s) return 0;
|
||||
if (*s == '#') ++s;
|
||||
auto h = [](char c) -> int {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
};
|
||||
int v[6];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
v[i] = h(s[i]);
|
||||
if (v[i] < 0) return 0;
|
||||
}
|
||||
int r = (v[0] << 4) | v[1];
|
||||
int g = (v[2] << 4) | v[3];
|
||||
int b = (v[4] << 4) | v[5];
|
||||
return IM_COL32(r, g, b, 255);
|
||||
}
|
||||
|
||||
// Hash-derivado: FNV-1a 32-bit -> H en [0,360), S=0.58, V=0.78 -> ImU32 ABGR.
|
||||
// Estable por nombre, distribuye razonablemente entre N apps.
|
||||
static ImU32 derive_color_from_name(const char* name) {
|
||||
if (!name || !*name) name = "fn_registry";
|
||||
unsigned h = 2166136261u;
|
||||
for (const char* p = name; *p; ++p) {
|
||||
h ^= (unsigned char)*p;
|
||||
h *= 16777619u;
|
||||
}
|
||||
float hue = (float)(h % 360u);
|
||||
float s = 0.58f, v = 0.78f;
|
||||
float c = v * s;
|
||||
float hp = hue / 60.0f;
|
||||
float x = c * (1.0f - std::fabs(std::fmod(hp, 2.0f) - 1.0f));
|
||||
float r=0, g=0, b=0;
|
||||
if (hp < 1) { r=c; g=x; }
|
||||
else if (hp < 2) { r=x; g=c; }
|
||||
else if (hp < 3) { g=c; b=x; }
|
||||
else if (hp < 4) { g=x; b=c; }
|
||||
else if (hp < 5) { r=x; b=c; }
|
||||
else { r=c; b=x; }
|
||||
float m = v - c;
|
||||
int R = (int)((r + m) * 255.0f + 0.5f);
|
||||
int G = (int)((g + m) * 255.0f + 0.5f);
|
||||
int B = (int)((b + m) * 255.0f + 0.5f);
|
||||
return IM_COL32(R, G, B, 255);
|
||||
}
|
||||
|
||||
// Decide string a renderizar como glyph. Devuelve puntero a buffer estatico
|
||||
// thread-local cuando hace falta normalizar la primera letra del nombre.
|
||||
static const char* resolve_badge_glyph(const AppConfig& cfg) {
|
||||
const char* g = cfg.header_badge.glyph;
|
||||
if (g && *g) return g;
|
||||
static thread_local char letter[8] = {0};
|
||||
const char* nm = (cfg.about.name && *cfg.about.name) ? cfg.about.name : cfg.title;
|
||||
char first = (nm && *nm) ? nm[0] : '?';
|
||||
if (first >= 'a' && first <= 'z') first = (char)(first - 'a' + 'A');
|
||||
letter[0] = first;
|
||||
letter[1] = '\0';
|
||||
return letter;
|
||||
}
|
||||
|
||||
// Color final con precedencia:
|
||||
// 1) Override explicito en cfg.header_badge.accent_hex (main.cpp)
|
||||
// 2) Codegen extern fn::app_header_accent_hex (icon.accent del app.md)
|
||||
// 3) Hash-derived desde about.name (siempre estable, da identidad gratis)
|
||||
static ImU32 resolve_badge_color(const AppConfig& cfg) {
|
||||
ImU32 c = parse_hex_color_abgr(cfg.header_badge.accent_hex);
|
||||
if (c != 0) return c;
|
||||
c = parse_hex_color_abgr(app_header_accent_hex);
|
||||
if (c != 0) return c;
|
||||
const char* nm = (cfg.about.name && *cfg.about.name) ? cfg.about.name : cfg.title;
|
||||
return derive_color_from_name(nm);
|
||||
}
|
||||
|
||||
// Textura GL del icono de la app — extraida del HICON embebido en el .exe
|
||||
// (resource ID 101 generado por add_imgui_app desde appicon.ico). Cargada
|
||||
// perezosamente al primer frame y reutilizada en cada draw. 0 = no disponible.
|
||||
static GLuint g_app_icon_texture = 0;
|
||||
static int g_app_icon_size = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
// Carga el icono embebido a 32x32 y sube como textura GL RGBA8. Linear
|
||||
// filtering para que escalar 32->18 sea suave. Devuelve 0 si falla.
|
||||
static GLuint upload_hicon_to_gl_texture() {
|
||||
const int sz = 32;
|
||||
HICON hicon = (HICON)LoadImageW(GetModuleHandleW(nullptr),
|
||||
MAKEINTRESOURCEW(FN_APP_ICON_RES_ID),
|
||||
IMAGE_ICON, sz, sz,
|
||||
LR_DEFAULTCOLOR);
|
||||
if (!hicon) return 0;
|
||||
|
||||
ICONINFO ii{};
|
||||
if (!GetIconInfo(hicon, &ii)) { DestroyIcon(hicon); return 0; }
|
||||
|
||||
HDC hdc = CreateCompatibleDC(nullptr);
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = sz;
|
||||
bmi.bmiHeader.biHeight = -sz; // top-down
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
std::vector<unsigned char> pixels(sz * sz * 4, 0);
|
||||
int ok = GetDIBits(hdc, ii.hbmColor, 0, sz, pixels.data(), &bmi, DIB_RGB_COLORS);
|
||||
DeleteDC(hdc);
|
||||
if (ii.hbmColor) DeleteObject(ii.hbmColor);
|
||||
if (ii.hbmMask) DeleteObject(ii.hbmMask);
|
||||
DestroyIcon(hicon);
|
||||
if (ok == 0) return 0;
|
||||
|
||||
// BGRA -> RGBA (Windows DIB es BGRA).
|
||||
for (int i = 0; i < sz * sz; ++i) {
|
||||
unsigned char b = pixels[i*4 + 0];
|
||||
unsigned char r = pixels[i*4 + 2];
|
||||
pixels[i*4 + 0] = r;
|
||||
pixels[i*4 + 2] = b;
|
||||
}
|
||||
|
||||
GLuint tex = 0;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sz, sz, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
pixels.data());
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return tex;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Itera todas las ventanas ImGui y pinta el badge en la title bar de las que
|
||||
// viven en un viewport secundario (panel arrastrado fuera del main window).
|
||||
// Usa imgui_internal.h para acceder a g.Windows y a TitleBarRect(), y dibuja
|
||||
// directamente en la DrawList propia de cada ventana — asi el badge va en
|
||||
// el mismo paso de render que el resto del panel, sin depender de
|
||||
// ForegroundDrawList del viewport (que no se renderiza en algunas combos de
|
||||
// backend + multi-viewport).
|
||||
//
|
||||
// Si tenemos icono GL cargado (Windows con appicon.ico embebido), se dibuja
|
||||
// el icono real (mismo bitmap que el taskbar). Sin icono, fallback a
|
||||
// cuadrado redondeado del color accent con la inicial blanca del app name.
|
||||
//
|
||||
// Filtro de ventanas:
|
||||
// - Activa, no Hidden, no Collapsed.
|
||||
// - No es child window.
|
||||
// - No es popup/tooltip/menu (NoTitleBar => skip).
|
||||
// - No esta dockeada en un nodo (DockIsActive => su titlebar es tabbar del
|
||||
// host; el host window aparece en g.Windows por separado y SI recibe badge).
|
||||
// - Su viewport != main viewport.
|
||||
static void draw_header_badge_on_floating_panels(const AppConfig& cfg) {
|
||||
if (!cfg.header_badge.enabled) return;
|
||||
ImGuiContext& g = *ImGui::GetCurrentContext();
|
||||
ImGuiViewport* main_vp = ImGui::GetMainViewport();
|
||||
|
||||
const ImU32 bg = resolve_badge_color(cfg);
|
||||
const char* glyph = resolve_badge_glyph(cfg);
|
||||
const float sz = cfg.header_badge.size_px > 4.0f ? cfg.header_badge.size_px : 18.0f;
|
||||
const float mg = cfg.header_badge.margin_px >= 0.0f ? cfg.header_badge.margin_px : 6.0f;
|
||||
const float round = sz * 0.22f;
|
||||
const bool has_texture = (g_app_icon_texture != 0);
|
||||
|
||||
for (int i = 0; i < g.Windows.Size; ++i) {
|
||||
ImGuiWindow* w = g.Windows[i];
|
||||
if (!w || !w->WasActive || w->Hidden) continue;
|
||||
if (w->Flags & ImGuiWindowFlags_ChildWindow) continue;
|
||||
if (w->Flags & ImGuiWindowFlags_NoTitleBar) continue;
|
||||
if (w->DockIsActive) continue; // titlebar reemplazado por tab bar del host
|
||||
if (w->Viewport == nullptr || w->Viewport == main_vp) continue;
|
||||
if (w->Collapsed) continue;
|
||||
|
||||
ImRect tb = w->TitleBarRect();
|
||||
ImVec2 p0(tb.Min.x + mg, tb.Min.y + (tb.GetHeight() - sz) * 0.5f);
|
||||
ImVec2 p1(p0.x + sz, p0.y + sz);
|
||||
ImDrawList* dl = w->DrawList;
|
||||
|
||||
if (has_texture) {
|
||||
dl->AddImageRounded((ImTextureID)(intptr_t)g_app_icon_texture,
|
||||
p0, p1, ImVec2(0,0), ImVec2(1,1),
|
||||
IM_COL32_WHITE, round);
|
||||
} else {
|
||||
dl->AddRectFilled(p0, p1, bg, round);
|
||||
ImVec2 ts = ImGui::CalcTextSize(glyph);
|
||||
ImVec2 tp(p0.x + (sz - ts.x) * 0.5f,
|
||||
p0.y + (sz - ts.y) * 0.5f);
|
||||
dl->AddText(tp, IM_COL32(255, 255, 255, 255), glyph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
// Logger primero para capturar fallos del propio init (GLFW, ventana, GL).
|
||||
if (config.log.file_path != nullptr) {
|
||||
@@ -460,6 +716,14 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
// barra de tareas, Alt+Tab y title bar (GLFW no propaga el icono de
|
||||
// recursos del .exe a su WNDCLASS por defecto).
|
||||
attach_app_icon_to_hwnd(glfwGetWin32Window(window));
|
||||
|
||||
// Title bar oscuro (DWM) si el tema lo es. Sin esto el header del SO
|
||||
// queda blanco aunque el cliente sea dark.
|
||||
{
|
||||
const bool dark = (config.theme == ThemeMode::FnDark ||
|
||||
config.theme == ThemeMode::ImGuiDark);
|
||||
attach_dark_titlebar_to_hwnd(glfwGetWin32Window(window), dark);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Carga punteros a funciones GL >= 2.0 si la app lo pide. En Linux es
|
||||
@@ -484,6 +748,14 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
// Multi-viewport docking: payload viewport y target viewport swappean
|
||||
// buffers independientes, asi que los dock preview overlays (los rects
|
||||
// azul/gris que indican zonas droppeables) parecen vibrar 1px contra el
|
||||
// payload arrastrado. Con TransparentPayload el payload se vuelve
|
||||
// invisible al arrastrar y los rects solo se pintan en el target ->
|
||||
// ningun desync visible. Recomendado por upstream cuando "rendering of
|
||||
// multiple viewport cannot be synced".
|
||||
io.ConfigDockingTransparentPayload = true;
|
||||
// Title-bar-only move for ImGui windows. Critical for secondary viewports
|
||||
// (floating panels) whose entire OS window is a single borderless ImGui
|
||||
// window: without this flag, ImGui moves the window when the user drags
|
||||
@@ -625,6 +897,9 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||
prune_dead_subclassed();
|
||||
prune_dead_icon_attached();
|
||||
prune_dead_dark_titlebar();
|
||||
const bool dark_tb = (config.theme == ThemeMode::FnDark ||
|
||||
config.theme == ThemeMode::ImGuiDark);
|
||||
ImGuiPlatformIO& pio_sub = ImGui::GetPlatformIO();
|
||||
for (int i = 0; i < pio_sub.Viewports.Size; ++i) {
|
||||
ImGuiViewport* vp = pio_sub.Viewports[i];
|
||||
@@ -636,6 +911,9 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
// SetClassLongPtrW. WM_SETICON per-HWND es la unica forma de
|
||||
// que el taskbar/titlebar muestren el icono.
|
||||
attach_app_icon_to_hwnd(glfwGetWin32Window(gw));
|
||||
// Misma logica para el title bar oscuro — cada viewport
|
||||
// secundario tiene su propio HWND con caption pintado por DWM.
|
||||
attach_dark_titlebar_to_hwnd(glfwGetWin32Window(gw), dark_tb);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -750,6 +1028,17 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
fps_overlay();
|
||||
}
|
||||
|
||||
// Identidad por app en viewports secundarios — badge en el title bar
|
||||
// de cada panel arrastrado fuera del main window. Si Windows + tiene
|
||||
// appicon.ico embebido, dibuja el mismo icono que el taskbar (PNG
|
||||
// RGBA escalado). Si no, fallback a cuadrado accent + inicial.
|
||||
#ifdef _WIN32
|
||||
if (g_app_icon_texture == 0) {
|
||||
g_app_icon_texture = upload_hicon_to_gl_texture();
|
||||
}
|
||||
#endif
|
||||
draw_header_badge_on_floating_panels(config);
|
||||
|
||||
ImGui::Render();
|
||||
int display_w, display_h;
|
||||
glfwGetFramebufferSize(window, &display_w, &display_h);
|
||||
@@ -800,6 +1089,10 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (g_app_icon_texture != 0) {
|
||||
glDeleteTextures(1, &g_app_icon_texture);
|
||||
g_app_icon_texture = 0;
|
||||
}
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImPlot3D::DestroyContext();
|
||||
|
||||
@@ -171,6 +171,42 @@ struct AppConfig {
|
||||
// render_fn) las ventanas docked aparecen flotantes hasta el siguiente
|
||||
// ciclo. Default null = no-op.
|
||||
std::function<void()> pre_frame{};
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Header badge — identidad visual en viewports secundarios (panels
|
||||
// arrastrados fuera del main window). Cuando un panel se separa, el
|
||||
// framework dibuja un pequeño cuadrado redondeado con la inicial de la
|
||||
// app (o un glyph custom) en la esquina top-left de la title bar de su
|
||||
// viewport, asi distingues de un vistazo de que app viene cada panel
|
||||
// flotante cuando tienes varias apps abiertas a la vez.
|
||||
//
|
||||
// Si todos los campos quedan por defecto, el framework auto-deriva color
|
||||
// estable desde about.name (hash -> HSV) y glyph desde la primera letra.
|
||||
// No requiere accion en la app; con solo declarar about.name ya hay
|
||||
// identidad. Apps que ya tengan icon.accent en su app.md deberian setear
|
||||
// header_badge.accent_hex con el mismo hex para coherencia visual con el
|
||||
// App Hub.
|
||||
// ------------------------------------------------------------------------
|
||||
struct AppHeaderBadge {
|
||||
// Color de fondo del badge en formato "#RRGGBB" o "RRGGBB" sRGB.
|
||||
// "" -> auto-derive desde about.name (hash estable).
|
||||
const char* accent_hex = "";
|
||||
|
||||
// Glyph dibujado en blanco encima del fondo. nullptr/"" -> primera
|
||||
// letra de about.name (uppercase). Soporta cualquier UTF-8 corto
|
||||
// (1-2 chars o un TI_* macro de cpp/functions/core/icons_tabler.h).
|
||||
const char* glyph = nullptr;
|
||||
|
||||
// Tamaño cuadrado del badge en pixels.
|
||||
float size_px = 18.0f;
|
||||
|
||||
// Margen desde top-left del viewport.
|
||||
float margin_px = 6.0f;
|
||||
|
||||
// false -> deshabilita el badge para esta app (capture mode, headless).
|
||||
bool enabled = true;
|
||||
};
|
||||
AppHeaderBadge header_badge{};
|
||||
};
|
||||
|
||||
// Run an ImGui application. The render_fn is called every frame
|
||||
|
||||
@@ -29,4 +29,19 @@ struct ModuleInfo {
|
||||
extern const ModuleInfo app_modules_array[];
|
||||
extern const unsigned long app_modules_count;
|
||||
|
||||
// App identity para el header badge en viewports secundarios (panels arrastrados
|
||||
// fuera del main window). Auto-generados desde el bloque `icon:` del app.md
|
||||
// por codegen_app_modules.py. Permiten que el framework dibuje el cuadrado
|
||||
// accent con la identidad visual de la app sin que main.cpp deba pasar el hex
|
||||
// manualmente.
|
||||
//
|
||||
// app_header_accent_hex: "#RRGGBB" desde icon.accent (default "" si no se
|
||||
// declara — framework cae a hash-derived).
|
||||
// app_header_glyph_name: nombre del glyph Phosphor desde icon.phosphor.
|
||||
// Hoy informativo: el framework C++ no tiene fuente
|
||||
// Phosphor cargada, asi que cae a primera letra de
|
||||
// about.name. Reservado para futuro mapping Tabler.
|
||||
extern const char* const app_header_accent_hex;
|
||||
extern const char* const app_header_glyph_name;
|
||||
|
||||
} // namespace fn
|
||||
|
||||
@@ -60,16 +60,10 @@ void about_window_render() {
|
||||
ImGui::TextWrapped("%s", g_description.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// --- Framework version (issue 0097) ---
|
||||
ImGui::Text("Framework");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("v%s", fn::framework_version());
|
||||
|
||||
// --- Modules consumidos por la app (issue 0097) ---
|
||||
// codegen_app_modules.py auto-prepende `framework_cpp` a uses_modules de
|
||||
// toda app C++, asi que la tabla Modules SIEMPRE lista al framework con
|
||||
// su version + cada modulo declarado en app.md::uses_modules.
|
||||
if (fn::app_modules_count > 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Promovido del playground tables (issue 0081-F). Pura — sin I/O ni estado.
|
||||
|
||||
#include "core/compute_column_stats.h"
|
||||
#include "core/auto_detect_type.h" // parse_number
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// Promovido del playground tables (issue 0081-F). Pura — sin I/O ni estado.
|
||||
#pragma once
|
||||
|
||||
#include "core/auto_detect_type.h" // parse_number reutilizado en la impl
|
||||
// NOTE: auto_detect_type.h (parse_number) is required by the .cpp impl only.
|
||||
// It is NOT included here because data_table_types.h includes this header,
|
||||
// and auto_detect_type.h itself references ColumnType (defined in
|
||||
// data_table_types.h) — that would create a circular include.
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#include "core/compute_ring_layout.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional> // std::hash
|
||||
#include <unordered_map>
|
||||
|
||||
namespace fn_ring {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constantes internas
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static constexpr float kPi = 3.14159265358979323846f;
|
||||
static constexpr float kTwoPi = 2.0f * kPi;
|
||||
|
||||
// Radio minimo para ring 0 cuando ring_radii[0]==0 (evita colocar nodos en el
|
||||
// origen exacto, reservado para HUD).
|
||||
static constexpr float kRing0InnerMin = 30.0f;
|
||||
|
||||
// Umbral: si el bin tiene mas nodos de los que caben radialmente con separacion
|
||||
// minima MIN_RADIAL_SPACING, activamos jitter angular.
|
||||
static constexpr float kMinRadialSpacing = 18.0f;
|
||||
|
||||
// Amplitud maxima del jitter angular: ±0.4 del half-sector (se mantiene dentro
|
||||
// del sector para no solapar el vecino).
|
||||
static constexpr float kJitterMaxFraction = 0.4f;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Hash deterministico FNV-1a 32 bits (estable cross-platform, sin depender de
|
||||
/// std::hash<string> que puede variar entre ABI/compiladores).
|
||||
static uint32_t fnv1a_32(const std::string& s) {
|
||||
uint32_t h = 2166136261u;
|
||||
for (unsigned char c : s) {
|
||||
h ^= c;
|
||||
h *= 16777619u;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/// Devuelve valor deterministico en [-1.0, +1.0] usando el hash del id.
|
||||
static float id_jitter(const std::string& id) {
|
||||
uint32_t h = fnv1a_32(id);
|
||||
// Normalizar a [-1, +1]
|
||||
float v = static_cast<float>(h) / static_cast<float>(0xFFFFFFFFu); // [0,1]
|
||||
return v * 2.0f - 1.0f; // [-1,+1]
|
||||
}
|
||||
|
||||
/// Devuelve el mapa canonico de status → ring.
|
||||
static StatusRingMap default_status_map() {
|
||||
return {
|
||||
{"completado", 0},
|
||||
{"completed", 0},
|
||||
{"in-progress", 1},
|
||||
{"pendiente_unlocked", 2},
|
||||
{"unlocked", 2},
|
||||
{"pending", 2},
|
||||
{"pendiente", 3},
|
||||
{"locked", 3},
|
||||
{"deferred", 4},
|
||||
{"bloqueado", 4},
|
||||
};
|
||||
}
|
||||
|
||||
/// Busca status en el mapa. Retorna -1 si no encontrado.
|
||||
static int lookup_ring(const std::string& status, const StatusRingMap& smap) {
|
||||
for (auto& [k, v] : smap) {
|
||||
if (k == status) return v;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// Busca domain en el orden. Retorna n_sectors-1 si no encontrado.
|
||||
static int lookup_sector(const std::string& domain,
|
||||
const DomainOrder& order,
|
||||
int n_sectors) {
|
||||
for (int i = 0; i < static_cast<int>(order.size()); ++i) {
|
||||
if (order[i] == domain) return i;
|
||||
}
|
||||
return n_sectors - 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Implementacion principal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<LayoutOutput>
|
||||
compute_ring_layout(const std::vector<LayoutInput>& nodes,
|
||||
const LayoutConfig& cfg,
|
||||
const StatusRingMap& status_map_in,
|
||||
const DomainOrder& domain_order) {
|
||||
if (nodes.empty()) return {};
|
||||
|
||||
const StatusRingMap& smap = status_map_in.empty()
|
||||
? default_status_map()
|
||||
: status_map_in;
|
||||
|
||||
const int n_rings = static_cast<int>(cfg.ring_radii.size()) - 1;
|
||||
const int n_sectors = cfg.n_sectors > 0 ? cfg.n_sectors : 18;
|
||||
const float sector_angle = kTwoPi / static_cast<float>(n_sectors);
|
||||
const float half_sector = sector_angle * 0.5f;
|
||||
|
||||
// Preasignar outputs con ring=-1 (descartados por defecto)
|
||||
std::vector<LayoutOutput> out(nodes.size());
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
out[i].id = nodes[i].id;
|
||||
}
|
||||
|
||||
// --- Paso 1: mapear cada nodo a (ring, sector) ---
|
||||
// indices_in_bin[ring][sector] = lista de indices en nodes[]
|
||||
using BinKey = std::pair<int,int>;
|
||||
struct BinKeyHash {
|
||||
size_t operator()(const BinKey& k) const noexcept {
|
||||
return std::hash<int>()(k.first) ^ (std::hash<int>()(k.second) << 16);
|
||||
}
|
||||
};
|
||||
std::unordered_map<BinKey, std::vector<size_t>, BinKeyHash> bins;
|
||||
|
||||
for (size_t i = 0; i < nodes.size(); ++i) {
|
||||
int ring = lookup_ring(nodes[i].status, smap);
|
||||
if (ring < 0 || ring >= n_rings) {
|
||||
// Nodo descartado: ring=-1, x/y=0 (ya inicializado)
|
||||
continue;
|
||||
}
|
||||
int sector = lookup_sector(nodes[i].domain, domain_order, n_sectors);
|
||||
out[i].ring = ring;
|
||||
out[i].sector = sector;
|
||||
bins[{ring, sector}].push_back(i);
|
||||
}
|
||||
|
||||
// --- Paso 2: posicionar nodos dentro de cada bin ---
|
||||
for (auto& [key, indices] : bins) {
|
||||
const int ring = key.first;
|
||||
const int sector = key.second;
|
||||
|
||||
// Ordenar bin: recency desc, id asc (deterministico)
|
||||
std::sort(indices.begin(), indices.end(),
|
||||
[&](size_t a, size_t b) {
|
||||
float ra = nodes[a].recency;
|
||||
float rb = nodes[b].recency;
|
||||
if (ra != rb) return ra > rb;
|
||||
return nodes[a].id < nodes[b].id;
|
||||
});
|
||||
|
||||
// Asignar rank_in_bin
|
||||
for (int rank = 0; rank < static_cast<int>(indices.size()); ++rank) {
|
||||
out[indices[rank]].rank_in_bin = rank;
|
||||
}
|
||||
|
||||
// Radio interior y exterior del ring
|
||||
float r_inner = cfg.ring_radii[ring];
|
||||
float r_outer = cfg.ring_radii[ring + 1];
|
||||
|
||||
// Caso especial: ring 0 con radio interno == 0
|
||||
if (r_inner == 0.0f) r_inner = kRing0InnerMin;
|
||||
|
||||
// Aplicar padding
|
||||
float r_lo = r_inner + cfg.bin_padding;
|
||||
float r_hi = r_outer - cfg.bin_padding;
|
||||
if (r_lo > r_hi) {
|
||||
// Bin demasiado estrecho: usar el punto medio
|
||||
r_lo = r_hi = (r_inner + r_outer) * 0.5f;
|
||||
}
|
||||
|
||||
const int N = static_cast<int>(indices.size());
|
||||
|
||||
// Angulo central del sector (sin jitter)
|
||||
float theta_center = cfg.start_angle
|
||||
+ (static_cast<float>(sector) + 0.5f) * sector_angle;
|
||||
|
||||
// Determinar si activamos jitter angular (bin sobrecargado radialmente)
|
||||
float band_height = r_hi - r_lo;
|
||||
int radial_capacity = (band_height < 1.0f)
|
||||
? 1
|
||||
: std::max(1, static_cast<int>(band_height / kMinRadialSpacing));
|
||||
bool use_jitter = (N > radial_capacity);
|
||||
|
||||
// Posicionar cada nodo
|
||||
for (int rank = 0; rank < N; ++rank) {
|
||||
size_t idx = indices[rank];
|
||||
|
||||
// Radio: distribucion uniforme en la banda
|
||||
float r;
|
||||
if (N == 1) {
|
||||
r = (r_lo + r_hi) * 0.5f;
|
||||
} else {
|
||||
r = r_lo + (static_cast<float>(rank) + 0.5f) * (r_hi - r_lo)
|
||||
/ static_cast<float>(N);
|
||||
}
|
||||
|
||||
// Sub-jitter angular deterministico si bin sobrecargado
|
||||
float jitter = 0.0f;
|
||||
if (use_jitter) {
|
||||
float raw = id_jitter(nodes[idx].id); // en [-1,+1]
|
||||
jitter = raw * half_sector * kJitterMaxFraction;
|
||||
}
|
||||
|
||||
float theta = theta_center + jitter;
|
||||
out[idx].x = cfg.center_x + r * std::cos(theta);
|
||||
out[idx].y = cfg.center_y + r * std::sin(theta);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fn_ring
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/// fn_ring — geometria pura para layout en anillos concentricos + sectores radiales.
|
||||
/// Pure: sin I/O, sin estado global, sin RNG. Misma entrada → mismo output siempre.
|
||||
/// C++17 STL only.
|
||||
|
||||
namespace fn_ring {
|
||||
|
||||
struct LayoutInput {
|
||||
std::string id; // identidad unica para mapeo deterministico
|
||||
std::string status; // categoria libre — mapeada al ring via status_map
|
||||
std::string domain; // categoria libre — mapeada al sector via domain_order
|
||||
float recency = 0.0f; // 0..1, ordena DENTRO de un (ring,sector) bin (desc)
|
||||
};
|
||||
|
||||
struct LayoutOutput {
|
||||
std::string id;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
int ring = -1; // -1 = nodo descartado (status no mapeado)
|
||||
int sector = 0;
|
||||
int rank_in_bin = 0; // 0..N-1 dentro del bin (ring,sector)
|
||||
};
|
||||
|
||||
struct LayoutConfig {
|
||||
int n_sectors = 18;
|
||||
float center_x = 0.0f;
|
||||
float center_y = 0.0f;
|
||||
// ring_radii[i] = radio interno del ring i
|
||||
// ring_radii[i+1] = radio externo del ring i
|
||||
// Default: 5 rings (0..4)
|
||||
std::vector<float> ring_radii { 0.0f, 150.0f, 280.0f, 450.0f, 650.0f, 850.0f };
|
||||
float bin_padding = 14.0f; // padding interior del bin en pixels
|
||||
float start_angle = 0.0f; // rotacion global del sector 0 en radianes
|
||||
};
|
||||
|
||||
/// Mapea strings de status a indice de ring.
|
||||
/// Status no listado → ring -1 (nodo descartado).
|
||||
///
|
||||
/// Default canonico (usado cuando se pasa vector vacio):
|
||||
/// {"completado",0}, {"completed",0},
|
||||
/// {"in-progress",1},
|
||||
/// {"pendiente_unlocked",2}, {"unlocked",2}, {"pending",2},
|
||||
/// {"pendiente",3}, {"locked",3},
|
||||
/// {"deferred",4}, {"bloqueado",4}
|
||||
using StatusRingMap = std::vector<std::pair<std::string, int>>;
|
||||
|
||||
/// Mapea strings de domain a indice de sector [0..n_sectors-1].
|
||||
/// Domains no listados → sector n_sectors-1 (sector "otros").
|
||||
using DomainOrder = std::vector<std::string>;
|
||||
|
||||
/// Compute ring+sector layout deterministico.
|
||||
///
|
||||
/// Para cada nodo:
|
||||
/// 1. ring = status_map[node.status]; descartar si -1.
|
||||
/// 2. sector = index(domain_order, node.domain); fallback n_sectors-1.
|
||||
/// 3. Bin (ring, sector): ordenar por (recency desc, id asc), distribuir
|
||||
/// uniformemente en la banda radial con padding. Sub-jitter angular
|
||||
/// deterministico via hash del id cuando el bin esta sobrecargado.
|
||||
/// 4. theta = start_angle + (sector + 0.5) * (2*PI / n_sectors) + jitter
|
||||
/// 5. x = center.x + r * cos(theta); y = center.y + r * sin(theta)
|
||||
///
|
||||
/// Ring 0 con radio interno 0: usa r_inner = 30.0f para no colocar nodos
|
||||
/// en el origen exacto (reservado para HUD overlay).
|
||||
std::vector<LayoutOutput>
|
||||
compute_ring_layout(const std::vector<LayoutInput>& nodes,
|
||||
const LayoutConfig& cfg,
|
||||
const StatusRingMap& status_map = {},
|
||||
const DomainOrder& domain_order = {});
|
||||
|
||||
} // namespace fn_ring
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: compute_ring_layout
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
purity: pure
|
||||
version: "1.0.0"
|
||||
signature: "std::vector<LayoutOutput> compute_ring_layout(const std::vector<LayoutInput>& nodes, const LayoutConfig& cfg, const StatusRingMap& status_map, const DomainOrder& domain_order)"
|
||||
description: "Calcula posiciones (x,y) deterministicas para layout en anillos concentricos por status + sectores radiales por domain. Pure, sin fisicas, output reproducible. Util para skill_tree, dashboards de roadmap, mapas de capability."
|
||||
tags: [layout, rings, sectors, polar, dashboard, registry, skill-tree]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["core/compute_ring_layout.h"]
|
||||
params:
|
||||
- name: nodes
|
||||
desc: "Vector de LayoutInput. Cada nodo tiene id (unico), status (bucket de ring), domain (sector), recency (0..1, ordena dentro del bin desc)."
|
||||
- name: cfg
|
||||
desc: "LayoutConfig: n_sectors, center_x/y, ring_radii (bordes de los anillos), bin_padding, start_angle. Defaults validos para 5 rings / 18 sectores / canvas 850px de radio."
|
||||
- name: status_map
|
||||
desc: "Mapa status→ring. Vector vacio usa el mapa canonico (completado→0, in-progress→1, unlocked→2, pendiente→3, deferred→4). Status no encontrado → ring=-1 (descartado)."
|
||||
- name: domain_order
|
||||
desc: "Lista ordenada de domains para asignar sector. Domain fuera de la lista → sector n_sectors-1 (sector 'otros'). Vector vacio → todos los domains caen en sector n_sectors-1."
|
||||
output: "Vector de LayoutOutput en el mismo orden que nodes[]. Cada salida: id, x, y (posicion en world units), ring (-1=descartado), sector, rank_in_bin (orden dentro del (ring,sector))."
|
||||
tested: true
|
||||
tests:
|
||||
- "empty_input_empty_output"
|
||||
- "single_node_centered_in_bin"
|
||||
- "two_nodes_same_bin_radial_distribution"
|
||||
- "default_status_map"
|
||||
- "unmapped_status_returns_ring_minus_one"
|
||||
- "domain_not_in_order_falls_back_to_last_sector"
|
||||
- "deterministic_repeated_call"
|
||||
- "ring_zero_avoids_origin"
|
||||
- "sector_wrap_around_last_sector"
|
||||
- "golden_snapshot_30_nodes"
|
||||
test_file_path: "cpp/tests/test_compute_ring_layout.cpp"
|
||||
file_path: "cpp/functions/core/compute_ring_layout.cpp"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
#include "core/compute_ring_layout.h"
|
||||
|
||||
fn_ring::LayoutConfig cfg;
|
||||
// cfg usa defaults: 5 rings, 18 sectors, ring_radii={0,150,280,450,650,850}
|
||||
|
||||
fn_ring::DomainOrder order = {"core", "infra", "finance", "datascience"};
|
||||
|
||||
std::vector<fn_ring::LayoutInput> nodes = {
|
||||
{"0001", "completado", "core", 1.0f},
|
||||
{"0002", "pendiente", "infra", 0.5f},
|
||||
{"0003", "in-progress", "finance", 0.8f},
|
||||
{"0004", "deferred", "core", 0.1f},
|
||||
};
|
||||
|
||||
auto out = fn_ring::compute_ring_layout(nodes, cfg, {}, order);
|
||||
// out[0]: ring=0, sector=0 (completado → ring0, core → sector0)
|
||||
// out[1]: ring=3, sector=1 (pendiente → ring3, infra → sector1)
|
||||
// out[2]: ring=1, sector=2 (in-progress→ ring1, finance → sector2)
|
||||
// out[3]: ring=4, sector=0 (deferred → ring4, core → sector0)
|
||||
|
||||
// Integrar en main.cpp del skill_tree:
|
||||
for (auto& o : out) {
|
||||
if (o.ring < 0) continue; // nodo descartado
|
||||
// ImGui::SetCursorScreenPos({base.x + o.x, base.y + o.y});
|
||||
// draw_node(o.id);
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesitas colocar N entidades en un mapa polar de progreso donde el anillo codifica el estado de avance (completado → centro, pendiente → exterior) y el sector codifica el dominio o categoria. Casos canonicos: skill_tree de issues/capabilities, roadmap visual de un proyecto, mapa de cobertura del registry por dominio.
|
||||
|
||||
La funcion calcula posiciones una vez; la animacion (interpolacion entre dos snapshots) la hace el caller en main.cpp, no esta funcion.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Hash deterministico dentro de la build**: el sub-jitter angular usa FNV-1a 32-bit sobre el id (no `std::hash<string>`), lo que garantiza estabilidad cross-platform dentro del mismo proceso. El output es reproducible entre compiladores distintos.
|
||||
- **Domain fuera de DomainOrder**: siempre cae en sector `n_sectors-1`. Si el caller quiere que todos los domains inesperados esten dispersos en lugar de apilados en el ultimo sector, debe pasar un `DomainOrder` exhaustivo o usar un sector dedicado `"otros"`.
|
||||
- **Ring 0 con `ring_radii[0]==0`**: la funcion usa `r_inner=30.0f` automaticamente para no colocar nodos en el origen. Si el caller quiere usar el origen, debe pasar `ring_radii[0] > 0`.
|
||||
- **Bins muy densamente cargados**: cuando el bin tiene mas nodos que la capacidad radial (`band_height / 18px`), se activa jitter angular `±0.4 * half_sector`. Los nodos siguen dentro del sector pero el angulo no es exactamente el centro del sector. Para bins con N > ~20 nodos el solapamiento visual es inevitable sin escalado del canvas.
|
||||
- **`status_map` vacio**: se usa el mapa canonico con 5 rings. Si el caller usa status propios, DEBE pasar su propio `StatusRingMap`; de lo contrario todos cairan en ring=-1 (descartados).
|
||||
|
||||
## Capability growth log
|
||||
|
||||
(sin bumps aun — v1.0.0)
|
||||
@@ -5,6 +5,8 @@
|
||||
// v1.4.0: ChipRule / ColorStop / CategoricalChip / ColorScale renderers.
|
||||
#pragma once
|
||||
|
||||
#include "compute_column_stats.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -49,13 +51,68 @@ struct Filter {
|
||||
std::string value;
|
||||
};
|
||||
|
||||
// ColorStop: one stop in an N-color gradient. Used by ColorRule (NumericRange)
|
||||
// y por ColumnSpec (ColorScale renderer). Definido aqui (no abajo) para que
|
||||
// ColorRule pueda contenerlo sin forward decl.
|
||||
struct ColorStop {
|
||||
float position; // 0.0 (leftmost/min) to 1.0 (rightmost/max)
|
||||
std::string color; // "#rrggbb" hex color at this stop
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Drill extendido (fase 10). Ver issue 0079.
|
||||
// Definidos aqui (no al final) para que State pueda contener vector<DrillStep>.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class DateGranularity { None, Year, Month, Week, Day, Hour };
|
||||
|
||||
enum class FilterPreset { Last7d, Last30d, Last90d, ExcludeNulls, NonZero };
|
||||
|
||||
// Step de drill grabado para history undo/redo (fase 10).
|
||||
struct DrillStep {
|
||||
int target_stage = -1; // stage donde se anadio el filter
|
||||
int filter_pos = -1; // index en target_stage.filters
|
||||
int prev_active_stage = 0; // active_stage antes del drill
|
||||
Filter added; // filter para redo
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ColorRule kind (v1.5.0): pintado condicional con tres modos.
|
||||
// CellBg — bg de celda cuando valor == `equals` (back-compat).
|
||||
// CategoricalDot — dot a la izquierda del texto, colores autoasignados por
|
||||
// valor distinto (palette deterministica). Util para
|
||||
// categoricas con muchos valores donde no quieres definir
|
||||
// cada color a mano.
|
||||
// NumericRange — gradiente continuo de N colores (range_stops) sobre el
|
||||
// bg de la celda. Para columnas numericas.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class ColorRuleKind : uint8_t {
|
||||
CellBg = 0, // legacy (equals + color)
|
||||
CategoricalDot = 1, // dots autoasignados por valor distinto
|
||||
NumericRange = 2, // gradiente N-color sobre rango numerico
|
||||
};
|
||||
|
||||
// ColorRule: pintado condicional de celdas (UI helper).
|
||||
// ----------------------------------------------------------------------------
|
||||
// v1.5.0: anade `kind` + campos para CategoricalDot / NumericRange.
|
||||
struct ColorRule {
|
||||
int col;
|
||||
std::string equals;
|
||||
unsigned int color;
|
||||
int col;
|
||||
std::string equals; // CellBg: match value; otros: ignorado
|
||||
unsigned int color; // CellBg: bg color; otros: ignorado
|
||||
|
||||
// v1.5.0 fields (defaults preservan back-compat — kind=CellBg).
|
||||
ColorRuleKind kind = ColorRuleKind::CellBg;
|
||||
|
||||
// CategoricalDot: alpha del relleno del dot (0..1). Tamaño en px (default 6).
|
||||
float dot_alpha = 1.0f;
|
||||
float dot_radius_px = 4.0f;
|
||||
// Si vacio -> autoasigna desde palette interna. Si no, mapping fijo:
|
||||
// pares (valor, "#rrggbb") consultados en orden.
|
||||
std::vector<std::pair<std::string, std::string>> dot_map;
|
||||
|
||||
// NumericRange: gradiente continuo. range_min..range_max + N>=2 stops.
|
||||
double range_min = 0.0;
|
||||
double range_max = 1.0;
|
||||
float range_alpha = 0.25f; // [0..1]; bg tint opacity
|
||||
std::vector<ColorStop> range_stops; // declarado mas abajo; vacio -> default green→amber→red
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -189,11 +246,7 @@ struct ChipRule {
|
||||
std::string color; // "#rrggbb" hex color for the filled circle
|
||||
};
|
||||
|
||||
// ColorStop: one stop in an N-color gradient for ColorScale renderer (v1.4.0).
|
||||
struct ColorStop {
|
||||
float position; // 0.0 (leftmost/min) to 1.0 (rightmost/max)
|
||||
std::string color; // "#rrggbb" hex color at this stop
|
||||
};
|
||||
// (ColorStop defined earlier, near Filter, so ColorRule can reference it.)
|
||||
|
||||
// ColumnSpec: rendering spec for one column. Indexed by column position.
|
||||
struct ColumnSpec {
|
||||
@@ -331,6 +384,41 @@ struct State {
|
||||
bool chrome_user_set = true;
|
||||
bool chrome_user_visible = false;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// v1.5.0 — per-table UI state. Antes vivia en UiState singleton, lo cual
|
||||
// hacia que toggle "Show stats" / seleccion de celdas / drill-history /
|
||||
// row inspector se aplicasen a TODAS las tablas a la vez. Ahora cada
|
||||
// State los lleva. Modal/popup state (ask AI, edit chips, etc.) sigue en
|
||||
// UiState porque solo uno esta abierto en toda la app a la vez.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
// Stats panel (cabecera de col muestra min/max/percentiles/hist).
|
||||
bool stats_mode = false;
|
||||
std::vector<ColStats> stats_cache;
|
||||
// Invalidacion del cache (data-identity y filter-set).
|
||||
const char* const* stats_last_cells = nullptr;
|
||||
int stats_last_rows = -1;
|
||||
int stats_last_eff_cols = -1;
|
||||
size_t stats_last_filter_h = (size_t)-1;
|
||||
int stats_last_visible = -1;
|
||||
|
||||
// Cell selection (drag-select rectangular Ctrl+C-able).
|
||||
int sel_anchor_row = -1;
|
||||
int sel_anchor_col = -1;
|
||||
int sel_end_row = -1;
|
||||
int sel_end_col = -1;
|
||||
bool sel_active = false;
|
||||
bool sel_dragging = false;
|
||||
|
||||
// Row inspector modal target (-1 = closed).
|
||||
int inspect_row = -1;
|
||||
bool inspect_open = false;
|
||||
|
||||
// Drill history (fase 10) — per-table porque cada tabla tiene su propio
|
||||
// pipeline de filters.
|
||||
std::vector<DrillStep> drill_back;
|
||||
std::vector<DrillStep> drill_forward;
|
||||
|
||||
// Helpers (definidos en compute_stage.cpp).
|
||||
Stage& raw();
|
||||
const Stage& raw() const;
|
||||
@@ -339,19 +427,4 @@ struct State {
|
||||
void ensure_stage0();
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Drill extendido (fase 10). Ver issue 0079.
|
||||
// ----------------------------------------------------------------------------
|
||||
enum class DateGranularity { None, Year, Month, Week, Day, Hour };
|
||||
|
||||
enum class FilterPreset { Last7d, Last30d, Last90d, ExcludeNulls, NonZero };
|
||||
|
||||
// Step de drill grabado para history undo/redo (fase 10).
|
||||
struct DrillStep {
|
||||
int target_stage = -1; // stage donde se anadio el filter
|
||||
int filter_pos = -1; // index en target_stage.filters
|
||||
int prev_active_stage = 0; // active_stage antes del drill
|
||||
Filter added; // filter para redo
|
||||
};
|
||||
|
||||
} // namespace data_table
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "core/parse_md_frontmatter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace fn_md {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// String helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string trim(const std::string& s) {
|
||||
const auto b = s.find_first_not_of(" \t\r");
|
||||
if (b == std::string::npos) return {};
|
||||
const auto e = s.find_last_not_of(" \t\r");
|
||||
return s.substr(b, e - b + 1);
|
||||
}
|
||||
|
||||
// Strip a trailing ` # comment` from a value string.
|
||||
// Only strips if the `#` is preceded by at least one space and is outside
|
||||
// any quote context. We take a simple approach: after stripping outer quotes
|
||||
// we look for ` #` outside of them.
|
||||
static std::string strip_comment(const std::string& s) {
|
||||
// If the whole string is quoted, skip comment stripping; the comment would
|
||||
// be inside the quotes and should be preserved as literal text.
|
||||
if (s.size() >= 2 &&
|
||||
((s.front() == '"' && s.back() == '"') ||
|
||||
(s.front() == '\'' && s.back() == '\''))) {
|
||||
return s;
|
||||
}
|
||||
const auto pos = s.find(" #");
|
||||
if (pos == std::string::npos) return s;
|
||||
return trim(s.substr(0, pos));
|
||||
}
|
||||
|
||||
// Remove surrounding " or ' quotes from a value, if present.
|
||||
static std::string unquote(const std::string& s) {
|
||||
if (s.size() >= 2 &&
|
||||
((s.front() == '"' && s.back() == '"') ||
|
||||
(s.front() == '\'' && s.back() == '\''))) {
|
||||
return s.substr(1, s.size() - 2);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Parse an inline YAML list: [a, b, c] or []
|
||||
static std::vector<std::string> parse_inline_list(const std::string& s) {
|
||||
std::vector<std::string> result;
|
||||
// strip brackets
|
||||
const auto lb = s.find('[');
|
||||
const auto rb = s.rfind(']');
|
||||
if (lb == std::string::npos || rb == std::string::npos || lb >= rb)
|
||||
return result;
|
||||
const std::string inner = s.substr(lb + 1, rb - lb - 1);
|
||||
if (trim(inner).empty()) return result;
|
||||
|
||||
// split on commas respecting quotes
|
||||
std::string token;
|
||||
bool in_quote = false;
|
||||
char quote_ch = 0;
|
||||
for (char c : inner) {
|
||||
if (!in_quote && (c == '"' || c == '\'')) {
|
||||
in_quote = true;
|
||||
quote_ch = c;
|
||||
token += c;
|
||||
} else if (in_quote && c == quote_ch) {
|
||||
in_quote = false;
|
||||
token += c;
|
||||
} else if (!in_quote && c == ',') {
|
||||
result.push_back(unquote(trim(token)));
|
||||
token.clear();
|
||||
} else {
|
||||
token += c;
|
||||
}
|
||||
}
|
||||
const auto t = unquote(trim(token));
|
||||
if (!t.empty()) result.push_back(t);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Split a frontmatter line into (key, raw_value).
|
||||
// Returns false if the line has no `:`.
|
||||
static bool split_kv(const std::string& line, std::string& key, std::string& raw_val) {
|
||||
const auto colon = line.find(':');
|
||||
if (colon == std::string::npos) return false;
|
||||
key = trim(line.substr(0, colon));
|
||||
raw_val = trim(line.substr(colon + 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Line iterator helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::vector<std::string> split_lines(const std::string& s) {
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream ss(s);
|
||||
std::string l;
|
||||
while (std::getline(ss, l)) lines.push_back(l);
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Returns true if a line looks like a block-list item: optional leading
|
||||
// whitespace, then `- `.
|
||||
static bool is_list_item(const std::string& line, std::string& item_val) {
|
||||
const auto b = line.find_first_not_of(" \t");
|
||||
if (b == std::string::npos) return false;
|
||||
if (line[b] != '-') return false;
|
||||
// Must have space (or be end) after `-`
|
||||
if (b + 1 < line.size() && line[b + 1] != ' ') return false;
|
||||
item_val = trim(line.substr(b + 1));
|
||||
item_val = unquote(item_val);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Frontmatter parse_md_frontmatter(const std::string& content) {
|
||||
Frontmatter fm;
|
||||
|
||||
const auto lines = split_lines(content);
|
||||
if (lines.empty()) return fm;
|
||||
|
||||
// Check opening `---`
|
||||
if (trim(lines[0]) != "---") {
|
||||
fm.body = content;
|
||||
return fm;
|
||||
}
|
||||
|
||||
fm.has_frontmatter = true;
|
||||
|
||||
// Find closing `---`
|
||||
std::size_t close_idx = std::string::npos;
|
||||
for (std::size_t i = 1; i < lines.size(); ++i) {
|
||||
if (trim(lines[i]) == "---") {
|
||||
close_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine where the body starts (after the closing `---\n`)
|
||||
if (close_idx == std::string::npos) {
|
||||
// No closing delimiter: treat everything after line 0 as frontmatter,
|
||||
// body is empty.
|
||||
close_idx = lines.size();
|
||||
} else {
|
||||
// Reconstruct body from lines after close_idx
|
||||
std::string body;
|
||||
for (std::size_t i = close_idx + 1; i < lines.size(); ++i) {
|
||||
body += lines[i] + '\n';
|
||||
}
|
||||
fm.body = body;
|
||||
}
|
||||
|
||||
// Parse frontmatter lines [1 .. close_idx)
|
||||
std::size_t i = 1;
|
||||
while (i < close_idx) {
|
||||
const std::string& line = lines[i];
|
||||
|
||||
// Skip blank lines and top-level comment lines
|
||||
const std::string tl = trim(line);
|
||||
if (tl.empty() || tl[0] == '#') {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string key, raw_val;
|
||||
if (!split_kv(line, key, raw_val)) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (key.empty()) { ++i; continue; }
|
||||
|
||||
// --- Determine the kind of value ---
|
||||
|
||||
// 1. Inline list: raw_val starts with `[`
|
||||
if (!raw_val.empty() && raw_val[0] == '[') {
|
||||
fm.fields[key] = parse_inline_list(raw_val);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Block scalar `|` or `>` — unsupported, store empty string
|
||||
if (raw_val == "|" || raw_val == ">") {
|
||||
fm.fields[key] = std::string{};
|
||||
// Skip continuation lines (more indented than current)
|
||||
++i;
|
||||
while (i < close_idx) {
|
||||
const std::string& next = lines[i];
|
||||
if (next.empty()) { ++i; continue; }
|
||||
if (next[0] == ' ' || next[0] == '\t') { ++i; continue; }
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Block list: raw_val is empty and next line(s) are list items
|
||||
if (raw_val.empty()) {
|
||||
// Peek ahead
|
||||
std::string dummy;
|
||||
std::size_t j = i + 1;
|
||||
bool found_items = false;
|
||||
while (j < close_idx) {
|
||||
const std::string& next = lines[j];
|
||||
if (trim(next).empty()) { ++j; continue; }
|
||||
std::string item_val;
|
||||
if (is_list_item(next, item_val)) {
|
||||
found_items = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (found_items) {
|
||||
std::vector<std::string> items;
|
||||
++i; // move past the key line
|
||||
while (i < close_idx) {
|
||||
const std::string& next = lines[i];
|
||||
if (trim(next).empty()) { ++i; continue; }
|
||||
std::string item_val;
|
||||
if (is_list_item(next, item_val)) {
|
||||
items.push_back(unquote(item_val));
|
||||
++i;
|
||||
} else {
|
||||
break; // end of block list
|
||||
}
|
||||
}
|
||||
fm.fields[key] = std::move(items);
|
||||
continue;
|
||||
}
|
||||
// Empty value (e.g. `key:` with no list following)
|
||||
fm.fields[key] = std::string{};
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Plain scalar (strip trailing comment, then unquote)
|
||||
const std::string stripped = strip_comment(raw_val);
|
||||
fm.fields[key] = unquote(trim(stripped));
|
||||
++i;
|
||||
}
|
||||
|
||||
return fm;
|
||||
}
|
||||
|
||||
} // namespace fn_md
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace fn_md {
|
||||
|
||||
/// A YAML value: absent, scalar string, or list of strings.
|
||||
using YamlValue = std::variant<std::monostate, std::string, std::vector<std::string>>;
|
||||
|
||||
/// Result of parsing a Markdown file that may contain a YAML frontmatter block.
|
||||
struct Frontmatter {
|
||||
std::unordered_map<std::string, YamlValue> fields;
|
||||
std::string body; ///< content after the closing `---` line
|
||||
bool has_frontmatter = false;
|
||||
};
|
||||
|
||||
/// Parse a Markdown file that may (or may not) begin with a YAML frontmatter
|
||||
/// block delimited by `---` lines.
|
||||
///
|
||||
/// Supported YAML subset (flat only, no nested maps):
|
||||
/// key: value -> string (bare, or "quoted", or 'quoted')
|
||||
/// key: [a, b, c] -> inline list -> vector<string>
|
||||
/// key: -> block list when the next indented lines are
|
||||
/// - item ` - item` -> vector<string>
|
||||
/// key: | -> unsupported block scalar; stored as ""
|
||||
/// # comment -> ignored (also inline trailing comments)
|
||||
///
|
||||
/// Notes:
|
||||
/// - Keys and values are trimmed of leading/trailing whitespace.
|
||||
/// - A value like `description: "foo: bar"` splits on the FIRST colon only.
|
||||
/// - Pure function: no I/O, no logging, no side-effects.
|
||||
Frontmatter parse_md_frontmatter(const std::string& content);
|
||||
|
||||
} // namespace fn_md
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: parse_md_frontmatter
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "Frontmatter parse_md_frontmatter(const std::string& content)"
|
||||
description: "Parsea frontmatter YAML simple (subset: key:value, listas inline [a,b], listas multilinea con - item) de un .md y devuelve struct con fields map + body. Pure, sin dependencias externas."
|
||||
tags: [markdown, frontmatter, yaml, parser, issues, meta, registry]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["core/parse_md_frontmatter.h"]
|
||||
example: |
|
||||
#include "core/parse_md_frontmatter.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
std::ifstream f("/home/lucas/fn_registry/dev/issues/0109-skill-tree-app-roadmap.md");
|
||||
std::stringstream ss; ss << f.rdbuf();
|
||||
auto fm = fn_md::parse_md_frontmatter(ss.str());
|
||||
auto title = std::get<std::string>(fm.fields["title"]);
|
||||
auto domain = std::get<std::vector<std::string>>(fm.fields["domain"]);
|
||||
tested: true
|
||||
tests:
|
||||
- "parses_no_frontmatter"
|
||||
- "parses_simple_key_value"
|
||||
- "parses_quoted_strings"
|
||||
- "parses_inline_list"
|
||||
- "parses_multiline_list"
|
||||
- "parses_body_after_frontmatter"
|
||||
- "parses_empty_inline_list"
|
||||
- "parses_strips_trailing_comment"
|
||||
- "parses_real_issue_0109"
|
||||
- "parses_real_issues_golden"
|
||||
test_file_path: "cpp/tests/test_parse_md_frontmatter.cpp"
|
||||
file_path: "cpp/functions/core/parse_md_frontmatter.cpp"
|
||||
params:
|
||||
- name: content
|
||||
desc: "Contenido completo del archivo .md como string. Puede o no comenzar con bloque frontmatter `---...---`."
|
||||
output: "Struct Frontmatter con: fields (unordered_map<string, YamlValue>) donde YamlValue = monostate|string|vector<string>; body (string con contenido despues del segundo ---); has_frontmatter (bool)."
|
||||
notes: |
|
||||
Subset YAML deliberado — los frontmatters de issues y flows del registry son planos. Si en el futuro hace falta soportar mapas anidados, considerar embeber yaml-cpp en cpp/vendor/ antes que extender este parser.
|
||||
---
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando una app C++ necesita leer metadata de archivos Markdown del registry (issues, flows, app.md, analysis.md, type.md). No requiere libreria externa ni yaml-cpp.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Subset YAML: NO soporta mapas anidados, NO soporta block scalars `|` ni `>` (devuelve string vacio en esos casos).
|
||||
- Si un value contiene `: ` literal debe ir entre comillas: `description: "foo: bar"`.
|
||||
- Comentarios solo se eliminan al final de linea en valores no citados. Un `# comentario` dentro de comillas se preserva como literal.
|
||||
- Block list: las lineas de items deben estar indentadas con al menos un espacio antes de `- `. Un `- item` sin indentacion al nivel raiz se ignora (no es list item de la clave anterior).
|
||||
- `YamlValue` es `std::variant` — usar `std::get<std::string>` o `std::get<std::vector<std::string>>` segun el campo. `std::get_if` es mas seguro si el tipo no es conocido a priori.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
#include "core/parse_md_frontmatter.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
std::ifstream f("/home/lucas/fn_registry/dev/issues/0109-skill-tree-app-roadmap.md");
|
||||
std::stringstream ss; ss << f.rdbuf();
|
||||
auto fm = fn_md::parse_md_frontmatter(ss.str());
|
||||
|
||||
// Scalar string
|
||||
auto id = std::get<std::string>(fm.fields["id"]); // "0109"
|
||||
auto status = std::get<std::string>(fm.fields["status"]); // "in-progress"
|
||||
|
||||
// List
|
||||
auto domain = std::get<std::vector<std::string>>(fm.fields["domain"]);
|
||||
// domain[0] == "meta", domain[1] == "cpp-stack"
|
||||
|
||||
// Body (content after closing ---)
|
||||
// fm.body starts with "\n# 0109 — skill_tree app..."
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
// data_table_ai_panel — modal "Ask AI" de la tabla TQL.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Rangos de lineas del fuente original (antes de la extraccion):
|
||||
// - Stub llm_anthropic (no-op) : lineas 82-112
|
||||
// - Modal "Ask AI" completo : lineas 4209-4328
|
||||
// - Boton trigger en viz selector: linea 1588
|
||||
//
|
||||
// NOTA DEUDA TECNICA: llm_anthropic no esta en el registry todavia (Wave 4 TODO).
|
||||
// Pendiente promover a cpp/functions/infra/llm_anthropic (.h + .cpp + .md).
|
||||
|
||||
#include "viz/data_table_ai_panel.h"
|
||||
#include "core/data_table_types.h"
|
||||
#include "core/tql_apply.h"
|
||||
#include "imgui.h"
|
||||
|
||||
// llm_anthropic: usar header real si disponible, stub si no.
|
||||
#ifdef FN_LLM_ANTHROPIC
|
||||
# include "core/llm_anthropic.h"
|
||||
#else
|
||||
// Stub no-op (inline en este TU hasta que llm_anthropic se promueva al registry).
|
||||
// TODO: Wave 4 — promover a cpp/functions/infra/llm_anthropic.cpp + .h + .md
|
||||
namespace llm_anthropic {
|
||||
enum class OutputMode { TQL, SQL };
|
||||
struct AskInput {
|
||||
std::string question;
|
||||
std::string tql_current;
|
||||
std::vector<std::string> col_names;
|
||||
std::vector<data_table::ColumnType> col_types;
|
||||
std::vector<std::string> joinable_names;
|
||||
OutputMode mode = OutputMode::TQL;
|
||||
std::string model;
|
||||
int max_tokens = 8192;
|
||||
};
|
||||
struct AskResult {
|
||||
std::string code;
|
||||
std::string raw;
|
||||
std::string error;
|
||||
int tokens_in = 0;
|
||||
int tokens_out = 0;
|
||||
};
|
||||
inline AskResult ask(const AskInput&, const std::string& = "") {
|
||||
AskResult r;
|
||||
r.error = "llm_anthropic not available (stub). Build with FN_LLM_ANTHROPIC=1.";
|
||||
return r;
|
||||
}
|
||||
} // namespace llm_anthropic
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_ask_ai_modal
|
||||
// Dibuja el modal "Ask AI". Debe llamarse cada frame (patron ImGui).
|
||||
// Abre el popup si ask_ai.open == true.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_ask_ai_modal(AskAiState& ask_ai,
|
||||
State& st,
|
||||
const std::vector<std::string>& active_headers,
|
||||
const std::vector<ColumnType>& active_types,
|
||||
int orig_cols)
|
||||
{
|
||||
if (ask_ai.open) ImGui::OpenPopup("Ask AI");
|
||||
ImGui::SetNextWindowSize(ImVec2(820, 560), ImGuiCond_Appearing);
|
||||
if (ImGui::BeginPopupModal("Ask AI", &ask_ai.open,
|
||||
ImGuiWindowFlags_NoSavedSettings)) {
|
||||
ImGui::TextDisabled("Ask en lenguaje natural. Default TQL. SQL solo si DuckDB linkado.");
|
||||
const char* modes[] = {"TQL", "SQL (DuckDB)"};
|
||||
#ifndef FN_TQL_DUCKDB
|
||||
// SQL mode disabled visually pero el toggle existe (informativo)
|
||||
if (ask_ai.mode == 1) ask_ai.mode = 0;
|
||||
#endif
|
||||
ImGui::Combo("Output##askmode", &ask_ai.mode, modes, IM_ARRAYSIZE(modes));
|
||||
#ifndef FN_TQL_DUCKDB
|
||||
if (ask_ai.mode == 1) {
|
||||
ImGui::TextColored(ImVec4(1, 0.5f, 0.3f, 1),
|
||||
"SQL mode requires FN_TQL_DUCKDB=1 build flag.");
|
||||
}
|
||||
#endif
|
||||
ImGui::InputTextMultiline("##ask_q", ask_ai.question, sizeof(ask_ai.question),
|
||||
ImVec2(-1, 80));
|
||||
ImGui::BeginDisabled(ask_ai.busy);
|
||||
if (ImGui::Button("Send")) {
|
||||
ask_ai.busy = true;
|
||||
ask_ai.status = "Sending...";
|
||||
ask_ai.error.clear();
|
||||
ask_ai.response_code.clear();
|
||||
ask_ai.response_raw.clear();
|
||||
|
||||
// Build AskInput desde el state actual.
|
||||
llm_anthropic::AskInput in;
|
||||
in.question = ask_ai.question;
|
||||
in.tql_current = ask_ai.current_tql;
|
||||
in.col_names = active_headers;
|
||||
in.col_types = active_types;
|
||||
in.mode = (ask_ai.mode == 1)
|
||||
? llm_anthropic::OutputMode::SQL
|
||||
: llm_anthropic::OutputMode::TQL;
|
||||
|
||||
// Llamada blocking (UI freeze breve durante red).
|
||||
auto r = llm_anthropic::ask(in);
|
||||
ask_ai.busy = false;
|
||||
if (!r.error.empty()) {
|
||||
ask_ai.error = r.error;
|
||||
ask_ai.status = "Error";
|
||||
} else {
|
||||
ask_ai.response_raw = r.raw;
|
||||
ask_ai.response_code = r.code;
|
||||
ask_ai.status = "Got response.";
|
||||
// Llenar edit buffer
|
||||
std::snprintf(ask_ai.edit_buf, sizeof(ask_ai.edit_buf),
|
||||
"%s", r.code.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (!ask_ai.status.empty()) {
|
||||
ImGui::TextDisabled("%s", ask_ai.status.c_str());
|
||||
}
|
||||
if (!ask_ai.error.empty()) {
|
||||
ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), "%s", ask_ai.error.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Columns(2, "ask_cols", true);
|
||||
ImGui::TextUnformatted("Current");
|
||||
ImGui::InputTextMultiline("##ask_cur",
|
||||
const_cast<char*>(ask_ai.current_tql.c_str()),
|
||||
ask_ai.current_tql.size() + 1,
|
||||
ImVec2(-1, 240),
|
||||
ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::NextColumn();
|
||||
ImGui::TextUnformatted("Proposed (editable before apply)");
|
||||
ImGui::InputTextMultiline("##ask_new", ask_ai.edit_buf, sizeof(ask_ai.edit_buf),
|
||||
ImVec2(-1, 240));
|
||||
ImGui::Columns(1);
|
||||
|
||||
bool can_apply = !ask_ai.busy && ask_ai.edit_buf[0] != '\0';
|
||||
ImGui::BeginDisabled(!can_apply);
|
||||
if (ImGui::Button("Apply")) {
|
||||
std::string err;
|
||||
if (ask_ai.mode == 0) {
|
||||
// TQL apply
|
||||
bool ok = tql::apply(ask_ai.edit_buf, st,
|
||||
active_headers,
|
||||
active_types,
|
||||
nullptr, 0,
|
||||
orig_cols,
|
||||
&err);
|
||||
if (ok) {
|
||||
ask_ai.status = "Applied OK.";
|
||||
ask_ai.open = false;
|
||||
} else {
|
||||
ask_ai.error = "tql::apply error: " + err;
|
||||
ask_ai.status = "Apply failed.";
|
||||
}
|
||||
} else {
|
||||
#ifdef FN_TQL_DUCKDB
|
||||
// SQL apply: ejecutar via tql_duckdb sobre TableInputs activas.
|
||||
// Para tablas en memoria construimos un TableInput basico desde
|
||||
// active_headers/types. v1 no recupera cells originales aqui;
|
||||
// reportamos solo error si fallo. Caller real deberia pasar
|
||||
// tables() del render scope. Sin esto, marcamos status info.
|
||||
ask_ai.status = "SQL execute disponible (FN_TQL_DUCKDB ON). "
|
||||
"Integracion full pendiente: usar tql_duckdb::execute desde caller.";
|
||||
#else
|
||||
ask_ai.status = "SQL execute requires FN_TQL_DUCKDB build flag.";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Reject")) {
|
||||
ask_ai.response_code.clear();
|
||||
ask_ai.edit_buf[0] = '\0';
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Close")) {
|
||||
ask_ai.open = false;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
// data_table_ai_panel — modal "Ask AI" de la tabla TQL.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Responsabilidad:
|
||||
// - draw_ask_ai_modal: modal ImGui con prompt de lenguaje natural, llamada
|
||||
// a llm_anthropic::ask, render de la respuesta (codigo TQL/SQL editable),
|
||||
// botones Apply/Reject/Close, export CSV de la respuesta.
|
||||
//
|
||||
// El modal usa UiState.ask_* que se mueve aqui (AskAiState) para
|
||||
// encapsulacion. Vive en UiState del data_table principal — no en State.
|
||||
//
|
||||
// Dependencia con llm_anthropic:
|
||||
// - Si FN_LLM_ANTHROPIC esta definido: usa core/llm_anthropic.h (real).
|
||||
// - Si no: el stub del data_table.cpp provee tipos/funciones no-op.
|
||||
// - Tras la extraccion este modulo incluye el stub o el header real.
|
||||
//
|
||||
// Rangos del fuente original:
|
||||
// - Modal "Ask AI" : lineas 4636-4755
|
||||
// - Boton trigger : linea 1869 (dentro de draw_viz_selector)
|
||||
//
|
||||
// Dependencias: data_table_types.h, llm_anthropic.h (o stub), imgui.h.
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "imgui.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// AskAiState: estado del modal Ask AI. Debe persistir entre frames
|
||||
// (es parte de UiState). Extraido aqui para encapsulacion.
|
||||
struct AskAiState {
|
||||
bool open = false;
|
||||
bool busy = false;
|
||||
int mode = 0; // 0 = TQL, 1 = SQL (DuckDB)
|
||||
char question[2048] = {};
|
||||
std::string current_tql; // TQL emitido del state al abrir modal
|
||||
std::string response_raw; // texto raw del modelo
|
||||
std::string response_code; // bloque extraido (Lua o SQL)
|
||||
std::string error;
|
||||
std::string status; // "Sending..." / "Got response." / error
|
||||
char edit_buf[8192] = {}; // buffer editable de la propuesta
|
||||
};
|
||||
|
||||
// draw_ask_ai_modal — dibuja el modal "Ask AI".
|
||||
// Debe llamarse cada frame (pattern ImGui). Abre el popup si ask_ai.open==true.
|
||||
//
|
||||
// Parametros:
|
||||
// ask_ai — estado mutable del modal.
|
||||
// st — State principal (para tql::apply en Apply + active_headers/types).
|
||||
// active_headers / active_types — snapshot del output activo (para la llamada llm).
|
||||
// orig_cols — numero de cols originales (para tql::apply signature).
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_ask_ai_modal(AskAiState& ask_ai,
|
||||
State& st,
|
||||
const std::vector<std::string>& active_headers,
|
||||
const std::vector<ColumnType>& active_types,
|
||||
int orig_cols);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: data_table_ai_panel
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "void data_table::draw_ask_ai_modal(AskAiState& ask_ai, State& st, const std::vector<std::string>& active_headers, const std::vector<ColumnType>& active_types, int orig_cols)"
|
||||
description: "Modal 'Ask AI' de la tabla TQL: UI de prompt en lenguaje natural, llamada bloqueante a llm_anthropic::ask, render de la respuesta (codigo TQL o SQL editable), botones Apply/Reject/Close. El Apply ejecuta tql::apply sobre el estado actual de la tabla. Soporta modo TQL (default) y SQL (requiere FN_TQL_DUCKDB). Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c, fase 11 / issue 0080)."
|
||||
tags: [viz, table, imgui, ui, ai, llm, tql, cpp-tables, ask-ai]
|
||||
uses_functions:
|
||||
- data_table_cpp_viz
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/data_table_ai_panel.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: ask_ai
|
||||
desc: "Estado mutable del modal (AskAiState): prompt, modo TQL/SQL, buffers de respuesta, flags open/busy. Debe persistir entre frames (vive en UiState del data_table)."
|
||||
- name: st
|
||||
desc: "State principal de la tabla. Mutado por Apply: tql::apply modifica st.stages, filtros, sorts."
|
||||
- name: active_headers / active_types
|
||||
desc: "Snapshot del output del stage activo en el momento de abrir el modal. Se pasan a llm_anthropic::AskInput para que el modelo conozca el schema."
|
||||
- name: orig_cols
|
||||
desc: "Numero de columnas originales. Necesario para la firma de tql::apply."
|
||||
output: "Void. Efectos: mutates ask_ai (flags, buffers), mutates st (via tql::apply en Apply)."
|
||||
---
|
||||
|
||||
## Documentacion
|
||||
|
||||
Sub-funcion que encapsula el modal "Ask AI" de la tabla TQL (issue 0080, fase 11). Permite al usuario escribir una pregunta en lenguaje natural y recibir codigo TQL o SQL generado por un modelo de lenguaje.
|
||||
|
||||
### Flujo del modal
|
||||
|
||||
1. `ask_ai.open = true` (seteado por boton "Ask AI" en `draw_viz_selector`).
|
||||
2. Primer frame con `open=true`: `ImGui::OpenPopup("Ask AI")`.
|
||||
3. Modal se abre: prompt input, combo modo TQL/SQL.
|
||||
4. Usuario escribe pregunta y hace click en "Send":
|
||||
- Construye `llm_anthropic::AskInput` con `question`, `col_names`, `col_types`, `tql_current`.
|
||||
- Llamada **bloqueante** a `llm_anthropic::ask(in)` → `AskResult`.
|
||||
- Si error: `ask_ai.error` se muestra en rojo.
|
||||
- Si ok: `ask_ai.response_code` se copia a `ask_ai.edit_buf` (editable).
|
||||
5. Panel dividido en dos columnas: TQL actual (read-only) | propuesta (editable).
|
||||
6. Botones:
|
||||
- **Apply**: ejecuta `tql::apply(ask_ai.edit_buf, st, ...)`. Si ok: cierra modal.
|
||||
- **Reject**: limpia `edit_buf` y `response_code`.
|
||||
- **Close**: `ask_ai.open = false`.
|
||||
|
||||
### Dependencia llm_anthropic
|
||||
|
||||
Si `FN_LLM_ANTHROPIC` esta definido en el build, se incluye `core/llm_anthropic.h` (real). Si no, se usa el stub no-op que retorna error "not available". Pendiente promover a `cpp/functions/infra/llm_anthropic` (Wave 4, issue 0107c deuda tecnica).
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// En el render principal, llamar una vez por frame tras el grid:
|
||||
data_table::draw_ask_ai_modal(U.ask_ai, st,
|
||||
U.active_headers, U.active_types, orig_cols);
|
||||
|
||||
// El boton de apertura vive en draw_viz_selector:
|
||||
// if (ImGui::SmallButton("Ask AI##ask_open")) U.ask_ai.open = true;
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Llamar una vez por frame desde el entrypoint thin `data_table::render()` en el bloque de modales (junto a TQL show/apply, Custom column, etc.), SIEMPRE que el contexto ImGui tenga la ventana activa. No llamar desde apps directamente.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- La llamada a `llm_anthropic::ask` es **bloqueante** (red HTTP). La UI se congela brevemente. Consideracion futura: mover a hilo background + flag de estado asincrono.
|
||||
- El modal se destruye (EndPopup) si el usuario hace click fuera. `ask_ai.open` se pone a false por el `&ask_ai.open` del `BeginPopupModal`. Esto borra el estado del prompt en curso — consideracion UX conocida.
|
||||
- En modo SQL (`ask_ai.mode == 1`), Apply solo funciona si `FN_TQL_DUCKDB` esta definido; de lo contrario muestra mensaje de error informativo.
|
||||
- `llm_anthropic` no esta en el registry (deuda Wave 4). Hasta que se promueva, el stub vive inlined en este .cpp. No duplicar el stub en otros archivos.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
// data_table_chips — barra de chips superior de la tabla TQL.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Responsabilidad:
|
||||
// Toda la UI de chips que aparece en el area de chrome de la tabla:
|
||||
// - draw_joins_chips : chips de join con tablas secundarias.
|
||||
// - draw_filter_chips : chips de filtros activos (con boton de borrar).
|
||||
// - draw_breakout_chips : chips de breakout (group-by) activos.
|
||||
// - draw_aggregation_chips: chips de agregaciones activas.
|
||||
// - draw_sort_chips : chips de ordenamiento activos.
|
||||
// - draw_header_menu : popup menu al hacer right-click en cabecera de col.
|
||||
// - apply_header_sort_click: procesa click en cabecera (sort / multi-sort con Shift).
|
||||
//
|
||||
// Popups de anadir/editar chips (activados desde los chips o botones "+"):
|
||||
// - draw_add_filter_popup / draw_edit_filter_popup
|
||||
// - draw_add_breakout_popup / draw_edit_breakout_popup
|
||||
// - draw_add_aggregation_popup / draw_edit_agg_popup
|
||||
// - draw_add_sort_popup / draw_edit_sort_popup
|
||||
//
|
||||
// TQL preview / save / load (area de chrome stage 0):
|
||||
// - draw_tql_bar (NEW helper que engloba Show TQL, Apply TQL, Save/Load .tql)
|
||||
//
|
||||
// Helpers de tipo/op para los popups:
|
||||
// - draw_typed_ops: dibuja radio buttons de Op segun ColumnType.
|
||||
// - type_supports_range: retorna true si el tipo soporta Op::Range.
|
||||
//
|
||||
// Rangos del fuente original:
|
||||
// - draw_joins_chips : lineas 1897-2003
|
||||
// - draw_filter_chips : lineas 2009-2093
|
||||
// - draw_breakout_chips : lineas 2095-2188
|
||||
// - draw_aggregation_chips : lineas 2189-2238
|
||||
// - draw_sort_chips : lineas 2240-2309
|
||||
// - apply_header_sort_click : lineas 2311-2327
|
||||
// - draw_edit_filter_popup : lineas 2329-2370
|
||||
// - draw_edit_breakout_popup : lineas 2372-2395
|
||||
// - draw_edit_agg_popup : lineas 2397-2445
|
||||
// - draw_edit_sort_popup : lineas 2447-2472
|
||||
// - draw_typed_ops : lineas 2503-2510
|
||||
// - type_supports_range : lineas 2512-2514
|
||||
// - draw_add_filter_popup : lineas 2516-2569
|
||||
// - draw_add_breakout_popup : lineas 2571-2604
|
||||
// - draw_add_aggregation_popup: lineas 2606-2655
|
||||
// - draw_add_sort_popup : lineas 2657-2682
|
||||
// - draw_header_menu : lineas 2684-2892
|
||||
// - TQL preview/save/load : lineas 3272-3366 (dentro de render() stage 0)
|
||||
//
|
||||
// Dependencias: data_table_types.h, tql_emit.h, tql_apply.h, imgui.h.
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "imgui.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers de tipo/operacion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// draw_typed_ops: dibuja radio buttons para los operadores compatibles con el
|
||||
// tipo `t`. Rellena `out` con el op seleccionado. Retorna true si se selecciono.
|
||||
bool draw_typed_ops(ColumnType t, Op& out);
|
||||
|
||||
// type_supports_range: true si el tipo admite Op::Range (Int, Float, Date).
|
||||
bool type_supports_range(ColumnType t);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sort
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// apply_header_sort_click: registra un click en el header de col_name.
|
||||
// Sin Shift: sort primario (reemplaza todos los sorts).
|
||||
// Con Shift: sort secundario (agrega al sort primario existente).
|
||||
// Ciclo: Asc -> Desc -> none.
|
||||
void apply_header_sort_click(Stage& stg, const std::string& col_name, bool shift);
|
||||
|
||||
void draw_sort_chips(Stage& stg);
|
||||
void draw_add_sort_popup(Stage& stg, const char* const* headers, int n_cols,
|
||||
const std::vector<ColumnType>& types);
|
||||
void draw_edit_sort_popup(Stage& stg, const char* const* headers, int n_cols);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filtros
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols,
|
||||
const std::vector<ColumnType>& eff_types);
|
||||
void draw_add_filter_popup(Stage& stg, const char* const* eff_headers, int eff_cols,
|
||||
const std::vector<ColumnType>& eff_types);
|
||||
void draw_edit_filter_popup(Stage& stg, const char* const* headers, int n_cols,
|
||||
const std::vector<ColumnType>& types);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Breakouts (group-by)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void draw_breakout_chips(Stage& stg, const char* const* in_headers, int in_cols,
|
||||
const std::vector<ColumnType>& in_types);
|
||||
void draw_add_breakout_popup(Stage& stg, const char* const* in_headers, int in_cols,
|
||||
const std::vector<ColumnType>& in_types,
|
||||
const char* const* cur_cells, int cur_rows);
|
||||
void draw_edit_breakout_popup(Stage& stg, const char* const* headers, int n_cols);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agregaciones
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void draw_aggregation_chips(Stage& stg, const char* const* in_headers, int in_cols);
|
||||
void draw_add_aggregation_popup(Stage& stg, const char* const* in_headers, int in_cols,
|
||||
const std::vector<ColumnType>& in_types);
|
||||
void draw_edit_agg_popup(Stage& stg, const char* const* headers, int n_cols);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Joins
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// draw_joins_chips: chips de join con las tablas joinables. Solo visible si
|
||||
// joinables no esta vacio. Mutates st.stages y la configuracion de join.
|
||||
void draw_joins_chips(State& st, const std::vector<TableInput>& joinables,
|
||||
const char* const* eff_headers, int eff_cols,
|
||||
const std::vector<ColumnType>& eff_types);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header menu (right-click en cabecera de columna)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// draw_header_menu: popup que aparece al right-click en el header de columna
|
||||
// `col`. Incluye sort, filter, conditional color, type change, etc.
|
||||
// is_raw_stage: true si estamos en stage 0 (permite "Change type" / "Derived").
|
||||
void draw_header_menu(State& st, Stage& stg, int col,
|
||||
const char* const* eff_headers_arr, int eff_cols,
|
||||
const std::vector<ColumnType>& eff_types,
|
||||
int orig_cols, bool is_raw_stage);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TQL bar (Show TQL / Apply TQL / Save .tql / Load .tql)
|
||||
// Area de chrome adicional en stage 0. Nuevo helper que extrae el bloque
|
||||
// de lineas 3272-3366 del render() original.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TqlBarState: estado del area TQL (vive en UiState).
|
||||
struct TqlBarState {
|
||||
bool show_open = false;
|
||||
std::string show_text;
|
||||
bool apply_open = false;
|
||||
std::string apply_text;
|
||||
std::string apply_error;
|
||||
char file_path[256] = "table.tql";
|
||||
std::string io_status; // "saved: ..." / "loaded: ..." / "load FAILED: ..."
|
||||
};
|
||||
|
||||
// draw_tql_bar: dibuja los botones Show TQL, Apply TQL, Save .tql, Load .tql
|
||||
// y los modales correspondientes. Mutates st via tql::apply en Apply.
|
||||
// active_headers/types/cells/row_count/orig_cols: necesarios para tql::emit + apply.
|
||||
void draw_tql_bar(TqlBarState& tql_bar,
|
||||
State& st,
|
||||
const std::vector<std::string>& orig_headers,
|
||||
const std::vector<ColumnType>& orig_types,
|
||||
const char* const* cells,
|
||||
int row_count, int orig_cols);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: data_table_chips
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "void data_table::draw_filter_chips(Stage& stg, const char* const* eff_headers, int eff_cols, const std::vector<ColumnType>& eff_types)"
|
||||
description: "Barra de chips superior de la tabla TQL: render y edicion de filtros activos, breakouts (group-by), agregaciones, sorts, joins con tablas secundarias, header-menu de columna (sort/filter/conditional-color/type-change), y area TQL (Show TQL / Apply TQL / Save .tql / Load .tql). Es la sub-funcion mas grande del refactor 0107c (~1000 LOC, 17 funciones). Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c)."
|
||||
tags: [viz, table, imgui, ui, chips, filters, sort, aggregation, tql, cpp-tables, joins]
|
||||
uses_functions:
|
||||
- data_table_color_rules_cpp_viz
|
||||
- data_table_cpp_viz
|
||||
- tql_emit_cpp_core
|
||||
- tql_apply_cpp_core
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/data_table_chips.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: stg
|
||||
desc: "Stage activo (st.stages[active]): contiene filters, breakouts, aggregations, sorts, derived. Mutado por todos los chips y popups de edicion."
|
||||
- name: eff_headers / eff_cols
|
||||
desc: "Headers y numero de columnas efectivas del stage activo (orig + derived). Usados en labels de chips y en los popups de anadir/editar."
|
||||
- name: eff_types
|
||||
desc: "Tipos de columna efectivos. Usados para filtrar los operadores disponibles (draw_typed_ops) y para formatear los popups."
|
||||
- name: st (para joins y header-menu)
|
||||
desc: "State completo: necesario para draw_joins_chips (accede a st.stages) y draw_header_menu (accede a st.color_rules, st.col_visible)."
|
||||
- name: joinables (para joins)
|
||||
desc: "Vector de TableInput secundarias disponibles para join. Si vacio, draw_joins_chips no muestra nada."
|
||||
output: "Void. Todos los efectos son mutaciones de stg (Stage) o st (State) en respuesta a la interaccion del usuario."
|
||||
---
|
||||
|
||||
## Documentacion
|
||||
|
||||
Sub-funcion mas grande del refactor 0107c. Encapsula toda la UI de la barra de chips de la tabla TQL.
|
||||
|
||||
### Mapa de funciones
|
||||
|
||||
| Funcion | LOC aprox | Responsabilidad |
|
||||
|---|---|---|
|
||||
| `draw_filter_chips` | ~85 | Chips de filtros activos con X para borrar |
|
||||
| `draw_add_filter_popup` | ~55 | Popup para anadir filtro nuevo |
|
||||
| `draw_edit_filter_popup` | ~42 | Popup edicion de filtro existente (right-click en chip) |
|
||||
| `draw_breakout_chips` | ~93 | Chips de breakout activos con X |
|
||||
| `draw_add_breakout_popup` | ~34 | Popup anadir breakout (col de agrupacion) |
|
||||
| `draw_edit_breakout_popup` | ~24 | Popup edicion breakout |
|
||||
| `draw_aggregation_chips` | ~50 | Chips de agregaciones activas con X |
|
||||
| `draw_add_aggregation_popup` | ~50 | Popup anadir agregacion (Count, Sum, Mean, ...) |
|
||||
| `draw_edit_agg_popup` | ~48 | Popup edicion agregacion |
|
||||
| `draw_sort_chips` | ~70 | Chips de sort activos con X |
|
||||
| `draw_add_sort_popup` | ~26 | Popup anadir sort |
|
||||
| `draw_edit_sort_popup` | ~26 | Popup edicion sort |
|
||||
| `apply_header_sort_click` | ~17 | Sort al click en cabecera (Asc/Desc/none ciclo) |
|
||||
| `draw_joins_chips` | ~107 | Chips de joins con tablas secundarias |
|
||||
| `draw_header_menu` | ~209 | Menu contextual de columna (sort/filter/color/type) |
|
||||
| `draw_typed_ops` | ~8 | Radio buttons de Op segun tipo de columna |
|
||||
| `type_supports_range` | ~3 | Bool tipo soporta rango |
|
||||
| `draw_tql_bar` | ~95 | Botones + modales TQL show/apply/save/load |
|
||||
|
||||
### Orden de llamada en el chrome
|
||||
|
||||
```cpp
|
||||
// Stage 0 (chrome_visible):
|
||||
draw_joins_chips(st, joinables, hdrs, cols, types);
|
||||
draw_filter_chips(act, hdrs, cols, types);
|
||||
draw_add_filter_popup(act, hdrs, cols, types);
|
||||
draw_edit_filter_popup(act, hdrs, cols, types);
|
||||
draw_breakout_chips(act, hdrs, cols, types);
|
||||
// ... etc.
|
||||
draw_sort_chips(act);
|
||||
draw_tql_bar(tql_bar, st, orig_headers, orig_types, cells, rows, orig_cols);
|
||||
```
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// Bloque chrome stage 0:
|
||||
if (chrome_visible) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 2));
|
||||
|
||||
data_table::draw_filter_chips(act, eff_headers, eff_cols, eff_types);
|
||||
data_table::draw_add_filter_popup(act, eff_headers, eff_cols, eff_types);
|
||||
data_table::draw_edit_filter_popup(act, eff_headers, eff_cols, eff_types);
|
||||
|
||||
data_table::draw_breakout_chips(act, eff_headers, eff_cols, eff_types);
|
||||
// ...
|
||||
|
||||
data_table::draw_sort_chips(act);
|
||||
data_table::draw_add_sort_popup(act, eff_headers, eff_cols, eff_types);
|
||||
data_table::draw_edit_sort_popup(act, eff_headers, eff_cols);
|
||||
|
||||
data_table::draw_tql_bar(U.tql_bar, st, orig_h, orig_t, cells, rows, orig_cols);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Llamar desde el entrypoint thin `data_table::render()` en el bloque `chrome_visible`. El orden importa: joins primero, luego filtros, breakouts, agregaciones, sorts, TQL bar. No llamar fuera del contexto de un frame ImGui activo.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `draw_header_menu` DEBE llamarse desde dentro del popup de cabecera (`ImGui::BeginPopupContextItem`) — no se puede llamar en el loop normal de render.
|
||||
- Los popups de add/edit usan IDs de ImGui fijos (`"##addf"`, `"##editf"`, etc.). Si se tienen multiples instancias de data_table en la misma ventana, usar `ImGui::PushID(table_id)` antes de llamar a los chips.
|
||||
- `draw_tql_bar` incluye los modales "Show TQL" y "Apply TQL" via `ImGui::BeginPopupModal`. Deben llamarse fuera del bloque `PushStyleVar` si los estilos interfieren con el modal.
|
||||
- `apply_header_sort_click` sin Shift reemplaza TODOS los sorts existentes por el nuevo. Es el comportamiento esperado para click simple. Shift agrega sort secundario.
|
||||
- `draw_joins_chips` asume que `joinables` es la slice de `tables` excluyendo la tabla principal. Si se pasa la tabla principal como joinable, se creara un self-join.
|
||||
@@ -0,0 +1,353 @@
|
||||
// data_table_color_rules — editor de reglas de color por columna + aplicacion.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Responsabilidad:
|
||||
// - Helpers de color: auto_categorical_color, resolve_categorical_dot_color,
|
||||
// apply_color_rules_for_cell.
|
||||
// - UI del submenu "Conditional color" dentro del header-menu de columnas:
|
||||
// draw_color_rule_menu.
|
||||
//
|
||||
// hex_to_imcolor, parse_hex_color, lerp_color_along_stops son helpers de color
|
||||
// puros. Se definen como static aqui (no se exportan) porque data_table.cpp
|
||||
// tiene sus propias versiones static tambien — ambos TU son independientes.
|
||||
|
||||
#include "viz/data_table_color_rules.h"
|
||||
#include "data_table/data_table_internal.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "core/data_table_types.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// hex_to_imcolor: "#rrggbb" -> ImVec4. Returns {-1,-1,-1,-1} on failure.
|
||||
static ImVec4 hex_to_imcolor(const std::string& hex) {
|
||||
const char* p = hex.c_str();
|
||||
if (*p == '#') ++p;
|
||||
unsigned int r = 0, g = 0, b = 0;
|
||||
if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3)
|
||||
return ImVec4(-1.f, -1.f, -1.f, -1.f);
|
||||
return ImVec4(r / 255.f, g / 255.f, b / 255.f, 1.f);
|
||||
}
|
||||
|
||||
// lerp_color_along_stops: LERP entre N color stops en [0,1].
|
||||
static ImU32 lerp_color_along_stops(
|
||||
const std::vector<data_table::ColorStop>& stops, float t, float alpha)
|
||||
{
|
||||
static const std::vector<data_table::ColorStop> kDefault = {
|
||||
{0.0f, "#22c55e"},
|
||||
{0.5f, "#f59e0b"},
|
||||
{1.0f, "#ef4444"},
|
||||
};
|
||||
const auto& sv = stops.empty() ? kDefault : stops;
|
||||
std::vector<data_table::ColorStop> sorted_sv = sv;
|
||||
std::sort(sorted_sv.begin(), sorted_sv.end(),
|
||||
[](const data_table::ColorStop& a, const data_table::ColorStop& b){
|
||||
return a.position < b.position;
|
||||
});
|
||||
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
|
||||
if (t <= sorted_sv.front().position)
|
||||
return data_table::parse_hex_color(sorted_sv.front().color, alpha);
|
||||
if (t >= sorted_sv.back().position)
|
||||
return data_table::parse_hex_color(sorted_sv.back().color, alpha);
|
||||
for (size_t i = 0; i + 1 < sorted_sv.size(); ++i) {
|
||||
const auto& lo = sorted_sv[i];
|
||||
const auto& hi = sorted_sv[i + 1];
|
||||
if (t >= lo.position && t <= hi.position) {
|
||||
float span = hi.position - lo.position;
|
||||
float f = (span > 1e-6f) ? (t - lo.position) / span : 0.f;
|
||||
ImVec4 ca = hex_to_imcolor(lo.color);
|
||||
ImVec4 cb = hex_to_imcolor(hi.color);
|
||||
if (ca.x < 0.f) ca = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
|
||||
if (cb.x < 0.f) cb = ImVec4(0.5f, 0.5f, 0.5f, 1.f);
|
||||
float r = ca.x + f * (cb.x - ca.x);
|
||||
float g = ca.y + f * (cb.y - ca.y);
|
||||
float b = ca.z + f * (cb.z - ca.z);
|
||||
unsigned int ri = (unsigned int)(r * 255.f + 0.5f);
|
||||
unsigned int gi = (unsigned int)(g * 255.f + 0.5f);
|
||||
unsigned int bi = (unsigned int)(b * 255.f + 0.5f);
|
||||
unsigned int ai = (unsigned int)(alpha * 255.f + 0.5f);
|
||||
return IM_COL32(ri, gi, bi, ai);
|
||||
}
|
||||
}
|
||||
return data_table::parse_hex_color(sorted_sv.back().color, alpha);
|
||||
}
|
||||
|
||||
} // anon namespace
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parse_hex_color: "#rrggbb" / "#rrggbbaa" -> ImU32 with explicit alpha.
|
||||
// Declared in data_table_color_rules.h (public API — used by data_table.cpp
|
||||
// cell renderers and by apply_color_rules_for_cell).
|
||||
// ---------------------------------------------------------------------------
|
||||
ImU32 parse_hex_color(const std::string& hex, float alpha)
|
||||
{
|
||||
const char* p = hex.c_str();
|
||||
if (*p == '#') ++p;
|
||||
unsigned int r = 0, g = 0, b = 0, a = 255;
|
||||
int parsed = std::sscanf(p, "%02x%02x%02x%02x", &r, &g, &b, &a);
|
||||
if (parsed < 3) return IM_COL32(128, 128, 128, 255);
|
||||
if (parsed == 3) {
|
||||
a = (unsigned int)(alpha * 255.f + 0.5f);
|
||||
}
|
||||
return IM_COL32(r, g, b, a);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// auto_categorical_color: deterministic palette for a string value.
|
||||
// Hashes the value to an index in a fixed 12-color palette (Tailwind-ish).
|
||||
// Same value always maps to the same color. FNV-1a 32-bit.
|
||||
// ---------------------------------------------------------------------------
|
||||
ImU32 auto_categorical_color(const char* value, float alpha)
|
||||
{
|
||||
static const unsigned int kPalette[] = {
|
||||
0xFFef4444u, // red-500
|
||||
0xFFf97316u, // orange-500
|
||||
0xFFf59e0bu, // amber-500
|
||||
0xFF84cc16u, // lime-500
|
||||
0xFF22c55eu, // green-500
|
||||
0xFF14b8a6u, // teal-500
|
||||
0xFF06b6d4u, // cyan-500
|
||||
0xFF3b82f6u, // blue-500
|
||||
0xFF8b5cf6u, // violet-500
|
||||
0xFFa855f7u, // purple-500
|
||||
0xFFec4899u, // pink-500
|
||||
0xFFd946efu, // fuchsia-500
|
||||
};
|
||||
constexpr size_t N = sizeof(kPalette) / sizeof(kPalette[0]);
|
||||
uint32_t h = 2166136261u;
|
||||
for (const char* p = value ? value : ""; *p; ++p) {
|
||||
h ^= (uint8_t)(*p);
|
||||
h *= 16777619u;
|
||||
}
|
||||
unsigned int packed = kPalette[h % N];
|
||||
unsigned int a = (unsigned int)(alpha * 255.f + 0.5f);
|
||||
unsigned int rgb = packed & 0x00FFFFFFu;
|
||||
return rgb | (a << 24);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolve_categorical_dot_color: look up dot color from rule's dot_map; fall
|
||||
// back to auto_categorical_color if no match.
|
||||
// ---------------------------------------------------------------------------
|
||||
ImU32 resolve_categorical_dot_color(const ColorRule& cr, const char* value)
|
||||
{
|
||||
if (!cr.dot_map.empty()) {
|
||||
for (const auto& kv : cr.dot_map) {
|
||||
if (kv.first == (value ? value : ""))
|
||||
return parse_hex_color(kv.second, cr.dot_alpha);
|
||||
}
|
||||
}
|
||||
return auto_categorical_color(value, cr.dot_alpha);
|
||||
}
|
||||
|
||||
// parse_cell_number: parse a cell string as double. Returns NaN on failure.
|
||||
// Only needed here (not shared).
|
||||
static double parse_cell_number(const char* s) {
|
||||
if (!s || !*s) return std::numeric_limits<double>::quiet_NaN();
|
||||
char* end = nullptr;
|
||||
double v = std::strtod(s, &end);
|
||||
if (end == s) return std::numeric_limits<double>::quiet_NaN();
|
||||
return v;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// apply_color_rules_for_cell: applies all matching ColorRules for column `c`.
|
||||
// Must be called AFTER ImGui::TableSetColumnIndex and BEFORE cell renderer.
|
||||
// ---------------------------------------------------------------------------
|
||||
void apply_color_rules_for_cell(const State& st,
|
||||
int c, const char* cell,
|
||||
ImVec2 cell_min, float cell_w, float cell_h,
|
||||
float& dot_advance_px)
|
||||
{
|
||||
dot_advance_px = 0.f;
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
for (const auto& cr : st.color_rules) {
|
||||
if (cr.col != c) continue;
|
||||
switch (cr.kind) {
|
||||
case ColorRuleKind::CellBg: {
|
||||
if (cell && cr.equals == cell) {
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ColorRuleKind::NumericRange: {
|
||||
double v = parse_cell_number(cell);
|
||||
if (std::isnan(v)) break;
|
||||
double span = cr.range_max - cr.range_min;
|
||||
float t = (span > 1e-12) ? (float)((v - cr.range_min) / span) : 0.5f;
|
||||
ImU32 tint = lerp_color_along_stops(cr.range_stops, t, cr.range_alpha);
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, tint);
|
||||
break;
|
||||
}
|
||||
case ColorRuleKind::CategoricalDot: {
|
||||
ImU32 dot = resolve_categorical_dot_color(cr, cell);
|
||||
float radius = cr.dot_radius_px > 0.f ? cr.dot_radius_px : 4.0f;
|
||||
ImVec2 c1(cell_min.x + radius + 2.f,
|
||||
cell_min.y + cell_h * 0.5f);
|
||||
draw->AddCircleFilled(c1, radius, dot, 12);
|
||||
dot_advance_px = radius * 2.f + 6.f;
|
||||
(void)cell_w;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_color_rule_menu: dibuja el submenu "Conditional color" para la columna
|
||||
// `col`. Retorna true si el usuario hizo click en "Apply" (la regla ya fue
|
||||
// anadida a st.color_rules).
|
||||
//
|
||||
// editor_st: ColorRuleEditorState de UiState (draft state per columna).
|
||||
// col_type: tipo efectivo de la columna (para auto-seleccion de modo).
|
||||
// ---------------------------------------------------------------------------
|
||||
bool draw_color_rule_menu(State& st, int col, ColumnType col_type,
|
||||
ColorRuleEditorState& editor_st)
|
||||
{
|
||||
bool applied = false;
|
||||
|
||||
// Auto-pick sensible default on first open for this column.
|
||||
if (editor_st.color_rule_kind.find(col) == editor_st.color_rule_kind.end()) {
|
||||
editor_st.color_rule_kind[col] =
|
||||
(col_type == ColumnType::Int || col_type == ColumnType::Float) ? 2 : 0;
|
||||
}
|
||||
int& kind_i = editor_st.color_rule_kind[col]; // 0=CellBg, 1=CatDot, 2=NumRange
|
||||
|
||||
ImGui::TextDisabled("Mode");
|
||||
ImGui::RadioButton("Cell bg", &kind_i, 0); ImGui::SameLine();
|
||||
ImGui::RadioButton("Categorical dot", &kind_i, 1); ImGui::SameLine();
|
||||
ImGui::RadioButton("Numeric range", &kind_i, 2);
|
||||
ImGui::Separator();
|
||||
|
||||
bool apply_clicked = false;
|
||||
ColorRule draft;
|
||||
draft.col = col;
|
||||
|
||||
if (kind_i == 0) {
|
||||
// ---- Cell bg ----
|
||||
auto& vbuf = editor_st.color_value_inputs[col];
|
||||
vbuf.resize(256, '\0');
|
||||
if (editor_st.color_picker_vals.find(col) == editor_st.color_picker_vals.end())
|
||||
editor_st.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f);
|
||||
ImVec4& cv = editor_st.color_picker_vals[col];
|
||||
ImGui::SetNextItemWidth(180);
|
||||
ImGui::InputText("equals", vbuf.data(), vbuf.size());
|
||||
ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs);
|
||||
if (ImGui::Button("Apply##cellbg")) {
|
||||
draft.kind = ColorRuleKind::CellBg;
|
||||
draft.equals = std::string(vbuf.c_str());
|
||||
draft.color = (unsigned int)ImGui::ColorConvertFloat4ToU32(cv);
|
||||
apply_clicked = true;
|
||||
}
|
||||
}
|
||||
else if (kind_i == 1) {
|
||||
// ---- Categorical dot ----
|
||||
auto& d = editor_st.cat_dot_drafts[col];
|
||||
ImGui::TextWrapped("Dibuja un punto coloreado al inicio de cada celda.");
|
||||
ImGui::TextWrapped("Color asignado automaticamente por valor (palette de 12).");
|
||||
ImGui::SetNextItemWidth(120);
|
||||
ImGui::SliderFloat("alpha", &d.alpha, 0.2f, 1.0f);
|
||||
ImGui::SetNextItemWidth(120);
|
||||
ImGui::SliderFloat("radius px", &d.radius_px, 2.0f, 9.0f);
|
||||
// Preview swatches (8 sample values).
|
||||
ImGui::TextDisabled("preview:");
|
||||
ImGui::SameLine();
|
||||
const char* samples[] = {"A","B","C","D","E","F","G","H"};
|
||||
for (auto* s : samples) {
|
||||
ImU32 col_u = auto_categorical_color(s, d.alpha);
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
ImGui::GetWindowDrawList()->AddCircleFilled(
|
||||
ImVec2(p.x + d.radius_px, p.y + ImGui::GetTextLineHeight() * 0.5f),
|
||||
d.radius_px, col_u, 12);
|
||||
ImGui::Dummy(ImVec2(d.radius_px * 2.f + 2.f, ImGui::GetTextLineHeight()));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::NewLine();
|
||||
if (ImGui::Button("Apply##catdot")) {
|
||||
draft.kind = ColorRuleKind::CategoricalDot;
|
||||
draft.dot_alpha = d.alpha;
|
||||
draft.dot_radius_px = d.radius_px;
|
||||
apply_clicked = true;
|
||||
}
|
||||
}
|
||||
else if (kind_i == 2) {
|
||||
// ---- Numeric range (3-color gradient) ----
|
||||
auto& d = editor_st.num_range_drafts[col];
|
||||
ImGui::SetNextItemWidth(110);
|
||||
ImGui::InputDouble("min", &d.min);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110);
|
||||
ImGui::InputDouble("max", &d.max);
|
||||
ImGui::SetNextItemWidth(140);
|
||||
ImGui::SliderFloat("alpha", &d.alpha, 0.05f, 0.9f);
|
||||
ImGui::SetNextItemWidth(100);
|
||||
ImGui::InputText("lo color (hex)", d.c_lo, sizeof(d.c_lo));
|
||||
ImGui::SetNextItemWidth(100);
|
||||
ImGui::InputText("mid color (hex)", d.c_md, sizeof(d.c_md));
|
||||
ImGui::SetNextItemWidth(100);
|
||||
ImGui::InputText("hi color (hex)", d.c_hi, sizeof(d.c_hi));
|
||||
// Preview gradient bar (32 lerped cells).
|
||||
std::vector<ColorStop> preview_stops = {
|
||||
{0.0f, std::string("#") + d.c_lo},
|
||||
{0.5f, std::string("#") + d.c_md},
|
||||
{1.0f, std::string("#") + d.c_hi},
|
||||
};
|
||||
ImVec2 p0 = ImGui::GetCursorScreenPos();
|
||||
float W = 256.f, H = 16.f;
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
float t = i / 31.f;
|
||||
ImU32 col_u = lerp_color_along_stops(preview_stops, t, 1.0f);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||
ImVec2(p0.x + (W / 32.f) * i, p0.y),
|
||||
ImVec2(p0.x + (W / 32.f) * (i + 1), p0.y + H),
|
||||
col_u);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(W, H + 4.f));
|
||||
if (ImGui::Button("Apply##numrange")) {
|
||||
draft.kind = ColorRuleKind::NumericRange;
|
||||
draft.range_min = d.min;
|
||||
draft.range_max = d.max;
|
||||
draft.range_alpha = d.alpha;
|
||||
draft.range_stops = preview_stops;
|
||||
apply_clicked = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (apply_clicked) {
|
||||
// Replace any existing rule of same kind on this col (one per kind).
|
||||
for (size_t i = 0; i < st.color_rules.size();) {
|
||||
if (st.color_rules[i].col == col &&
|
||||
st.color_rules[i].kind == draft.kind) {
|
||||
st.color_rules.erase(st.color_rules.begin() + (int)i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
st.color_rules.push_back(draft);
|
||||
ImGui::CloseCurrentPopup();
|
||||
applied = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear col")) {
|
||||
for (size_t i = 0; i < st.color_rules.size();) {
|
||||
if (st.color_rules[i].col == col)
|
||||
st.color_rules.erase(st.color_rules.begin() + (int)i);
|
||||
else
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
// data_table_color_rules — editor de reglas de color por columna + aplicacion.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp.
|
||||
// Issue 0107c. Wave de refactor para partir el god-file 4777 LOC.
|
||||
//
|
||||
// Responsabilidad: helpers estaticos de color (parse_hex_color, resolve_categorical_dot_color,
|
||||
// apply_color_rules_for_cell, auto_categorical_color) + UI del submenu "Conditional color"
|
||||
// dentro del header-menu de columnas.
|
||||
//
|
||||
// Dependencias: data_table_types.h (ColorRule, ColorRuleKind, State).
|
||||
// imgui.h, core/icons_tabler.h.
|
||||
// NO depende de UiState global — recibe el draft state que necesita por referencia.
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "imgui.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers de color (pure/static, no ImGui state).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// auto_categorical_color: palette determinista de 12 colores para un valor
|
||||
// string. Mismo valor -> mismo color siempre. v1.5.0.
|
||||
ImU32 auto_categorical_color(const char* value, float alpha = 1.0f);
|
||||
|
||||
// resolve_categorical_dot_color: busca en dot_map de la regla; fallback a
|
||||
// auto_categorical_color si no hay match.
|
||||
ImU32 resolve_categorical_dot_color(const ColorRule& cr, const char* value);
|
||||
|
||||
// parse_hex_color: "#rrggbb" / "#rrggbbaa" -> ImU32 con alpha explicitamente.
|
||||
// Fallback a gris IM_COL32(128,128,128,255) en error.
|
||||
ImU32 parse_hex_color(const std::string& hex, float alpha = 1.0f);
|
||||
|
||||
// apply_color_rules_for_cell: aplica todas las ColorRules para la columna `c`
|
||||
// ANTES del renderer de celda. Llama ImGui::TableSetBgColor para CellBg,
|
||||
// dibuja dots para CategoricalDot, y pinta overlay de rango para NumericRange.
|
||||
// `dot_advance_px` (out): pixeles a avanzar en x antes de texto (por dot).
|
||||
// Debe llamarse justo despues de ImGui::TableSetColumnIndex, antes del renderer.
|
||||
void apply_color_rules_for_cell(const State& st,
|
||||
int c, const char* cell,
|
||||
ImVec2 cell_min, float cell_w, float cell_h,
|
||||
float& dot_advance_px);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI del editor de reglas de color (submenu dentro del header-menu de cols).
|
||||
// Llamada desde draw_header_menu cuando el usuario abre "Conditional color".
|
||||
// Mutates `st.color_rules` al hacer click en "Apply".
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ColorRuleEditorState: estado de draft del editor de reglas de color.
|
||||
// Un ColorRuleEditorState vive en UiState (singleton por instancia de tabla).
|
||||
// Guarda los drafts per-column en maps indexados por columna.
|
||||
struct ColorRuleEditorState {
|
||||
// 0=CellBg, 1=CategoricalDot, 2=NumericRange — per column.
|
||||
std::unordered_map<int, int> color_rule_kind;
|
||||
|
||||
// CellBg draft per col.
|
||||
std::unordered_map<int, std::string> color_value_inputs;
|
||||
std::unordered_map<int, ImVec4> color_picker_vals;
|
||||
|
||||
// NumericRange draft per col.
|
||||
struct NumRangeDraft {
|
||||
double min = 0.0;
|
||||
double max = 100.0;
|
||||
float alpha = 0.25f;
|
||||
char c_lo[8] = "22c55e"; // green-500
|
||||
char c_md[8] = "f59e0b"; // amber-500
|
||||
char c_hi[8] = "ef4444"; // red-500
|
||||
};
|
||||
std::unordered_map<int, NumRangeDraft> num_range_drafts;
|
||||
|
||||
// CategoricalDot draft per col.
|
||||
struct CatDotDraft {
|
||||
float alpha = 1.0f;
|
||||
float radius_px = 4.0f;
|
||||
};
|
||||
std::unordered_map<int, CatDotDraft> cat_dot_drafts;
|
||||
};
|
||||
|
||||
// draw_color_rule_menu: dibuja el submenu "Conditional color" para la columna
|
||||
// `col`. Retorna true si el usuario hizo click en "Apply" (la regla ya fue
|
||||
// anadida a st.color_rules por la funcion).
|
||||
// editor_st: draft state per tabla (ColorRuleEditorState de UiState).
|
||||
// col_type: tipo efectivo de la columna (para auto-seleccion de modo).
|
||||
bool draw_color_rule_menu(State& st, int col, ColumnType col_type,
|
||||
ColorRuleEditorState& editor_st);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: data_table_color_rules
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "void data_table::apply_color_rules_for_cell(const State& st, int col, const char* cell, ImVec2 cell_min, float cell_w, float cell_h, float& dot_advance_px)"
|
||||
description: "Helpers de color (parse_hex_color, auto_categorical_color, resolve_categorical_dot_color, apply_color_rules_for_cell) + editor UI del submenu Conditional color para columnas de tabla. Soporta tres modos: CellBg (igualdad exacta), CategoricalDot (punto coloreado por valor via palette determinista de 12 colores), NumericRange (degradado lo/mid/hi por rango). Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c)."
|
||||
tags: [viz, table, imgui, ui, color-rules, tql, cpp-tables]
|
||||
uses_functions:
|
||||
- data_table_cpp_viz
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/data_table_color_rules.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: st
|
||||
desc: "State del data_table que contiene st.color_rules (vector de ColorRule). Solo lectura en apply_color_rules_for_cell; mutado por draw_color_rule_menu al anadir reglas."
|
||||
- name: col
|
||||
desc: "Indice de columna (0-based en el output visible del stage activo)."
|
||||
- name: cell
|
||||
desc: "Valor de la celda como C-string nullable. Null se trata como cadena vacia."
|
||||
- name: cell_min
|
||||
desc: "Esquina top-left de la celda en coordenadas de pantalla (ImGui::GetItemRectMin o equivalente). Usado para posicionar dots CategoricalDot."
|
||||
- name: cell_w / cell_h
|
||||
desc: "Anchura y altura de la celda en pixeles. Usados para centrar el dot verticalmente y calcular la zona de fondo NumericRange."
|
||||
- name: dot_advance_px
|
||||
desc: "Salida: pixeles a avanzar en X antes de renderizar texto (reserva el espacio del dot). 0 si no hay regla CategoricalDot activa para esta columna."
|
||||
output: "Void para apply_color_rules_for_cell (efectos: TableSetBgColor + draw dot). Bool para draw_color_rule_menu (true = regla anadida a st.color_rules)."
|
||||
---
|
||||
|
||||
## Documentacion
|
||||
|
||||
Sub-funcion que encapsula toda la logica de color condicional por celda de la tabla TQL. Se extrae de `modules/data_table/data_table.cpp` como parte del issue 0107c (partir el god-file 4777 LOC).
|
||||
|
||||
### Funciones publicas
|
||||
|
||||
| Funcion | Uso |
|
||||
|---|---|
|
||||
| `parse_hex_color(hex, alpha)` | Convierte `"#rrggbb"` / `"#rrggbbaa"` a `ImU32`. Fallback gris en error. |
|
||||
| `auto_categorical_color(value, alpha)` | Palette determinista 12 colores via FNV-1a hash. Mismo valor → mismo color siempre. |
|
||||
| `resolve_categorical_dot_color(cr, value)` | Busca en `cr.dot_map`; fallback a `auto_categorical_color`. |
|
||||
| `apply_color_rules_for_cell(st, col, cell, ...)` | Aplica todas las reglas activas para `col`. Llamar ANTES del renderer de celda, despues de `TableSetColumnIndex`. |
|
||||
| `draw_color_rule_menu(st, col, col_type, editor_st)` | UI del submenu "Conditional color" (dentro del header-menu). Mutates `st.color_rules` en Apply. |
|
||||
|
||||
### Modo CellBg
|
||||
|
||||
Igualdad exacta: si `cell == rule.equals` pinta el fondo con `rule.color`.
|
||||
|
||||
### Modo CategoricalDot
|
||||
|
||||
Dibuja un circulo coloreado a la izquierda de cada celda. Color determinado por `resolve_categorical_dot_color`. `dot_advance_px` retorna el offset para que el texto no solape el punto.
|
||||
|
||||
### Modo NumericRange
|
||||
|
||||
Parsea la celda como double. Interpola entre tres colores (lo/mid/hi) segun el valor relativo en `[rule.range_min, rule.range_max]`. Pinta fondo con `TableSetBgColor`.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// En el render del grid, por cada celda antes del renderer:
|
||||
float dot_px = 0.f;
|
||||
data_table::apply_color_rules_for_cell(st, col, cell_value,
|
||||
ImGui::GetItemRectMin(), col_width, row_height, dot_px);
|
||||
if (dot_px > 0.f)
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + dot_px);
|
||||
// ... renderer normal ...
|
||||
|
||||
// Para anadir una regla desde el header-menu:
|
||||
data_table::ColorRuleEditorState editor;
|
||||
if (ImGui::BeginMenu("Conditional color")) {
|
||||
data_table::draw_color_rule_menu(st, col, col_type, editor);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usarla cuando renderices celdas individuales de la tabla TQL (llamar desde `data_table_grid`) y cuando muestres el menu de cabecera de columna que permita al usuario definir reglas de color. NO llamarla fuera del contexto de un `ImGui::BeginTable / EndTable` activo.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `apply_color_rules_for_cell` DEBE llamarse despues de `ImGui::TableSetColumnIndex` y ANTES del renderer de celda. Si se invierte el orden, `TableSetBgColor` no afecta a la celda correcta.
|
||||
- El dot CategoricalDot requiere que el draw list de la ventana este activo (`ImGui::GetWindowDrawList()`). No llamar desde un contexto de renderizado diferido.
|
||||
- `parse_hex_color` acepta strings sin `#` pero si el string tiene caracteres no hex devuelve gris fallback — no paniquea.
|
||||
- `auto_categorical_color` usa FNV-1a 32-bit: con >12 valores distintos habra colisiones (intencional, visualmente aceptable).
|
||||
- En la extraccion desde data_table.cpp (tarea 3.8) hay que eliminar la declaracion `static` de `parse_hex_color` y `auto_categorical_color` en el .cpp original y hacer forward-include a este header.
|
||||
@@ -0,0 +1,204 @@
|
||||
// data_table_drill — drill-down stack + breadcrumb de stages.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Rangos de lineas del fuente original:
|
||||
// - make_drill_filter : lineas 699-706
|
||||
// - apply_drill_step : lineas 708-718
|
||||
// - undo_drill_step : lineas 720-730
|
||||
// - drill_up : lineas 732-737
|
||||
// - draw_stage_breadcrumb : lineas 1386-1483
|
||||
// - drill_into : lineas 2898-2919
|
||||
|
||||
#include "viz/data_table_drill.h"
|
||||
#include "core/data_table_types.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// make_drill_filter: crea un Filter Op::Eq para col_idx con el valor dado.
|
||||
// ---------------------------------------------------------------------------
|
||||
Filter make_drill_filter(int col_idx, const std::string& value) {
|
||||
Filter f;
|
||||
f.col = col_idx;
|
||||
f.op = Op::Eq;
|
||||
f.value = value;
|
||||
return f;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// apply_drill_step: inserta step.added en el stage en step.target_stage y
|
||||
// actualiza st.active_stage. Retorna true si el step se aplico correctamente.
|
||||
// ---------------------------------------------------------------------------
|
||||
bool apply_drill_step(State& st, const DrillStep& step) {
|
||||
if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false;
|
||||
Stage& s = st.stages[step.target_stage];
|
||||
int pos = step.filter_pos;
|
||||
if (pos < 0 || pos > (int)s.filters.size()) return false;
|
||||
s.filters.insert(s.filters.begin() + pos, step.added);
|
||||
st.active_stage = step.target_stage;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// undo_drill_step: elimina el filter insertado por apply_drill_step y restaura
|
||||
// st.active_stage a step.prev_active_stage. Retorna true si se deshizo.
|
||||
// ---------------------------------------------------------------------------
|
||||
bool undo_drill_step(State& st, const DrillStep& step) {
|
||||
if (step.target_stage < 0 || step.target_stage >= (int)st.stages.size()) return false;
|
||||
Stage& s = st.stages[step.target_stage];
|
||||
int pos = step.filter_pos;
|
||||
if (pos < 0 || pos >= (int)s.filters.size()) return false;
|
||||
s.filters.erase(s.filters.begin() + pos);
|
||||
if (step.prev_active_stage >= 0 && step.prev_active_stage < (int)st.stages.size())
|
||||
st.active_stage = step.prev_active_stage;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// drill_up: retrocede st.active_stage en 1 si hay stages previos.
|
||||
// Retorna true si se pudo retroceder.
|
||||
// ---------------------------------------------------------------------------
|
||||
bool drill_up(State& st) {
|
||||
if (st.stages.empty()) return false;
|
||||
if (st.active_stage <= 0) return false;
|
||||
st.active_stage -= 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_stage_breadcrumb: barra de navegacion de drill con botones < > ^ y
|
||||
// selector de stages. Mutates st.drill_back/forward y st.active_stage.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_stage_breadcrumb(State& st) {
|
||||
st.ensure_stage0();
|
||||
|
||||
// Drill history back/forward (fase 10). Botones al inicio.
|
||||
{
|
||||
bool can_back = !st.drill_back.empty();
|
||||
ImGui::BeginDisabled(!can_back);
|
||||
if (ImGui::SmallButton("<##drill_back")) {
|
||||
DrillStep s = st.drill_back.back();
|
||||
st.drill_back.pop_back();
|
||||
if (undo_drill_step(st, s)) {
|
||||
st.drill_forward.push_back(s);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (can_back && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Drill back (%zu)", st.drill_back.size());
|
||||
ImGui::SameLine();
|
||||
bool can_fwd = !st.drill_forward.empty();
|
||||
ImGui::BeginDisabled(!can_fwd);
|
||||
if (ImGui::SmallButton(">##drill_fwd")) {
|
||||
DrillStep s = st.drill_forward.back();
|
||||
st.drill_forward.pop_back();
|
||||
if (apply_drill_step(st, s)) {
|
||||
st.drill_back.push_back(s);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (can_fwd && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Drill forward (%zu)", st.drill_forward.size());
|
||||
ImGui::SameLine();
|
||||
bool can_up = (st.active_stage > 0);
|
||||
ImGui::BeginDisabled(!can_up);
|
||||
if (ImGui::SmallButton("^##drill_up")) drill_up(st);
|
||||
ImGui::EndDisabled();
|
||||
if (can_up && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Drill up (stage previo, sin perder filters)");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("|");
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
for (int si = 0; si < (int)st.stages.size(); ++si) {
|
||||
if (si > 0) { ImGui::SameLine(); ImGui::TextDisabled(">"); ImGui::SameLine(); }
|
||||
|
||||
bool active = (si == st.active_stage);
|
||||
if (active) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 80, 140, 200, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 60, 120, 180, 240));
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32( 70, 70, 90, 200));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32( 90, 90, 120, 220));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32( 55, 55, 75, 220));
|
||||
}
|
||||
|
||||
char label[256];
|
||||
if (si == 0) {
|
||||
std::snprintf(label, sizeof(label), "Raw##stage%d", si);
|
||||
} else {
|
||||
const Stage& s = st.stages[si];
|
||||
std::string desc;
|
||||
for (size_t i = 0; i < s.breakouts.size() && i < 2; ++i) {
|
||||
if (i > 0) desc += ", ";
|
||||
desc += s.breakouts[i];
|
||||
}
|
||||
if (s.breakouts.size() > 2) desc += "...";
|
||||
if (desc.empty())
|
||||
std::snprintf(label, sizeof(label), "Stage %d##s%d", si, si);
|
||||
else
|
||||
std::snprintf(label, sizeof(label), "Stage %d: by %s##s%d",
|
||||
si, desc.c_str(), si);
|
||||
}
|
||||
if (ImGui::Button(label)) st.active_stage = si;
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
if (si > 0) {
|
||||
ImGui::SameLine();
|
||||
char xlbl[32];
|
||||
std::snprintf(xlbl, sizeof(xlbl), "x##rm_s%d", si);
|
||||
if (ImGui::SmallButton(xlbl)) {
|
||||
// borra ese stage y sucesores
|
||||
while ((int)st.stages.size() > si) st.stages.pop_back();
|
||||
if (st.active_stage >= (int)st.stages.size())
|
||||
st.active_stage = (int)st.stages.size() - 1;
|
||||
if (st.active_stage < 0) st.active_stage = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(">");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Stage##add_stage")) {
|
||||
st.stages.push_back(Stage{});
|
||||
st.active_stage = (int)st.stages.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// drill_into: API publica. Anade un filter Op::Eq sobre col_name=value al
|
||||
// stage (from_stage - 1) y cambia st.active_stage a ese stage previo.
|
||||
// Graba el step en st.drill_back y limpia st.drill_forward (rama nueva).
|
||||
// ---------------------------------------------------------------------------
|
||||
void drill_into(State& st, int from_stage,
|
||||
const std::string& col_name, const std::string& value,
|
||||
const std::vector<std::string>& prev_input_headers)
|
||||
{
|
||||
if (from_stage <= 0 || from_stage >= (int)st.stages.size()) return;
|
||||
int target = from_stage - 1;
|
||||
int ci = -1;
|
||||
for (size_t i = 0; i < prev_input_headers.size(); ++i) {
|
||||
if (prev_input_headers[i] == col_name) { ci = (int)i; break; }
|
||||
}
|
||||
if (ci < 0) return;
|
||||
|
||||
// Fase 10: graba step en drill_back, limpia forward (rama nueva).
|
||||
DrillStep step;
|
||||
step.target_stage = target;
|
||||
step.filter_pos = (int)st.stages[target].filters.size();
|
||||
step.prev_active_stage = st.active_stage;
|
||||
step.added = make_drill_filter(ci, value);
|
||||
apply_drill_step(st, step);
|
||||
st.drill_back.push_back(step);
|
||||
st.drill_forward.clear();
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
// data_table_drill — drill-down stack + breadcrumb de stages.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Responsabilidad:
|
||||
// - make_drill_filter: construye un Filter Op::Eq sobre un col_idx.
|
||||
// - apply_drill_step / undo_drill_step: manipula el stack drill_back/forward.
|
||||
// - drill_up: retrocede un stage.
|
||||
// - drill_into: API publica que anade un filter al stage previo y cambia active.
|
||||
// - draw_stage_breadcrumb: UI de la barra de breadcrumb con botones < > ^.
|
||||
//
|
||||
// Rangos del fuente original:
|
||||
// - make_drill_filter : linea 700-706
|
||||
// - apply_drill_step : lineas 708-718
|
||||
// - undo_drill_step : lineas 720-730
|
||||
// - drill_up : lineas 731-745
|
||||
// - drill_into : lineas 2898-2919
|
||||
// - draw_stage_breadcrumb: lineas 1383-1488
|
||||
//
|
||||
// Dependencias: data_table_types.h (State, DrillStep, Filter, Stage).
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers internos del drill stack (usados por drill_into y draw_stage_breadcrumb).
|
||||
// Se exponen en el header para que data_table_grid pueda llamarlos al
|
||||
// procesar el drill popup de celdas en stage > 0.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// make_drill_filter: crea un Filter Op::Eq para col_idx con el valor dado.
|
||||
Filter make_drill_filter(int col_idx, const std::string& value);
|
||||
|
||||
// apply_drill_step: inserta step.added en el stage en step.target_stage y
|
||||
// actualiza st.active_stage. Retorna true si el step se aplico correctamente.
|
||||
bool apply_drill_step(State& st, const DrillStep& step);
|
||||
|
||||
// undo_drill_step: elimina el filter insertado por apply_drill_step y restaura
|
||||
// st.active_stage a step.prev_active_stage. Retorna true si se deshizo.
|
||||
bool undo_drill_step(State& st, const DrillStep& step);
|
||||
|
||||
// drill_up: retrocede st.active_stage en 1 si hay stages previos.
|
||||
// Retorna true si se pudo retroceder.
|
||||
bool drill_up(State& st);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API publica: drill_into
|
||||
// Anade un filter Op::Eq sobre col_name=value al stage (from_stage - 1) y
|
||||
// cambia st.active_stage a from_stage - 1. Graba el step en st.drill_back
|
||||
// y limpia st.drill_forward (rama nueva del arbol de drill).
|
||||
//
|
||||
// prev_input_headers: headers del INPUT del stage from_stage (para traducir
|
||||
// col_name a col_idx en el stage previo).
|
||||
// ---------------------------------------------------------------------------
|
||||
void drill_into(State& st, int from_stage,
|
||||
const std::string& col_name, const std::string& value,
|
||||
const std::vector<std::string>& prev_input_headers);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UI: draw_stage_breadcrumb
|
||||
// Dibuja la barra de navegacion de drill con botones < (back), > (forward),
|
||||
// ^ (up) y el nombre del stage activo. Mutates st.drill_back/forward y
|
||||
// st.active_stage en respuesta a clicks.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_stage_breadcrumb(State& st);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: data_table_drill
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "void data_table::drill_into(State& st, int from_stage, const std::string& col_name, const std::string& value, const std::vector<std::string>& prev_input_headers)"
|
||||
description: "Drill-down stack + UI breadcrumb para la tabla TQL. Gestiona el arbol de navegacion de stages: drill_into anade un filtro Op::Eq al stage previo y avanza al nivel de detalle, draw_stage_breadcrumb dibuja los botones de navegacion (< back, > forward, ^ up) y el selector de stages activo. Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c)."
|
||||
tags: [viz, table, imgui, ui, drill-down, navigation, tql, cpp-tables]
|
||||
uses_functions:
|
||||
- data_table_cpp_viz
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/data_table_drill.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: st
|
||||
desc: "State mutable que contiene st.stages, st.active_stage, st.drill_back (undo stack), st.drill_forward (redo stack). Todos son mutados por las funciones de este modulo."
|
||||
- name: from_stage
|
||||
desc: "Stage desde el que se origina el drill (stage activo al hacer right-click en una celda de breakout). Debe ser > 0 para que haya stage previo donde anadir el filtro."
|
||||
- name: col_name
|
||||
desc: "Nombre de la columna breakout sobre la que se drilla (debe existir en prev_input_headers)."
|
||||
- name: value
|
||||
desc: "Valor de la celda seleccionada. Se aplica como Filter Op::Eq en el stage from_stage-1."
|
||||
- name: prev_input_headers
|
||||
desc: "Headers del INPUT del stage from_stage (= output del stage from_stage-1). Necesarios para traducir col_name a col_idx."
|
||||
output: "Void. Mutates st.stages[target].filters, st.active_stage, st.drill_back, st.drill_forward."
|
||||
---
|
||||
|
||||
## Documentacion
|
||||
|
||||
Sub-funcion que encapsula la logica de drill-down de la tabla TQL. El drill-down permite al usuario hacer right-click en una celda de una columna breakout (stage > 0) y "entrar" al detalle del grupo seleccionado anadiendo un filtro al stage previo.
|
||||
|
||||
### Funciones publicas
|
||||
|
||||
| Funcion | Uso |
|
||||
|---|---|
|
||||
| `make_drill_filter(col_idx, value)` | Helper: crea `Filter{col_idx, Op::Eq, value}`. |
|
||||
| `apply_drill_step(st, step)` | Inserta `step.added` en `st.stages[step.target_stage].filters` y actualiza `st.active_stage`. |
|
||||
| `undo_drill_step(st, step)` | Invierte `apply_drill_step`: elimina el filtro y restaura `st.active_stage`. |
|
||||
| `drill_up(st)` | Decrementa `st.active_stage` en 1 (sin crear entry en el undo stack). |
|
||||
| `drill_into(st, from, col, val, hdrs)` | API publica: compone make_drill_filter + apply_drill_step + graba en drill_back + limpia drill_forward. |
|
||||
| `draw_stage_breadcrumb(st)` | UI: botones < > ^ + combo de stages. |
|
||||
|
||||
### Invariante del stack
|
||||
|
||||
- `st.drill_back`: historial de drill steps (undo). Cada `drill_into` agrega al final.
|
||||
- `st.drill_forward`: pasos deshecho (redo). Se limpia en cada `drill_into` nueva.
|
||||
- `drill_up` NO agrega al stack — es un atajo que no se puede rehacer.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// En el cell popup de una celda de breakout col (stage > 0):
|
||||
if (ImGui::MenuItem(lbl)) {
|
||||
data_table::drill_into(st, active_stage,
|
||||
cur_headers[col], cell_value,
|
||||
input_headers_active);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
|
||||
// Breadcrumb al inicio del area de chrome:
|
||||
if (chrome_visible) {
|
||||
data_table::draw_stage_breadcrumb(st);
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
`drill_into` se llama desde el popup de celda en `data_table_grid_cpp_viz` cuando el usuario hace right-click en una columna breakout. `draw_stage_breadcrumb` se llama una vez por frame en el area de chrome antes de los chips.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `drill_into` requiere `from_stage > 0`. Si `from_stage <= 0` la funcion retorna sin hacer nada (no hay stage previo).
|
||||
- `col_name` debe existir en `prev_input_headers`; si no se encuentra, la funcion retorna sin efecto (el filter no se anade).
|
||||
- `apply_drill_step` y `undo_drill_step` modifican `st.stages[step.target_stage].filters` por posicion (`filter_pos`). Hay un riesgo de corrupcion si los filtros del stage cambian entre apply y undo por otra via. El design actual asume que el caller no muta los filtros del stage target fuera de este API.
|
||||
- `draw_stage_breadcrumb` dibuja los botones en linea horizontal; si el area es muy estrecha (< 120px) los botones solapan. El caller debe asegurar suficiente ancho o usar `ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ...)`.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
// data_table_grid — render del grid de datos: ImGui::BeginTable, headers con sort,
|
||||
// freeze cols, drag-drop de columnas, renderers declarativos (Badge/Progress/
|
||||
// Duration/Icon/Button/Dots/CategoricalChip/ColorScale), color rules per cell,
|
||||
// selection (Ctrl+C TSV), stats overlay, cell popup.
|
||||
//
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
// Rangos del fuente original:
|
||||
// - draw_cell_custom (declarative renderers): lineas 376-655
|
||||
// - Grid setup + header row + cell loop (stage 0): lineas 3436-3765
|
||||
// - Grid setup + cell loop (stage > 0): lineas 4040-4311
|
||||
// - Ctrl+C TSV copy: lineas 3725-3766
|
||||
// - Stats overlay en encabezados: lineas 4160-4226
|
||||
//
|
||||
// Dependencias: data_table_types.h, data_table_color_rules.h, imgui.h.
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "imgui.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_cell_custom — renderer declarativo por ColumnSpec.renderer.
|
||||
// Soporta: Text (fallback), Badge, Progress, Duration, Icon, Button, Dots,
|
||||
// CategoricalChip, ColorScale.
|
||||
// Emite TableEvent (ButtonClick, RowDoubleClick) en events_out si no-null.
|
||||
// Llamar dentro del contexto de un ImGui::BeginTable activo.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_cell_custom(const ColumnSpec& spec, const char* value,
|
||||
int row, int col,
|
||||
std::vector<TableEvent>* events_out);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// render_grid_stage0 — renderiza el grid para el path stage==0 (datos crudos
|
||||
// + derivados via Lua). Incluye: BeginTable, headers (sort click, drag-drop,
|
||||
// header menu via draw_header_menu), cell loop con draw_cell_custom,
|
||||
// apply_color_rules_for_cell, selection rect, Ctrl+C TSV copy.
|
||||
//
|
||||
// Parametros:
|
||||
// id — ID ImGui unico para el BeginTable.
|
||||
// st — State mutable (sort, selection, color_rules, col_order, ...).
|
||||
// cells — cells originales row-major [rows * orig_cols].
|
||||
// row_count — numero de filas originales.
|
||||
// orig_cols — numero de columnas originales (sin derived).
|
||||
// eff_cols — numero de columnas efectivas (orig + derived).
|
||||
// eff_headers — punteros a los nombres de columna efectivos (size eff_cols).
|
||||
// eff_types — tipos de columna efectivos (size eff_cols).
|
||||
// src_for_eff — mapeo eff col -> src col en orig (size eff_cols).
|
||||
// visible_rows — filas visibles tras filtrado (indices en [0, row_count)).
|
||||
// events_out — si no null, recibe TableEvent de esta frame.
|
||||
// ---------------------------------------------------------------------------
|
||||
void render_grid_stage0(const char* id,
|
||||
State& st,
|
||||
const char* const* cells,
|
||||
int row_count, int orig_cols, int eff_cols,
|
||||
const char* const* eff_headers,
|
||||
const ColumnType* eff_types,
|
||||
const int* src_for_eff,
|
||||
const std::vector<int>& visible_rows,
|
||||
const TableInput& main_t,
|
||||
std::vector<TableEvent>* events_out);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// render_grid_stage_n — renderiza el grid para el path stage > 0 (datos
|
||||
// materializados del compute_stage chain). Similar a stage0 pero opera sobre
|
||||
// cur_cells/cur_rows/cur_cols_n ya materializados.
|
||||
//
|
||||
// input_headers_active — headers del INPUT del stage activo (para drill popup).
|
||||
// n_breakouts — numero de breakout cols en el stage activo.
|
||||
// ---------------------------------------------------------------------------
|
||||
void render_grid_stage_n(const char* id,
|
||||
State& st,
|
||||
const char* const* cur_cells,
|
||||
int cur_rows, int cur_cols_n,
|
||||
const std::vector<std::string>& cur_headers,
|
||||
const std::vector<ColumnType>& cur_types,
|
||||
const std::vector<std::string>& input_headers_active,
|
||||
int n_breakouts,
|
||||
const TableInput& main_t,
|
||||
std::vector<TableEvent>* events_out);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: data_table_grid
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "void data_table::render_grid_stage0(const char* id, State& st, const char* const* cells, int row_count, int orig_cols, int eff_cols, const char* const* eff_headers, const ColumnType* eff_types, const int* src_for_eff, const std::vector<int>& visible_rows, const TableInput& main_t, std::vector<TableEvent>* events_out)"
|
||||
description: "Render del grid de datos de la tabla TQL: ImGui::BeginTable con freeze de cabecera, headers clicables (sort), drag-drop de columnas para reordenar, renderers declarativos por ColumnSpec (Badge/Progress/Duration/Icon/Button/Dots/CategoricalChip/ColorScale), aplicacion de ColorRules por celda, seleccion de rango (Ctrl+C TSV), stats overlay en encabezados. Dos entrypoints: render_grid_stage0 (datos crudos + derived Lua) y render_grid_stage_n (output materializado de compute_stage). Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c)."
|
||||
tags: [viz, table, imgui, ui, grid, cell-renderer, sorting, tql, cpp-tables]
|
||||
uses_functions:
|
||||
- data_table_color_rules_cpp_viz
|
||||
- data_table_cpp_viz
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/data_table_grid.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: id
|
||||
desc: "ID unico de ImGui para BeginTable (ej. '##my_table'). Debe ser unico por instancia en la ventana."
|
||||
- name: st
|
||||
desc: "State mutable. El grid lee st.col_order, st.col_visible, st.color_rules, st.sel_anchor/end, st.stages[active].sorts. Lo muta en respuesta a clicks, drag-drop, seleccion."
|
||||
- name: cells
|
||||
desc: "Puntero a cells originales row-major [rows * orig_cols]. Para stage0; stage_n recibe cur_cells ya materializados."
|
||||
- name: row_count / orig_cols / eff_cols
|
||||
desc: "Dimensiones de la tabla. orig_cols = columnas del input; eff_cols = orig_cols + derived cols del stage 0."
|
||||
- name: eff_headers / eff_types
|
||||
desc: "Headers y tipos efectivos (size eff_cols). Usados para headers del BeginTable y para render de celda."
|
||||
- name: src_for_eff
|
||||
desc: "Mapeo eff_col_idx -> src_col_idx en la tabla original. Necesario para leer cells[row * orig_cols + src]. Solo para stage0."
|
||||
- name: visible_rows
|
||||
desc: "Indices de filas visibles tras filtrado (output de compute_visible_rows). Solo para stage0."
|
||||
- name: main_t
|
||||
desc: "TableInput principal: proporciona column_specs para draw_cell_custom y los column_spec ids para eventos."
|
||||
- name: events_out
|
||||
desc: "Si no null, recibe TableEvent (ButtonClick, RowDoubleClick, RowRightClick) generados este frame."
|
||||
output: "Void. Mutates st (sort, selection, col_order). Appends to events_out si no null."
|
||||
---
|
||||
|
||||
## Documentacion
|
||||
|
||||
Sub-funcion que encapsula el render del grid de celdas de la tabla TQL. Es el bloque mas grande del refactor 0107c (~1300 LOC del fuente original).
|
||||
|
||||
### Renderers declarativos (draw_cell_custom)
|
||||
|
||||
| Renderer | Que muestra |
|
||||
|---|---|
|
||||
| `Text` | Selectable de texto, tooltip on-hover si `tooltip_on_hover=true` |
|
||||
| `Badge` | Pastilla de color (background del spec) con texto |
|
||||
| `Progress` | Barra de progreso ImGui [0..100] |
|
||||
| `Duration` | Formato "Xh Ym Zs" desde segundos enteros |
|
||||
| `Icon` | Glyph Tabler (usa `icons_tabler.h`) |
|
||||
| `Button` | Boton clickable; emite `TableEventKind::ButtonClick` con `action_id` |
|
||||
| `Dots` | Circulos coloreados por reglas de chips del ColumnSpec |
|
||||
| `CategoricalChip` | Pastilla con borde coloreado; color por palette hash |
|
||||
| `ColorScale` | Degradado de fondo segun valor numerico normalizado |
|
||||
|
||||
### Flujo render_grid_stage0
|
||||
|
||||
1. Setup `ImGuiTableFlags` (Borders + RowBg + Resizable + ScrollY).
|
||||
2. `BeginTable` → `TableSetupColumn` por cada col visible en `st.col_order`.
|
||||
3. `TableSetupScrollFreeze(0, 1)` para freeze de header.
|
||||
4. Header row: `Selectable` con sort-click (`apply_header_sort_click`), drag-drop source/target para reordenar columnas.
|
||||
5. Por cada fila en `visible_rows`: por cada col visible en `st.col_order`: `apply_color_rules_for_cell` → `draw_cell_custom`.
|
||||
6. Seleccion de rango: mouse drag con `sel_anchor/sel_end`; Ctrl+C genera TSV al portapapeles.
|
||||
7. `EndTable` + `PopStyleColor(3)`.
|
||||
|
||||
### Flujo render_grid_stage_n
|
||||
|
||||
Igual pero opera sobre `cur_cells` ya materializados del chain `compute_stage`. Ademas:
|
||||
- Stats overlay en headers (histograma/top-categories de `st.stats_cache`).
|
||||
- Drill popup: right-click en celda de breakout col ofrece "Drill into: col = val".
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// Dentro del render() principal, path stage == 0:
|
||||
if (st.display == data_table::ViewMode::Table && visible_cols > 0) {
|
||||
data_table::render_grid_stage0(id, st,
|
||||
cells_in, row_count_in, orig_cols, eff_cols,
|
||||
eff_headers.data(), eff_types.data(), src_for_eff.data(),
|
||||
visible_rows, main_t, events_out);
|
||||
}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Llamar desde el entrypoint thin `data_table::render()` tras calcular `visible_rows` (stage 0) o materializar el chain (stage > 0). No llamar directamente desde apps — la API publica es `data_table::render()`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `render_grid_stage0` y `render_grid_stage_n` deben llamarse dentro de un `ImGui::BeginChild` o contexto de ventana activo. No funcionan fuera de un frame ImGui.
|
||||
- La funcion hace `PushStyleColor` para Header/HeaderHovered/HeaderActive y debe hacer el correspondiente `PopStyleColor(3)`. No anidar pushes adicionales sin su pop.
|
||||
- `draw_cell_custom` con renderer `Button` emite eventos; si `events_out` es null se ignoran silenciosamente (back-compat).
|
||||
- El `thread_local` en el entrypoint original (main_hdr_ptrs, joinables_v) no se mueve a esta funcion — esos son responsabilidad del entrypoint thin.
|
||||
- Despues del split, `apply_color_rules_for_cell` vive en `data_table_color_rules_cpp_viz` — no re-implementar aqui.
|
||||
@@ -0,0 +1,429 @@
|
||||
// data_table_viz_panels — paneles de visualizacion lateral de la tabla TQL.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Funciones implementadas:
|
||||
// - draw_table_toggle (ex lineas 745-767)
|
||||
// - draw_extra_panel (ex lineas 771-856)
|
||||
// - draw_viz_config_popup (ex lineas 858-1021)
|
||||
// - draw_viz_selector (ex lineas 1034-1110)
|
||||
// - maybe_recompute_stats (ex lineas 1118-1145)
|
||||
|
||||
#include "viz/data_table_viz_panels.h"
|
||||
#include "data_table/data_table_internal.h"
|
||||
#include "viz/data_table_grid.h" // draw_cell_custom
|
||||
#include "core/data_table_types.h"
|
||||
#include "core/compute_column_stats.h"
|
||||
#include "core/tql_emit.h"
|
||||
#include "viz/viz_render.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_table_toggle
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_table_toggle(ViewMode& display, ViewMode& last_non_table,
|
||||
const char* id_suffix,
|
||||
State* st_opt)
|
||||
{
|
||||
bool is_table = (display == ViewMode::Table);
|
||||
char b[64];
|
||||
std::snprintf(b, sizeof(b), "%s##tbl_%s",
|
||||
is_table ? "Show chart" : "Show table", id_suffix);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(80, 140, 200, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(100, 160, 220, 240));
|
||||
if (ImGui::SmallButton(b)) {
|
||||
if (is_table) {
|
||||
ViewMode tgt = (last_non_table == ViewMode::Table)
|
||||
? ViewMode::Bar : last_non_table;
|
||||
display = tgt;
|
||||
if (st_opt && view_mode_needs_aggregation(tgt)) {
|
||||
auto_promote_aggregated(*st_opt);
|
||||
}
|
||||
} else {
|
||||
last_non_table = display;
|
||||
display = ViewMode::Table;
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_extra_panel
|
||||
// ---------------------------------------------------------------------------
|
||||
bool draw_extra_panel(State& st, VizPanel& p, int idx,
|
||||
const StageOutput& so,
|
||||
const std::vector<ColumnSpec>* col_specs)
|
||||
{
|
||||
bool close_req = false;
|
||||
char child_id[64]; std::snprintf(child_id, sizeof(child_id), "##extra_viz_%d", idx);
|
||||
ImGui::BeginChild(child_id, ImVec2(0, 320), true);
|
||||
|
||||
// Toolbar
|
||||
int n_modes = 0;
|
||||
const ViewMode* modes = all_view_modes(&n_modes);
|
||||
ImGui::TextDisabled("View:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(180);
|
||||
char combo_id[64]; std::snprintf(combo_id, sizeof(combo_id), "##ev_mode_%d", idx);
|
||||
if (ImGui::BeginCombo(combo_id, view_mode_label(p.display))) {
|
||||
for (int i = 0; i < n_modes; ++i) {
|
||||
bool sel = (modes[i] == p.display);
|
||||
if (ImGui::Selectable(view_mode_label(modes[i]), sel)) {
|
||||
p.display = modes[i];
|
||||
p.config.fit_request = true;
|
||||
}
|
||||
if (sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
char fit_id[32]; std::snprintf(fit_id, sizeof(fit_id), "Fit##ev_fit_%d", idx);
|
||||
if (ImGui::SmallButton(fit_id)) p.config.fit_request = true;
|
||||
ImGui::SameLine();
|
||||
char lock_id[32]; std::snprintf(lock_id, sizeof(lock_id), "%s##ev_lock_%d",
|
||||
p.config.locked ? "Locked" : "Lock", idx);
|
||||
if (p.config.locked) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 230));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(150, 40, 40, 240));
|
||||
}
|
||||
if (ImGui::SmallButton(lock_id)) p.config.locked = !p.config.locked;
|
||||
if (p.config.locked) ImGui::PopStyleColor(3);
|
||||
ImGui::SameLine();
|
||||
char close_id[32]; std::snprintf(close_id, sizeof(close_id), "X##ev_close_%d", idx);
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(120, 50, 50, 220));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(160, 70, 70, 240));
|
||||
if (ImGui::SmallButton(close_id)) close_req = true;
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
// Toggle Table <-> View per-panel
|
||||
char ts[32]; std::snprintf(ts, sizeof(ts), "ep%d", idx);
|
||||
draw_table_toggle(p.display, p.last_non_table, ts);
|
||||
|
||||
// Render: si Table -> mini table; else chart.
|
||||
if (p.display == ViewMode::Table) {
|
||||
ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
|
||||
ImGuiTableFlags_ScrollX;
|
||||
char tid[64]; std::snprintf(tid, sizeof(tid), "##ep_table_%d", idx);
|
||||
if (so.cols > 0 && ImGui::BeginTable(tid, so.cols, flags, ImVec2(0, 0))) {
|
||||
for (int c = 0; c < so.cols; ++c)
|
||||
ImGui::TableSetupColumn(so.headers[c].c_str());
|
||||
ImGui::TableHeadersRow();
|
||||
for (int r = 0; r < so.rows; ++r) {
|
||||
ImGui::TableNextRow();
|
||||
for (int c = 0; c < so.cols; ++c) {
|
||||
ImGui::TableSetColumnIndex(c);
|
||||
const char* s = so.cells[(size_t)r * so.cols + c];
|
||||
// Issue 0081-N: declarative renderer for extra panel mini-table.
|
||||
// events_out not propagated to mini-table (secondary render).
|
||||
bool custom_ep = false;
|
||||
if (col_specs && c < (int)col_specs->size()) {
|
||||
const ColumnSpec& cs = (*col_specs)[(size_t)c];
|
||||
if (cs.renderer != CellRenderer::Text) {
|
||||
draw_cell_custom(cs, s, r, c, nullptr);
|
||||
custom_ep = true;
|
||||
}
|
||||
}
|
||||
if (!custom_ep) ImGui::TextUnformatted(s ? s : "");
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
} else {
|
||||
viz::render(so, p.display, p.config, ImVec2(-1, -1));
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
(void)st;
|
||||
return close_req;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_viz_config_popup
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_viz_config_popup(State& st) {
|
||||
if (!ImGui::BeginPopup("##viz_cfg_popup")) return;
|
||||
ImGui::Text("Configure: %s", view_mode_label(st.display));
|
||||
ImGui::Separator();
|
||||
|
||||
auto cols = collect_active_col_info(st);
|
||||
std::vector<const char*> all_names;
|
||||
std::vector<const char*> num_names;
|
||||
std::vector<const char*> cat_names;
|
||||
for (auto& c : cols) {
|
||||
all_names.push_back(c.name.c_str());
|
||||
if (c.type == ColumnType::Int || c.type == ColumnType::Float)
|
||||
num_names.push_back(c.name.c_str());
|
||||
else
|
||||
cat_names.push_back(c.name.c_str());
|
||||
}
|
||||
|
||||
auto& vc = st.viz_config;
|
||||
ViewMode m = st.display;
|
||||
|
||||
auto combo_for_col = [&](const char* label, std::string& target,
|
||||
const std::vector<const char*>& options) {
|
||||
const char* preview = target.empty() ? "(auto)" : target.c_str();
|
||||
ImGui::SetNextItemWidth(220);
|
||||
if (ImGui::BeginCombo(label, preview)) {
|
||||
if (ImGui::Selectable("(auto)", target.empty())) target.clear();
|
||||
for (auto& o : options) {
|
||||
bool sel = (target == o);
|
||||
if (ImGui::Selectable(o, sel)) target = o;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
};
|
||||
|
||||
// X col: scatter, line, area, stairs, hist2d, bubble
|
||||
bool needs_x = (m == ViewMode::Scatter || m == ViewMode::Line ||
|
||||
m == ViewMode::Area || m == ViewMode::Stairs ||
|
||||
m == ViewMode::Histogram2D || m == ViewMode::Bubble);
|
||||
if (needs_x) combo_for_col("X column", vc.x_col, num_names);
|
||||
|
||||
// Y cols: most modes
|
||||
bool needs_y = (m != ViewMode::Pie && m != ViewMode::Donut && m != ViewMode::Funnel &&
|
||||
m != ViewMode::Candlestick);
|
||||
if (needs_y) {
|
||||
ImGui::Text("Y columns:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%d selected; empty = auto)", (int)vc.y_cols.size());
|
||||
ImGui::Indent();
|
||||
for (auto& nn : num_names) {
|
||||
std::string ns = nn;
|
||||
bool checked = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns) != vc.y_cols.end();
|
||||
if (ImGui::Checkbox(nn, &checked)) {
|
||||
if (checked) vc.y_cols.push_back(ns);
|
||||
else {
|
||||
auto it = std::find(vc.y_cols.begin(), vc.y_cols.end(), ns);
|
||||
if (it != vc.y_cols.end()) vc.y_cols.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Unindent();
|
||||
if (ImGui::SmallButton("Clear Y##clr_y")) vc.y_cols.clear();
|
||||
}
|
||||
|
||||
// Cat col: bar/pie/funnel/box/waterfall
|
||||
bool needs_cat = (m == ViewMode::Bar || m == ViewMode::Column ||
|
||||
m == ViewMode::GroupedBar || m == ViewMode::StackedBar ||
|
||||
m == ViewMode::Pie || m == ViewMode::Donut ||
|
||||
m == ViewMode::Funnel || m == ViewMode::BoxPlot ||
|
||||
m == ViewMode::Waterfall);
|
||||
if (needs_cat) {
|
||||
// Si el active stage YA esta agrupado (breakouts != empty), la categoria
|
||||
// del chart la dicta el breakout. Mostrar todas las cols del INPUT del
|
||||
// stage (= cols pre-agrupacion). Selecionar otra = reemplaza breakouts[0]
|
||||
// (re-agrupa).
|
||||
int as = st.active_stage;
|
||||
bool grouped = (as >= 0 && as < (int)st.stages.size() &&
|
||||
!st.stages[as].breakouts.empty());
|
||||
const auto& U = ui();
|
||||
if (grouped) {
|
||||
std::vector<const char*> input_cat_names;
|
||||
for (size_t i = 0; i < U.input_headers_active.size() &&
|
||||
i < U.input_types_active.size(); ++i) {
|
||||
ColumnType t = U.input_types_active[i];
|
||||
if (t == ColumnType::String || t == ColumnType::Date ||
|
||||
t == ColumnType::Bool || t == ColumnType::Json) {
|
||||
input_cat_names.push_back(U.input_headers_active[i].c_str());
|
||||
}
|
||||
}
|
||||
std::string cur_break = st.stages[as].breakouts[0];
|
||||
const char* preview = cur_break.empty() ? "(none)" : cur_break.c_str();
|
||||
ImGui::SetNextItemWidth(220);
|
||||
if (ImGui::BeginCombo("Category (breakout)", preview)) {
|
||||
for (auto& o : input_cat_names) {
|
||||
bool sel = (cur_break == o);
|
||||
if (ImGui::Selectable(o, sel)) {
|
||||
st.stages[as].breakouts[0] = o;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
} else {
|
||||
combo_for_col("Category", vc.cat_col, cat_names);
|
||||
}
|
||||
}
|
||||
|
||||
// Size col: bubble
|
||||
if (m == ViewMode::Bubble) combo_for_col("Size column", vc.size_col, num_names);
|
||||
|
||||
// Color
|
||||
ImGui::Separator();
|
||||
float col_f[4] = {
|
||||
((vc.primary_color) & 0xFF) / 255.0f,
|
||||
((vc.primary_color >> 8) & 0xFF) / 255.0f,
|
||||
((vc.primary_color >> 16) & 0xFF) / 255.0f,
|
||||
((vc.primary_color >> 24) & 0xFF) / 255.0f,
|
||||
};
|
||||
if (vc.primary_color == 0) { col_f[0]=col_f[1]=col_f[2]=1.0f; col_f[3]=1.0f; }
|
||||
if (ImGui::ColorEdit4("Primary color", col_f, ImGuiColorEditFlags_AlphaBar)) {
|
||||
unsigned int r2 = (unsigned int)(col_f[0] * 255);
|
||||
unsigned int g2 = (unsigned int)(col_f[1] * 255);
|
||||
unsigned int b2 = (unsigned int)(col_f[2] * 255);
|
||||
unsigned int a2 = (unsigned int)(col_f[3] * 255);
|
||||
vc.primary_color = (a2 << 24) | (b2 << 16) | (g2 << 8) | r2;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Auto##color")) vc.primary_color = 0;
|
||||
|
||||
// Hist bins
|
||||
if (m == ViewMode::Histogram || m == ViewMode::Histogram2D) {
|
||||
ImGui::SetNextItemWidth(120);
|
||||
int bins = vc.hist_bins;
|
||||
if (ImGui::InputInt("Bins (0=auto)", &bins)) {
|
||||
if (bins < 0) bins = 0;
|
||||
vc.hist_bins = bins;
|
||||
}
|
||||
}
|
||||
|
||||
// Pie radius
|
||||
if (m == ViewMode::Pie || m == ViewMode::Donut) {
|
||||
ImGui::SetNextItemWidth(120);
|
||||
float rad = vc.pie_radius;
|
||||
if (ImGui::SliderFloat("Radius (0=auto)", &rad, 0.0f, 0.5f, "%.2f")) {
|
||||
vc.pie_radius = rad;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggles
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Show legend", &vc.show_legend);
|
||||
if (m == ViewMode::Line || m == ViewMode::Area || m == ViewMode::Stairs) {
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Show markers", &vc.show_markers);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::SmallButton("Reset config")) {
|
||||
vc = ViewConfig{};
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Close")) ImGui::CloseCurrentPopup();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_viz_selector
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_viz_selector(State& st) {
|
||||
int n_modes = 0;
|
||||
const ViewMode* modes = all_view_modes(&n_modes);
|
||||
|
||||
// Right-align: reserve "View: [combo] [Fit] [Lock] [Config] [Ask AI] [+ Viz]"
|
||||
const float combo_w = 200.0f;
|
||||
const float total_w = combo_w + 50.0f + 280.0f;
|
||||
float right_edge = ImGui::GetWindowContentRegionMax().x;
|
||||
float target_x = right_edge - total_w;
|
||||
float min_x = ImGui::GetCursorPosX() + 20.0f; // do not overlap breadcrumb
|
||||
if (target_x < min_x) target_x = min_x;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(target_x);
|
||||
|
||||
ImGui::TextDisabled("View:");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(combo_w);
|
||||
if (ImGui::BeginCombo("##viz_mode", view_mode_label(st.display))) {
|
||||
for (int i = 0; i < n_modes; ++i) {
|
||||
bool sel = (modes[i] == st.display);
|
||||
if (ImGui::Selectable(view_mode_label(modes[i]), sel)) {
|
||||
ViewMode nm = modes[i];
|
||||
if (nm != st.display) {
|
||||
st.display = nm;
|
||||
if (view_mode_needs_aggregation(nm)) {
|
||||
auto_promote_aggregated(st);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sel) ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Fit##viz_fit")) {
|
||||
st.viz_config.fit_request = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool locked = st.viz_config.locked;
|
||||
if (locked) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 230));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(200, 80, 80, 240));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(150, 40, 40, 240));
|
||||
}
|
||||
if (ImGui::SmallButton(locked ? "Locked##viz_lock" : "Lock##viz_lock")) {
|
||||
st.viz_config.locked = !st.viz_config.locked;
|
||||
}
|
||||
if (locked) ImGui::PopStyleColor(3);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Config##viz_cfg")) {
|
||||
ImGui::OpenPopup("##viz_cfg_popup");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Ask AI##ask_open")) {
|
||||
auto& U2 = ui();
|
||||
U2.ask_ai.open = true;
|
||||
U2.ask_ai.busy = false;
|
||||
U2.ask_ai.error.clear();
|
||||
U2.ask_ai.status.clear();
|
||||
U2.ask_ai.response_code.clear();
|
||||
U2.ask_ai.response_raw.clear();
|
||||
U2.ask_ai.current_tql = tql::emit(st,
|
||||
std::vector<std::string>(),
|
||||
std::vector<ColumnType>());
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Viz##viz_add")) {
|
||||
VizPanel p;
|
||||
p.display = ViewMode::Bar;
|
||||
if (view_mode_needs_aggregation(p.display)) {
|
||||
auto_promote_aggregated(st);
|
||||
}
|
||||
st.extra_panels.push_back(p);
|
||||
}
|
||||
draw_viz_config_popup(st);
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// maybe_recompute_stats
|
||||
// ---------------------------------------------------------------------------
|
||||
void maybe_recompute_stats(State& st,
|
||||
const char* const* cells,
|
||||
int row_count, int orig_cols, int eff_cols,
|
||||
const std::vector<Filter>& active_filters,
|
||||
const std::vector<int>& visible_rows,
|
||||
const std::vector<int>& src_for_eff)
|
||||
{
|
||||
if (!st.stats_mode) return;
|
||||
size_t fh = filters_hash(active_filters);
|
||||
bool ds_changed = (cells != st.stats_last_cells || row_count != st.stats_last_rows ||
|
||||
eff_cols != st.stats_last_eff_cols ||
|
||||
(int)st.stats_cache.size() != eff_cols);
|
||||
bool fl_changed = (fh != st.stats_last_filter_h ||
|
||||
(int)visible_rows.size() != st.stats_last_visible);
|
||||
if (!ds_changed && !fl_changed) return;
|
||||
st.stats_cache.resize(eff_cols);
|
||||
const int* idx = visible_rows.empty() ? nullptr : visible_rows.data();
|
||||
int n = (int)visible_rows.size();
|
||||
for (int c = 0; c < eff_cols; ++c) {
|
||||
int src = src_for_eff[c];
|
||||
st.stats_cache[c] = compute_column_stats(cells, row_count, orig_cols, src,
|
||||
100000, idx, n);
|
||||
}
|
||||
st.stats_last_cells = cells;
|
||||
st.stats_last_rows = row_count;
|
||||
st.stats_last_eff_cols = eff_cols;
|
||||
st.stats_last_filter_h = fh;
|
||||
st.stats_last_visible = (int)visible_rows.size();
|
||||
}
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
// data_table_viz_panels — paneles de visualizacion lateral de la tabla TQL.
|
||||
// Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c).
|
||||
//
|
||||
// Responsabilidad:
|
||||
// - draw_table_toggle: boton de toggle entre modo tabla y ultimo modo viz.
|
||||
// - draw_extra_panel: render de un panel de viz adicional (VizPanel) con su
|
||||
// toolbar mini (titulo, pin/close, selector de chart).
|
||||
// - draw_viz_config_popup: popup de configuracion de visualizacion (columnas
|
||||
// x/y, modo, config del grafico activo).
|
||||
// - draw_viz_selector: selector de modo de viz (iconos/grid de ViewMode).
|
||||
// - maybe_recompute_stats: recalcula las estadisticas de columna si cambiaron
|
||||
// los datos visibles (para el stats overlay en headers y config popup).
|
||||
//
|
||||
// Rangos del fuente original:
|
||||
// - draw_table_toggle : lineas 1527-1552
|
||||
// - draw_extra_panel : lineas 1553-1638
|
||||
// - draw_viz_config_popup : lineas 1640-1815
|
||||
// - draw_viz_selector : lineas 1816-1892
|
||||
// - maybe_recompute_stats : lineas 2474-2502
|
||||
//
|
||||
// Dependencias: data_table_types.h, viz_render.h (viz::render), imgui.h.
|
||||
|
||||
#include "core/data_table_types.h"
|
||||
#include "imgui.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace data_table {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_table_toggle
|
||||
// Boton que cambia display entre ViewMode::Table y el ultimo modo viz no-tabla.
|
||||
// `id_suffix` diferencia multiples toggles en la misma ventana.
|
||||
// `st_ptr` si no null se usa para actualizar `st.display` directamente;
|
||||
// si null usa display/last_non_table pasados por ref.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_table_toggle(ViewMode& display, ViewMode& last_non_table,
|
||||
const char* id_suffix,
|
||||
State* st_ptr = nullptr);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_extra_panel
|
||||
// Dibuja un VizPanel adicional (child window con toolbar mini + chart).
|
||||
// Retorna true si el usuario cerro el panel (el caller debe borrar el entry).
|
||||
// ---------------------------------------------------------------------------
|
||||
bool draw_extra_panel(State& st, VizPanel& p, int idx,
|
||||
const StageOutput& so,
|
||||
const std::vector<ColumnSpec>* col_specs);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_viz_config_popup
|
||||
// Popup de configuracion de visualizacion: columnas x/y, modo de chart,
|
||||
// colores, histogramas bins, pie radius, etc. Mutates st.viz_config y
|
||||
// st.extra_panels.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_viz_config_popup(State& st);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_viz_selector
|
||||
// Selector de modo de visualizacion: grid de iconos con ViewMode disponibles.
|
||||
// Se abre via boton "Ask AI" o desde draw_viz_config_popup.
|
||||
// ---------------------------------------------------------------------------
|
||||
void draw_viz_selector(State& st);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// maybe_recompute_stats
|
||||
// Recalcula st.stats_cache si el snapshot de datos ha cambiado
|
||||
// (se detecta por hash de visible_rows + filtros). Solo recalcula si
|
||||
// st.stats_mode == true.
|
||||
// ---------------------------------------------------------------------------
|
||||
void maybe_recompute_stats(State& st,
|
||||
const char* const* cells,
|
||||
int row_count, int orig_cols, int eff_cols,
|
||||
const std::vector<Filter>& active_filters,
|
||||
const std::vector<int>& visible_rows,
|
||||
const std::vector<int>& src_for_eff);
|
||||
|
||||
} // namespace data_table
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: data_table_viz_panels
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "bool data_table::draw_extra_panel(State& st, VizPanel& p, int idx, const StageOutput& so, const std::vector<ColumnSpec>* col_specs)"
|
||||
description: "Paneles de visualizacion lateral de la tabla TQL: toggle tabla/chart, paneles extra independientes (histogramas, linea, scatter, value-counts), popup de configuracion de viz (cols x/y, modo, color, bins), selector de ViewMode via grid de iconos, y recalculo lazy de estadisticas de columna. Sub-funcion extraida de modules/data_table/data_table.cpp (issue 0107c)."
|
||||
tags: [viz, table, imgui, ui, charts, visualization, tql, cpp-tables, implot]
|
||||
uses_functions:
|
||||
- viz_render_cpp_viz
|
||||
- compute_column_stats_cpp_core
|
||||
- data_table_cpp_viz
|
||||
uses_types:
|
||||
- data_table_types_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui, implot]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/viz/data_table_viz_panels.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: st
|
||||
desc: "State mutable: st.display (modo actual), st.viz_config (config de viz), st.extra_panels (paneles adicionales), st.stats_cache/stats_mode (estadisticas)."
|
||||
- name: p
|
||||
desc: "VizPanel a renderizar: contiene su propio ViewConfig, titulo, display mode. Mutado si el usuario cambia la config del panel."
|
||||
- name: idx
|
||||
desc: "Indice del panel en st.extra_panels. Usado como ID ImGui para unicidad."
|
||||
- name: so
|
||||
desc: "StageOutput del stage activo: headers, types, cells row-major. Input de viz::render."
|
||||
- name: col_specs
|
||||
desc: "ColumnSpecs del TableInput principal si no vacias; null si no hay specs declarativas."
|
||||
output: "Bool para draw_extra_panel: true si el usuario cerro el panel (caller debe hacer erase de st.extra_panels[idx]). Void para el resto."
|
||||
---
|
||||
|
||||
## Documentacion
|
||||
|
||||
Sub-funcion que encapsula los paneles de visualizacion lateral de la tabla TQL. Permite al usuario ver graficos (bar, line, scatter, pie, etc.) en paralelo a la tabla de datos, con configuracion interactiva.
|
||||
|
||||
### Funciones publicas
|
||||
|
||||
| Funcion | Que hace |
|
||||
|---|---|
|
||||
| `draw_table_toggle(display, last_non_table, id, st_ptr)` | Boton toggle "Table / Chart". Guarda el ultimo modo viz en `last_non_table`. |
|
||||
| `draw_extra_panel(st, p, idx, so, col_specs)` | Child window con header/footer mini: titulo, pin, close. Llama `viz::render`. Retorna true si cerrado. |
|
||||
| `draw_viz_config_popup(st)` | Popup tabbado: config de viz principal + config de cada panel extra. Permite cambiar columnas x/y, modo, color, bins, anadir panel. |
|
||||
| `draw_viz_selector(st)` | Grid de iconos ViewMode; seleccion cambia `st.display`. Incluye boton "Ask AI" -> abre `data_table_ai_panel`. |
|
||||
| `maybe_recompute_stats(st, cells, ...)` | Recalcula stats lazy si `visible_rows` cambio. Solo cuando `st.stats_mode == true`. |
|
||||
|
||||
### draw_extra_panel: ciclo de vida de un VizPanel
|
||||
|
||||
1. El usuario abre un panel desde `draw_viz_selector` (boton "+").
|
||||
2. Se agrega un `VizPanel` a `st.extra_panels` con config por defecto.
|
||||
3. Cada frame: `draw_extra_panel(st, p, i, so, specs)`.
|
||||
4. Si retorna `true`: `st.extra_panels.erase(begin + i)`.
|
||||
|
||||
### maybe_recompute_stats: politica de cache
|
||||
|
||||
Compara un hash de `visible_rows` (FNV-1a sobre el vector de indices) contra `st.stats_last_visible_hash`. Si difiere, llama `compute_column_stats_cpp_core` por cada columna efectiva y actualiza `st.stats_cache`. Costo O(cols * visible_rows) — llamar solo cuando stats_mode este activo.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
// Llamado desde render() tras el grid (path stage 0):
|
||||
if (st.display != data_table::ViewMode::Table) {
|
||||
data_table::draw_table_toggle(st.display, U.last_non_table_main, "main", &st);
|
||||
}
|
||||
|
||||
// Paneles extra cuando NO estamos en modo tabla:
|
||||
if (st.display != data_table::ViewMode::Table && !st.extra_panels.empty()) {
|
||||
int close_idx = -1;
|
||||
for (int i = 0; i < (int)st.extra_panels.size(); ++i) {
|
||||
if (data_table::draw_extra_panel(st, st.extra_panels[i], i, so, &main_t.column_specs))
|
||||
close_idx = i;
|
||||
}
|
||||
if (close_idx >= 0) st.extra_panels.erase(st.extra_panels.begin() + close_idx);
|
||||
}
|
||||
|
||||
// Config popup (activado por boton en draw_viz_selector):
|
||||
data_table::draw_viz_config_popup(st);
|
||||
data_table::draw_viz_selector(st);
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Llamar desde el entrypoint thin `data_table::render()` despues del grid y antes de los modales. No llamar directamente desde apps — la API publica es siempre `data_table::render()`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `draw_extra_panel` abre un `ImGui::BeginChild` interno — no anidar dentro de otro child que ya recorte el area de pintado.
|
||||
- `draw_viz_selector` incluye la apertura del modal Ask AI (`st.ask_open = true`). El modal real lo dibuja `data_table_ai_panel_cpp_viz`. El order de calls importa: selector primero, luego el modal.
|
||||
- `maybe_recompute_stats` es potencialmente caro (O(visible_rows * cols)). Solo activar con `st.stats_mode = true` via boton "Show stats"; el boton vive en el area de chrome del render principal.
|
||||
- ImPlot context debe estar activo cuando se llama `viz::render` desde `draw_extra_panel`. Garantizado si el caller usa `fn::run_app` con ImPlot inicializado en `cfg`.
|
||||
@@ -5,32 +5,24 @@
|
||||
#include <imgui.h>
|
||||
#include <cstdio>
|
||||
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history, int history_count,
|
||||
const char* format,
|
||||
const char* icon) {
|
||||
static void kpi_card_impl(const char* label, float value, float delta_percent,
|
||||
const float* history, int history_count,
|
||||
const char* format, const char* icon,
|
||||
bool fixed_y, float y_min, float y_max) {
|
||||
using namespace fn_tokens;
|
||||
|
||||
// Card container — surface bg, border, rounded, padding.
|
||||
// Mirrors Mantine <Paper withBorder shadow="xs" radius="md" p="md" /> usado
|
||||
// en @fn_library/kpi_card.tsx, adaptado a ImGui dark theme via tokens.
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::sm, spacing::sm));
|
||||
|
||||
// Unique child id por label para que multiples cards en la misma ventana
|
||||
// no colisionen.
|
||||
char child_id[96];
|
||||
std::snprintf(child_id, sizeof(child_id), "##kpi_%s", label);
|
||||
|
||||
float width = ImGui::GetContentRegionAvail().x;
|
||||
if (width < 1.0f) width = 1.0f;
|
||||
|
||||
// Altura fija (no AutoResizeY) para que:
|
||||
// (a) todas las cards de un grid queden alineadas visualmente,
|
||||
// (b) no haya recalculo de layout por card en cada resize.
|
||||
constexpr float card_height = 86.0f;
|
||||
ImGui::BeginChild(child_id, ImVec2(width, card_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
@@ -39,8 +31,6 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
const bool has_history = history != nullptr && history_count > 0;
|
||||
const bool has_delta = delta_percent != 0.0f;
|
||||
|
||||
// Layout de dos columnas: izquierda info (label, value, delta), derecha sparkline.
|
||||
// El sparkline se sitia verticalmente centrado a la derecha de la card.
|
||||
constexpr float spark_w = 100.0f;
|
||||
constexpr float spark_h = 36.0f;
|
||||
|
||||
@@ -50,7 +40,6 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
ImGui::BeginGroup();
|
||||
ImGui::PushItemWidth(info_w);
|
||||
|
||||
// Top row: optional icon + label, ambos en text_muted.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
if (icon && *icon) {
|
||||
ImGui::TextUnformatted(icon);
|
||||
@@ -59,14 +48,12 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Value — escala compacta 1.4x, proporcional a una card de 86px.
|
||||
ImGui::SetWindowFontScale(1.4f);
|
||||
char value_buf[64];
|
||||
std::snprintf(value_buf, sizeof(value_buf), format, value);
|
||||
ImGui::TextUnformatted(value_buf);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
// Delta / trend — SIEMPRE se reserva la linea aunque no haya tendencia.
|
||||
if (has_delta) {
|
||||
const bool positive = delta_percent >= 0.0f;
|
||||
const ImVec4 delta_color = positive ? colors::success : colors::error;
|
||||
@@ -80,7 +67,6 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
ImGui::TextUnformatted(delta_buf);
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
// Placeholder para preservar altura.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim);
|
||||
ImGui::TextUnformatted(TI_MINUS);
|
||||
ImGui::PopStyleColor();
|
||||
@@ -89,24 +75,23 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::EndGroup();
|
||||
|
||||
// Sparkline a la derecha, centrado verticalmente respecto a la card.
|
||||
if (has_history) {
|
||||
const ImVec4 spark_color = has_delta
|
||||
? (delta_percent >= 0.0f ? colors::success : colors::error)
|
||||
: colors::primary;
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// Centrar verticalmente: la card mide card_height, el sparkline spark_h.
|
||||
// Restamos el padding interno (spacing::sm) ya consumido al inicio.
|
||||
const float card_inner_h = card_height - 2.0f * spacing::sm;
|
||||
float y_offset = (card_inner_h - spark_h) * 0.5f;
|
||||
if (y_offset < 0.0f) y_offset = 0.0f;
|
||||
// Anclamos el sparkline al borde derecho.
|
||||
const float spark_x = inner_w - spark_w;
|
||||
ImGui::SetCursorPos(ImVec2(spacing::sm + spark_x,
|
||||
spacing::sm + y_offset));
|
||||
sparkline(label, history, history_count, spark_color, spark_w, spark_h);
|
||||
ImGui::SetCursorPos(ImVec2(spacing::sm + spark_x, spacing::sm + y_offset));
|
||||
if (fixed_y) {
|
||||
sparkline(label, history, history_count, spark_color,
|
||||
y_min, y_max, spark_w, spark_h);
|
||||
} else {
|
||||
sparkline(label, history, history_count, spark_color, spark_w, spark_h);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
@@ -114,3 +99,20 @@ void kpi_card(const char* label, float value, float delta_percent,
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history, int history_count,
|
||||
const char* format,
|
||||
const char* icon) {
|
||||
kpi_card_impl(label, value, delta_percent, history, history_count,
|
||||
format, icon, /*fixed_y=*/false, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history, int history_count,
|
||||
float y_min, float y_max,
|
||||
const char* format,
|
||||
const char* icon) {
|
||||
kpi_card_impl(label, value, delta_percent, history, history_count,
|
||||
format, icon, /*fixed_y=*/true, y_min, y_max);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,18 @@
|
||||
// - Optional icon (Tabler glyph) + label (small, muted) on top row
|
||||
// - Value (large font)
|
||||
// - Delta badge (green TI_TRENDING_UP / red TI_TRENDING_DOWN)
|
||||
// - Sparkline of history
|
||||
// - Sparkline of history (auto Y by default; fixed Y in overload v1.4)
|
||||
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history = nullptr, int history_count = 0,
|
||||
const char* format = "%.1f",
|
||||
const char* icon = nullptr);
|
||||
|
||||
// Fixed Y-scale variant (v1.4): el sparkline interno usa [y_min, y_max]
|
||||
// absolutos. Util para metricas con dominio conocido (CPU%/RAM% -> 0,100)
|
||||
// donde la comparacion visual entre cards exige misma escala.
|
||||
void kpi_card(const char* label, float value, float delta_percent,
|
||||
const float* history, int history_count,
|
||||
float y_min, float y_max,
|
||||
const char* format = "%.1f",
|
||||
const char* icon = nullptr);
|
||||
|
||||
@@ -3,11 +3,11 @@ name: kpi_card
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
purity: pure
|
||||
signature: "void kpi_card(const char* label, float value, float delta_percent, const float* history = nullptr, int history_count = 0, const char* format = \"%.1f\", const char* icon = nullptr)"
|
||||
description: "Card de KPI con icono opcional + label, valor grande, delta porcentual con TI_TRENDING_UP/DOWN y sparkline historico. Contenedor con surface bg, borde y radius via tokens (Mantine Paper equivalente)."
|
||||
tags: [imgui, kpi, card, dashboard, metrics, sparkline, tokens, tabler]
|
||||
tags: [imgui, kpi, card, dashboard, metrics, sparkline, tokens, tabler, cpp-dashboard-viz]
|
||||
uses_functions: ["sparkline_cpp_viz", "tokens_cpp_core"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -79,3 +79,7 @@ ImGui::Columns(1);
|
||||
- La linea de trend siempre se reserva (delta, sparkline o em dash placeholder en `text_dim`) para que un grid de KPIs quede alineado vertical.
|
||||
- ~~Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) y del em dash (`—` U+2014) requieren que la fuente ImGui tenga el rango de simbolos geometricos / puntuacion general cargado.~~ → Obsoleto en v1.3: ahora se usan glyphs Tabler que estan en el atlas mergeado por `icon_font_cpp_core`.
|
||||
- Colores: delta usa `fn_tokens::colors::{success, error}`, placeholder `TI_MINUS` usa `text_dim`, label + icono usan `text_muted`.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.4.0 (2026-05-18) — Overload con `y_min, y_max` que se propaga al sparkline interno. Cards de un grid con dominio fijo (CPU%/RAM% -> 0,100) ahora son visualmente comparables — outliers no aplastan la escala. Default sigue siendo auto-scale.
|
||||
|
||||
@@ -5,28 +5,48 @@
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
void draw_line(const char* title, const T* xs, const T* ys, int count, float height) {
|
||||
void draw_line(const char* title, const T* xs, const T* ys, int count,
|
||||
float height, bool auto_x, double x_lo_in, double x_hi_in,
|
||||
bool auto_y, double y_lo_in, double y_hi_in) {
|
||||
if (count <= 0) return;
|
||||
|
||||
T x_min = xs[0], x_max = xs[0];
|
||||
T y_min = ys[0], y_max = ys[0];
|
||||
for (int i = 1; i < count; i++) {
|
||||
if (xs[i] < x_min) x_min = xs[i];
|
||||
if (xs[i] > x_max) x_max = xs[i];
|
||||
if (ys[i] < y_min) y_min = ys[i];
|
||||
if (ys[i] > y_max) y_max = ys[i];
|
||||
double x_lo, x_hi;
|
||||
if (auto_x) {
|
||||
T x_min = xs[0], x_max = xs[0];
|
||||
for (int i = 1; i < count; i++) {
|
||||
if (xs[i] < x_min) x_min = xs[i];
|
||||
if (xs[i] > x_max) x_max = xs[i];
|
||||
}
|
||||
x_lo = static_cast<double>(x_min);
|
||||
x_hi = static_cast<double>(x_max);
|
||||
} else {
|
||||
x_lo = x_lo_in;
|
||||
x_hi = x_hi_in;
|
||||
}
|
||||
if (x_hi - x_lo < 1e-9) x_hi = x_lo + 1.0;
|
||||
|
||||
double y_lo, y_hi;
|
||||
if (auto_y) {
|
||||
T y_min = ys[0], y_max = ys[0];
|
||||
for (int i = 1; i < count; i++) {
|
||||
if (ys[i] < y_min) y_min = ys[i];
|
||||
if (ys[i] > y_max) y_max = ys[i];
|
||||
}
|
||||
double dy = static_cast<double>(y_max) - static_cast<double>(y_min);
|
||||
if (dy < 1e-9) dy = 1.0;
|
||||
y_lo = static_cast<double>(y_min) - dy * 0.05;
|
||||
y_hi = static_cast<double>(y_max) + dy * 0.05;
|
||||
} else {
|
||||
y_lo = y_lo_in;
|
||||
y_hi = y_hi_in;
|
||||
if (y_hi - y_lo < 1e-9) y_hi = y_lo + 1.0;
|
||||
}
|
||||
double dy = static_cast<double>(y_max) - static_cast<double>(y_min);
|
||||
if (dy < 1e-9) dy = 1.0;
|
||||
double y_lo = static_cast<double>(y_min) - dy * 0.05;
|
||||
double y_hi = static_cast<double>(y_max) + dy * 0.05;
|
||||
|
||||
const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f);
|
||||
|
||||
if (ImPlot::BeginPlot(title, plot_size, plot_static::kPlotFlags)) {
|
||||
ImPlot::SetupAxes(nullptr, nullptr, plot_static::kAxisFlags, plot_static::kAxisFlags);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, static_cast<double>(x_min),
|
||||
static_cast<double>(x_max), ImPlotCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, x_lo, x_hi, ImPlotCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, y_lo, y_hi, ImPlotCond_Always);
|
||||
ImPlot::PlotLine("##data", xs, ys, count);
|
||||
ImPlot::EndPlot();
|
||||
@@ -36,9 +56,41 @@ void draw_line(const char* title, const T* xs, const T* ys, int count, float hei
|
||||
} // namespace
|
||||
|
||||
void line_plot(const char* title, const float* xs, const float* ys, int count, float height) {
|
||||
draw_line<float>(title, xs, ys, count, height);
|
||||
draw_line<float>(title, xs, ys, count, height,
|
||||
/*auto_x=*/true, 0.0, 0.0,
|
||||
/*auto_y=*/true, 0.0, 0.0);
|
||||
}
|
||||
|
||||
void line_plot(const char* title, const double* xs, const double* ys, int count, float height) {
|
||||
draw_line<double>(title, xs, ys, count, height);
|
||||
draw_line<double>(title, xs, ys, count, height,
|
||||
/*auto_x=*/true, 0.0, 0.0,
|
||||
/*auto_y=*/true, 0.0, 0.0);
|
||||
}
|
||||
|
||||
void line_plot(const char* title, const float* xs, const float* ys, int count,
|
||||
float y_min, float y_max, float height) {
|
||||
draw_line<float>(title, xs, ys, count, height,
|
||||
/*auto_x=*/true, 0.0, 0.0,
|
||||
/*auto_y=*/false, (double)y_min, (double)y_max);
|
||||
}
|
||||
|
||||
void line_plot(const char* title, const double* xs, const double* ys, int count,
|
||||
double y_min, double y_max, float height) {
|
||||
draw_line<double>(title, xs, ys, count, height,
|
||||
/*auto_x=*/true, 0.0, 0.0,
|
||||
/*auto_y=*/false, y_min, y_max);
|
||||
}
|
||||
|
||||
void line_plot(const char* title, const float* xs, const float* ys, int count,
|
||||
float x_min, float x_max, float y_min, float y_max, float height) {
|
||||
draw_line<float>(title, xs, ys, count, height,
|
||||
/*auto_x=*/false, (double)x_min, (double)x_max,
|
||||
/*auto_y=*/false, (double)y_min, (double)y_max);
|
||||
}
|
||||
|
||||
void line_plot(const char* title, const double* xs, const double* ys, int count,
|
||||
double x_min, double x_max, double y_min, double y_max, float height) {
|
||||
draw_line<double>(title, xs, ys, count, height,
|
||||
/*auto_x=*/false, x_min, x_max,
|
||||
/*auto_y=*/false, y_min, y_max);
|
||||
}
|
||||
|
||||
@@ -8,3 +8,20 @@ void line_plot(const char* title, const float* xs, const float* ys, int count,
|
||||
float height = 200.0f);
|
||||
void line_plot(const char* title, const double* xs, const double* ys, int count,
|
||||
float height = 200.0f);
|
||||
|
||||
// Fixed Y-range overloads — pinea Y a [y_min, y_max] para series con dominio
|
||||
// conocido (ej. 0-100 en CPU%/RAM%). v1.2.
|
||||
void line_plot(const char* title, const float* xs, const float* ys, int count,
|
||||
float y_min, float y_max, float height = 200.0f);
|
||||
void line_plot(const char* title, const double* xs, const double* ys, int count,
|
||||
double y_min, double y_max, float height = 200.0f);
|
||||
|
||||
// Fixed XY-range overloads — pinea BOTH X y Y. Util para ventanas temporales
|
||||
// fijas (ej. ultimos 5 min de CPU%): xs en [0, WINDOW] y ys en [0, 100]. El
|
||||
// plot NO se aplasta al variar count. v1.3.
|
||||
void line_plot(const char* title, const float* xs, const float* ys, int count,
|
||||
float x_min, float x_max, float y_min, float y_max,
|
||||
float height = 200.0f);
|
||||
void line_plot(const char* title, const double* xs, const double* ys, int count,
|
||||
double x_min, double x_max, double y_min, double y_max,
|
||||
float height = 200.0f);
|
||||
|
||||
@@ -3,11 +3,11 @@ name: line_plot
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.1.0"
|
||||
version: "1.3.0"
|
||||
purity: pure
|
||||
signature: "void line_plot(const char* title, const float* xs, const float* ys, int count, float height = 200.0f)"
|
||||
description: "Line plot 2D con ImPlot, ejes pineados y altura explicita para no vibrar al redimensionar"
|
||||
tags: [implot, chart, visualization, gpu, line, locked-axes]
|
||||
tags: [implot, chart, visualization, gpu, line, locked-axes, cpp-dashboard-viz]
|
||||
uses_functions: ["plot_static_cpp_viz"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -45,3 +45,8 @@ Wrapper atomico sobre `ImPlot::PlotLine` configurado para visualizacion estatica
|
||||
- **Sin inputs, sin auto-fit** — ver `viz/plot_static.h`.
|
||||
|
||||
Soporta `float` y `double`.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.2.0 (2026-05-18) — Overloads `(..., y_min, y_max, height)` para series con dominio conocido (CPU%/RAM% -> 0,100). Mantiene los overloads de auto-fit historico intactos. Tipos float y double.
|
||||
- v1.3.0 (2026-05-18) — Overloads `(..., x_min, x_max, y_min, y_max, height)` que pinea AMBOS ejes. Util para ventanas temporales fijas (ej. ultimos 5 min): xs en [0, 300], el grafico NO se aplasta al variar count durante warmup.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "viz/sparkline.h"
|
||||
#include "imgui.h"
|
||||
|
||||
void sparkline(const char* id, const float* values, int count, ImVec4 color,
|
||||
float width, float height) {
|
||||
// Implementacion comun. Si auto_y=true, calcula min/max de values; si no,
|
||||
// usa [y_min, y_max] explicitos.
|
||||
static void sparkline_impl(const char* id, const float* values, int count,
|
||||
ImVec4 color, float width, float height,
|
||||
bool auto_y, float y_min, float y_max) {
|
||||
if (count <= 0) return;
|
||||
|
||||
ImGui::PushID(id);
|
||||
@@ -10,67 +13,89 @@ void sparkline(const char* id, const float* values, int count, ImVec4 color,
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
// Reserve inline space
|
||||
ImGui::Dummy(ImVec2(width, height));
|
||||
|
||||
// Find min/max for Y auto-scale
|
||||
float min_val = values[0];
|
||||
float max_val = values[0];
|
||||
for (int i = 1; i < count; i++) {
|
||||
if (values[i] < min_val) min_val = values[i];
|
||||
if (values[i] > max_val) max_val = values[i];
|
||||
if (auto_y) {
|
||||
y_min = values[0];
|
||||
y_max = values[0];
|
||||
for (int i = 1; i < count; i++) {
|
||||
if (values[i] < y_min) y_min = values[i];
|
||||
if (values[i] > y_max) y_max = values[i];
|
||||
}
|
||||
}
|
||||
float range = max_val - min_val;
|
||||
if (range < 1e-6f) range = 1.0f; // avoid division by zero for flat lines
|
||||
float range = y_max - y_min;
|
||||
if (range < 1e-6f) range = 1.0f;
|
||||
|
||||
auto y_at = [&](float v) {
|
||||
// Clamp visualmente al rango — para fixed Y sirve para que outliers
|
||||
// no salgan del card; para auto Y es no-op.
|
||||
if (v < y_min) v = y_min;
|
||||
if (v > y_max) v = y_max;
|
||||
return pos.y + height - ((v - y_min) / range) * height;
|
||||
};
|
||||
|
||||
// Fade gradient v1.2: alpha sube de older->newer.
|
||||
// El segmento [i, i+1] se pinta con alpha proporcional al endpoint derecho
|
||||
// (mas cercano a "ahora"). Hace efecto de rastro / trail.
|
||||
const int r_ = (int)(color.x * 255);
|
||||
const int g_ = (int)(color.y * 255);
|
||||
const int b_ = (int)(color.z * 255);
|
||||
|
||||
auto seg_t = [&](int i) {
|
||||
return (count > 1) ? (float)(i + 1) / (float)(count - 1) : 1.0f;
|
||||
};
|
||||
|
||||
// Fill area under curve (low alpha)
|
||||
if (count >= 2) {
|
||||
ImU32 fill_color = IM_COL32(
|
||||
(int)(color.x * 255),
|
||||
(int)(color.y * 255),
|
||||
(int)(color.z * 255),
|
||||
40);
|
||||
|
||||
// Build fill polygon: baseline bottom-left -> points -> baseline bottom-right
|
||||
// We use AddConvexPolyFilled workaround: draw as a series of triangles from baseline
|
||||
float x0 = pos.x;
|
||||
float y_base = pos.y + height;
|
||||
|
||||
for (int i = 0; i + 1 < count; i++) {
|
||||
float t = seg_t(i);
|
||||
int fill_a = (int)((0.10f + 0.70f * t) * 40.0f); // 4..28 alpha
|
||||
ImU32 fill_color = IM_COL32(r_, g_, b_, fill_a);
|
||||
float xa = x0 + ((float)i / (count - 1)) * width;
|
||||
float xb = x0 + ((float)(i + 1) / (count - 1)) * width;
|
||||
float ya = pos.y + height - ((values[i] - min_val) / range) * height;
|
||||
float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height;
|
||||
|
||||
float ya = y_at(values[i]);
|
||||
float yb = y_at(values[i + 1]);
|
||||
draw_list->AddQuadFilled(
|
||||
ImVec2(xa, y_base),
|
||||
ImVec2(xa, ya),
|
||||
ImVec2(xb, yb),
|
||||
ImVec2(xb, y_base),
|
||||
ImVec2(xa, y_base), ImVec2(xa, ya),
|
||||
ImVec2(xb, yb), ImVec2(xb, y_base),
|
||||
fill_color);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw polyline
|
||||
ImU32 line_color = IM_COL32(
|
||||
(int)(color.x * 255),
|
||||
(int)(color.y * 255),
|
||||
(int)(color.z * 255),
|
||||
(int)(color.w * 255));
|
||||
|
||||
for (int i = 0; i + 1 < count; i++) {
|
||||
float t = seg_t(i);
|
||||
float a = 0.20f + 0.80f * t;
|
||||
ImU32 line_color = IM_COL32(r_, g_, b_, (int)(color.w * a * 255));
|
||||
float xa = pos.x + ((float)i / (count - 1)) * width;
|
||||
float xb = pos.x + ((float)(i + 1) / (count - 1)) * width;
|
||||
float ya = pos.y + height - ((values[i] - min_val) / range) * height;
|
||||
float yb = pos.y + height - ((values[i + 1] - min_val) / range) * height;
|
||||
float ya = y_at(values[i]);
|
||||
float yb = y_at(values[i + 1]);
|
||||
draw_list->AddLine(ImVec2(xa, ya), ImVec2(xb, yb), line_color, 1.5f);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void sparkline(const char* id, const float* values, int count, ImVec4 color,
|
||||
float width, float height) {
|
||||
sparkline_impl(id, values, count, color, width, height,
|
||||
/*auto_y=*/true, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void sparkline(const char* id, const float* values, int count,
|
||||
float width, float height) {
|
||||
// Default color: soft green
|
||||
sparkline(id, values, count, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), width, height);
|
||||
}
|
||||
|
||||
void sparkline(const char* id, const float* values, int count, ImVec4 color,
|
||||
float y_min, float y_max, float width, float height) {
|
||||
sparkline_impl(id, values, count, color, width, height,
|
||||
/*auto_y=*/false, y_min, y_max);
|
||||
}
|
||||
|
||||
void sparkline(const char* id, const float* values, int count,
|
||||
float y_min, float y_max, float width, float height) {
|
||||
sparkline(id, values, count, ImVec4(0.35f, 0.85f, 0.45f, 1.0f),
|
||||
y_min, y_max, width, height);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,23 @@
|
||||
#include "imgui.h"
|
||||
|
||||
// Renders a mini inline line chart for use in tables, headers and KPI cards.
|
||||
// Auto-scales Y to the min/max of values.
|
||||
// Uses PushID/PopID with id for uniqueness inside tables.
|
||||
//
|
||||
// Auto Y-scale variants:
|
||||
void sparkline(const char* id, const float* values, int count,
|
||||
float width = 100.0f, float height = 20.0f);
|
||||
|
||||
void sparkline(const char* id, const float* values, int count, ImVec4 color,
|
||||
float width = 100.0f, float height = 20.0f);
|
||||
|
||||
// Fixed Y-scale variants — clamp the polyline to [y_min, y_max] so cards in a
|
||||
// grid stay visually comparable (ej. CPU% / RAM%: pasar 0,100). v1.1.
|
||||
// width y height son explicitos (sin default) para que el compilador no haga
|
||||
// match contra los overloads sin y_min/y_max.
|
||||
void sparkline(const char* id, const float* values, int count,
|
||||
float y_min, float y_max,
|
||||
float width, float height);
|
||||
|
||||
void sparkline(const char* id, const float* values, int count, ImVec4 color,
|
||||
float y_min, float y_max,
|
||||
float width, float height);
|
||||
|
||||
@@ -3,11 +3,11 @@ name: sparkline
|
||||
kind: component
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "1.0.0"
|
||||
version: "1.2.0"
|
||||
purity: pure
|
||||
signature: "void sparkline(const char* id, const float* values, int count, float width = 100.0f, float height = 20.0f)"
|
||||
description: "Renderiza un mini grafico de lineas inline para uso en tablas, headers y KPI cards"
|
||||
tags: [imgui, visualization, sparkline, inline, dashboard]
|
||||
tags: [imgui, visualization, sparkline, inline, dashboard, data-table-renderers, cpp-dashboard-viz]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
@@ -69,3 +69,8 @@ sparkline("kpi_rev", data, count);
|
||||
- Si todos los valores son iguales (rango < 1e-6), la linea se dibuja en el centro verticalmente.
|
||||
- El grosor de linea es 1.5px para que sea legible a alturas de 16-24px.
|
||||
- `id` no se muestra visualmente; solo se pasa a `PushID` para que ImGui diferencie widgets con los mismos datos en la misma tabla.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-05-18) — Overloads con `y_min, y_max` explicitos. Cuando la metrica tiene dominio conocido (CPU%/RAM% -> 0,100) varias cards comparten escala y son comparables visualmente. Outliers se clampan al rango. Variant sin bounds preserva auto-scale historico.
|
||||
- v1.2.0 (2026-05-18) — Alpha fade gradient default: segmento i pinta con alpha lerp(0.2, 1.0, i/(count-1)). Older -> faded, newest -> bright. Efecto "trail" hacia el numero actual. Fill bajo curva tambien fade (alpha 4..28). Cambio visual sin cambio de API.
|
||||
|
||||
@@ -256,6 +256,16 @@ add_fn_test(test_tql_to_sql test_tql_to_sql.cpp
|
||||
add_fn_test(test_llm_anthropic test_llm_anthropic.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/llm_anthropic.cpp)
|
||||
|
||||
# --- Issue 0109a — parse_md_frontmatter: pure YAML-subset frontmatter parser --
|
||||
add_fn_test(test_parse_md_frontmatter test_parse_md_frontmatter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/parse_md_frontmatter.cpp)
|
||||
target_compile_definitions(test_parse_md_frontmatter PRIVATE
|
||||
FN_TEST_REPO_ROOT="${CMAKE_CURRENT_SOURCE_DIR}/../..")
|
||||
|
||||
# --- Issue 0109b — compute_ring_layout: geometria pura para skill_tree -------
|
||||
add_fn_test(test_compute_ring_layout test_compute_ring_layout.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/compute_ring_layout.cpp)
|
||||
|
||||
# --- Visual golden-image diff (issue 0048) ---------------------------------
|
||||
# El binario primitives_gallery se compila con --capture; el test compara los
|
||||
# PNGs generados con los goldens en cpp/tests/golden/. Si no hay goldens o el
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
// Tests para fn_ring::compute_ring_layout (cpp/functions/core/compute_ring_layout).
|
||||
// Pure: sin ImGui context, sin I/O.
|
||||
// Issue 0109b — skill_tree ring layout.
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/compute_ring_layout.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace fn_ring;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const LayoutConfig kDefault; // default 5 rings, 18 sectors
|
||||
|
||||
static LayoutOutput find_by_id(const std::vector<LayoutOutput>& out,
|
||||
const std::string& id) {
|
||||
for (auto& o : out) {
|
||||
if (o.id == id) return o;
|
||||
}
|
||||
// not found — return sentinel with ring=-99
|
||||
LayoutOutput bad;
|
||||
bad.id = id;
|
||||
bad.ring = -99;
|
||||
return bad;
|
||||
}
|
||||
|
||||
static float dist2(float x, float y) {
|
||||
return std::sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: empty input → empty output
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("empty_input_empty_output") {
|
||||
auto out = compute_ring_layout({}, kDefault);
|
||||
REQUIRE(out.empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: un solo nodo cae en centro de su banda radial + centro de su sector
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("single_node_centered_in_bin") {
|
||||
std::vector<LayoutInput> nodes = {{"n1", "completado", "core", 1.0f}};
|
||||
|
||||
// "completado" → ring 0, ring_radii default: [0, 150)
|
||||
// r_inner efectivo = 30 (ring0 avoidance), r_outer = 150
|
||||
// r_lo = 30 + 14 = 44, r_hi = 150 - 14 = 136, center = (44+136)/2 = 90
|
||||
|
||||
auto out = compute_ring_layout(nodes, kDefault);
|
||||
REQUIRE(out.size() == 1);
|
||||
|
||||
auto o = out[0];
|
||||
REQUIRE(o.ring == 0);
|
||||
REQUIRE(o.sector == 17); // "core" no esta en DomainOrder vacia → sector n_sectors-1 = 17
|
||||
|
||||
float r = dist2(o.x, o.y);
|
||||
// Centro de banda: [44, 136] -> 90.0
|
||||
REQUIRE(std::abs(r - 90.0f) < 0.01f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: dos nodos en mismo bin → radios distintos, uniformemente distribuidos
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("two_nodes_same_bin_radial_distribution") {
|
||||
std::vector<LayoutInput> nodes = {
|
||||
{"a", "completado", "alpha", 0.5f},
|
||||
{"b", "completado", "alpha", 0.5f},
|
||||
};
|
||||
|
||||
DomainOrder order = {"alpha"};
|
||||
auto out = compute_ring_layout(nodes, kDefault, {}, order);
|
||||
REQUIRE(out.size() == 2);
|
||||
|
||||
auto oa = find_by_id(out, "a");
|
||||
auto ob = find_by_id(out, "b");
|
||||
|
||||
REQUIRE(oa.ring == 0);
|
||||
REQUIRE(ob.ring == 0);
|
||||
REQUIRE(oa.sector == 0);
|
||||
REQUIRE(ob.sector == 0);
|
||||
|
||||
float ra = dist2(oa.x, oa.y);
|
||||
float rb = dist2(ob.x, ob.y);
|
||||
|
||||
// Radios deben ser distintos
|
||||
REQUIRE(std::abs(ra - rb) > 0.01f);
|
||||
|
||||
// Ambos deben estar en la banda [44, 136]
|
||||
float r_lo = 44.0f; // 30 + 14
|
||||
float r_hi = 136.0f; // 150 - 14
|
||||
REQUIRE(ra >= r_lo - 0.01f);
|
||||
REQUIRE(ra <= r_hi + 0.01f);
|
||||
REQUIRE(rb >= r_lo - 0.01f);
|
||||
REQUIRE(rb <= r_hi + 0.01f);
|
||||
|
||||
// N=2: r_0 = 44 + 0.5*(136-44)/2 = 44+23 = 67; r_1 = 44+1.5*46 = 113
|
||||
// (aprox, sin jitter porque bin tiene 2 nodos y capacidad radial = band/18 ~ 5)
|
||||
// No verificamos valores exactos porque el jitter angular puede activarse,
|
||||
// pero la diferencia de radios debe ser aprox (r_hi-r_lo)/N = 46
|
||||
float expected_step = (r_hi - r_lo) / 2.0f; // 46
|
||||
REQUIRE(std::abs(std::abs(ra - rb) - expected_step) < 0.5f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: default status map mapea correctamente
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("default_status_map") {
|
||||
std::vector<LayoutInput> nodes = {
|
||||
{"c1", "completado", "d", 0.0f},
|
||||
{"c2", "completed", "d", 0.0f},
|
||||
{"c3", "in-progress", "d", 0.0f},
|
||||
{"c4", "pendiente", "d", 0.0f},
|
||||
{"c5", "deferred", "d", 0.0f},
|
||||
{"c6", "locked", "d", 0.0f},
|
||||
{"c7", "unlocked", "d", 0.0f},
|
||||
{"c8", "bloqueado", "d", 0.0f},
|
||||
};
|
||||
|
||||
auto out = compute_ring_layout(nodes, kDefault);
|
||||
|
||||
REQUIRE(find_by_id(out, "c1").ring == 0);
|
||||
REQUIRE(find_by_id(out, "c2").ring == 0);
|
||||
REQUIRE(find_by_id(out, "c3").ring == 1);
|
||||
REQUIRE(find_by_id(out, "c4").ring == 3);
|
||||
REQUIRE(find_by_id(out, "c5").ring == 4);
|
||||
REQUIRE(find_by_id(out, "c6").ring == 3);
|
||||
REQUIRE(find_by_id(out, "c7").ring == 2);
|
||||
REQUIRE(find_by_id(out, "c8").ring == 4);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: status no mapeado → ring == -1, x/y == 0
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("unmapped_status_returns_ring_minus_one") {
|
||||
std::vector<LayoutInput> nodes = {{"x1", "unknown_status", "core", 0.5f}};
|
||||
|
||||
auto out = compute_ring_layout(nodes, kDefault);
|
||||
REQUIRE(out.size() == 1);
|
||||
|
||||
auto o = out[0];
|
||||
REQUIRE(o.ring == -1);
|
||||
REQUIRE(o.x == 0.0f);
|
||||
REQUIRE(o.y == 0.0f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: domain fuera del orden → sector n_sectors-1
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("domain_not_in_order_falls_back_to_last_sector") {
|
||||
DomainOrder order = {"alpha", "beta", "gamma"};
|
||||
std::vector<LayoutInput> nodes = {{"n1", "completado", "unknown_domain", 0.5f}};
|
||||
|
||||
LayoutConfig cfg = kDefault;
|
||||
cfg.n_sectors = 5;
|
||||
|
||||
auto out = compute_ring_layout(nodes, cfg, {}, order);
|
||||
REQUIRE(out.size() == 1);
|
||||
REQUIRE(out[0].sector == 4); // n_sectors-1 = 4
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: determinismo — dos llamadas identicas producen el mismo output
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("deterministic_repeated_call") {
|
||||
std::vector<LayoutInput> nodes = {
|
||||
{"0001", "completado", "core", 1.0f},
|
||||
{"0002", "pendiente", "infra", 0.5f},
|
||||
{"0003", "in-progress", "finance", 0.8f},
|
||||
{"0004", "deferred", "core", 0.1f},
|
||||
{"0005", "completado", "infra", 0.9f},
|
||||
};
|
||||
|
||||
auto out1 = compute_ring_layout(nodes, kDefault);
|
||||
auto out2 = compute_ring_layout(nodes, kDefault);
|
||||
|
||||
REQUIRE(out1.size() == out2.size());
|
||||
for (size_t i = 0; i < out1.size(); ++i) {
|
||||
REQUIRE(out1[i].id == out2[i].id);
|
||||
REQUIRE(out1[i].x == out2[i].x);
|
||||
REQUIRE(out1[i].y == out2[i].y);
|
||||
REQUIRE(out1[i].ring == out2[i].ring);
|
||||
REQUIRE(out1[i].sector == out2[i].sector);
|
||||
REQUIRE(out1[i].rank_in_bin == out2[i].rank_in_bin);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: ring 0 con radio interno 0 → nodos colocados con r >= 30
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("ring_zero_avoids_origin") {
|
||||
// ring_radii[0] == 0 por defecto
|
||||
std::vector<LayoutInput> nodes = {
|
||||
{"n1", "completado", "d1", 1.0f},
|
||||
{"n2", "completed", "d2", 0.9f},
|
||||
};
|
||||
|
||||
auto out = compute_ring_layout(nodes, kDefault);
|
||||
for (auto& o : out) {
|
||||
if (o.ring == 0) {
|
||||
float r = dist2(o.x, o.y);
|
||||
REQUIRE(r >= 30.0f - 0.01f); // kRing0InnerMin = 30
|
||||
REQUIRE(r > 0.5f); // definitivamente no en el origen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 9: sector wrap-around — sector 17 (ultimo) con 18 sectores
|
||||
// theta ≈ 2*PI*(17+0.5)/18 = 2*PI*17.5/18 ≈ 6.109 rad
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("sector_wrap_around_last_sector") {
|
||||
// Forzamos que el nodo caiga en sector 17 pasando domain_order sin "misc"
|
||||
std::vector<LayoutInput> nodes = {{"n1", "completado", "misc_domain", 1.0f}};
|
||||
|
||||
LayoutConfig cfg;
|
||||
cfg.n_sectors = 18;
|
||||
cfg.ring_radii = {0.0f, 150.0f, 280.0f, 450.0f, 650.0f, 850.0f};
|
||||
cfg.start_angle = 0.0f;
|
||||
cfg.bin_padding = 14.0f;
|
||||
|
||||
DomainOrder order; // vacia → "misc_domain" cae en sector n_sectors-1 = 17
|
||||
|
||||
auto out = compute_ring_layout(nodes, cfg, {}, order);
|
||||
REQUIRE(out.size() == 1);
|
||||
REQUIRE(out[0].sector == 17);
|
||||
|
||||
// theta = start_angle + (17 + 0.5) * (2*PI / 18) = 17.5 / 18 * 2*PI
|
||||
const float pi = 3.14159265358979323846f;
|
||||
const float theta = (17.5f / 18.0f) * 2.0f * pi; // ≈ 6.1087 rad
|
||||
|
||||
// Radio del nodo: ring 0, r_inner=30, r_outer=150, padding=14 → [44,136] → 90
|
||||
const float r = 90.0f;
|
||||
|
||||
float expected_x = r * std::cos(theta);
|
||||
float expected_y = r * std::sin(theta);
|
||||
|
||||
REQUIRE(std::abs(out[0].x - expected_x) < 0.05f);
|
||||
REQUIRE(std::abs(out[0].y - expected_y) < 0.05f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 10: golden snapshot — 30 nodos, mezcla de status/domain
|
||||
// Verifica primer y ultimo nodo con tolerancia 0.001
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("golden_snapshot_30_nodes") {
|
||||
// Input fijo: 30 nodos con mix de status/domain/recency
|
||||
const std::vector<LayoutInput> nodes = {
|
||||
{"0001", "completado", "core", 1.00f},
|
||||
{"0002", "completado", "infra", 0.95f},
|
||||
{"0003", "in-progress", "finance", 0.90f},
|
||||
{"0004", "pendiente", "core", 0.85f},
|
||||
{"0005", "deferred", "datascience", 0.80f},
|
||||
{"0006", "completado", "core", 0.75f},
|
||||
{"0007", "locked", "infra", 0.70f},
|
||||
{"0008", "unlocked", "finance", 0.65f},
|
||||
{"0009", "pendiente", "core", 0.60f},
|
||||
{"0010", "in-progress", "datascience", 0.55f},
|
||||
{"0011", "completado", "infra", 0.50f},
|
||||
{"0012", "bloqueado", "core", 0.45f},
|
||||
{"0013", "deferred", "finance", 0.40f},
|
||||
{"0014", "pendiente", "infra", 0.35f},
|
||||
{"0015", "completado", "datascience", 0.30f},
|
||||
{"0016", "unknown_status", "core", 0.25f}, // descartado
|
||||
{"0017", "in-progress", "infra", 0.20f},
|
||||
{"0018", "pendiente_unlocked", "finance", 0.15f},
|
||||
{"0019", "completed", "datascience", 0.10f},
|
||||
{"0020", "locked", "core", 0.05f},
|
||||
{"0021", "completado", "core", 1.00f},
|
||||
{"0022", "completado", "infra", 0.92f},
|
||||
{"0023", "pendiente", "finance", 0.88f},
|
||||
{"0024", "in-progress", "core", 0.84f},
|
||||
{"0025", "deferred", "infra", 0.80f},
|
||||
{"0026", "unlocked", "datascience", 0.76f},
|
||||
{"0027", "completado", "finance", 0.72f},
|
||||
{"0028", "locked", "datascience", 0.68f},
|
||||
{"0029", "bloqueado", "core", 0.64f},
|
||||
{"0030", "pendiente", "finance", 0.60f},
|
||||
};
|
||||
|
||||
DomainOrder order = {"core", "infra", "finance", "datascience"};
|
||||
LayoutConfig cfg;
|
||||
cfg.n_sectors = 18;
|
||||
cfg.ring_radii = {0.0f, 150.0f, 280.0f, 450.0f, 650.0f, 850.0f};
|
||||
cfg.start_angle = 0.0f;
|
||||
cfg.bin_padding = 14.0f;
|
||||
cfg.center_x = 0.0f;
|
||||
cfg.center_y = 0.0f;
|
||||
|
||||
auto out = compute_ring_layout(nodes, cfg, {}, order);
|
||||
REQUIRE(out.size() == 30);
|
||||
|
||||
// Nodo "0016" tiene status desconocido → debe ser descartado
|
||||
auto o16 = find_by_id(out, "0016");
|
||||
REQUIRE(o16.ring == -1);
|
||||
REQUIRE(o16.x == 0.0f);
|
||||
REQUIRE(o16.y == 0.0f);
|
||||
|
||||
// Verificar "0001": completado→ring0, core→sector0
|
||||
auto o1 = find_by_id(out, "0001");
|
||||
REQUIRE(o1.ring == 0);
|
||||
REQUIRE(o1.sector == 0);
|
||||
// theta = 0 + (0 + 0.5) * (2*PI / 18) = PI/18 ≈ 0.17453 rad
|
||||
// En el bin (ring=0, sector=0) hay "0001", "0006", "0021", "0024"
|
||||
// ordenados por (recency desc): recency 1.00, 0.75, 1.00 (0021), 0.84
|
||||
// -> "0001"(1.0), "0021"(1.0, id mayor), "0024"(0.84), "0006"(0.75)
|
||||
// rank "0001" = 0 si su id < "0021": "0001" < "0021" → rank 0
|
||||
REQUIRE(o1.rank_in_bin == 0);
|
||||
// El radio y angulo exactos dependen del jitter deterministico.
|
||||
// Verificamos que esta dentro de la banda del ring 0: [44, 136]
|
||||
float r1 = dist2(o1.x, o1.y);
|
||||
REQUIRE(r1 >= 44.0f - 0.01f);
|
||||
REQUIRE(r1 <= 136.0f + 0.01f);
|
||||
|
||||
// Verificar "0030": pendiente→ring3, finance→sector2
|
||||
auto o30 = find_by_id(out, "0030");
|
||||
REQUIRE(o30.ring == 3);
|
||||
REQUIRE(o30.sector == 2);
|
||||
// ring 3: [450, 650), r_lo = 450+14 = 464, r_hi = 650-14 = 636
|
||||
float r30 = dist2(o30.x, o30.y);
|
||||
REQUIRE(r30 >= 464.0f - 0.01f);
|
||||
REQUIRE(r30 <= 636.0f + 0.01f);
|
||||
|
||||
// El output es deterministico: dos llamadas dan resultado identico
|
||||
auto out2 = compute_ring_layout(nodes, cfg, {}, order);
|
||||
for (size_t i = 0; i < out.size(); ++i) {
|
||||
REQUIRE(out[i].x == out2[i].x);
|
||||
REQUIRE(out[i].y == out2[i].y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// Tests for parse_md_frontmatter (cpp/functions/core/parse_md_frontmatter).
|
||||
// Pure function — no ImGui context, no I/O (except the golden-run test which
|
||||
// reads dev/issues/ using FN_TEST_REPO_ROOT defined at compile time).
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/parse_md_frontmatter.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace fn_md;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string str(const YamlValue& v) {
|
||||
if (auto* s = std::get_if<std::string>(&v)) return *s;
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::vector<std::string> lst(const YamlValue& v) {
|
||||
if (auto* l = std::get_if<std::vector<std::string>>(&v)) return *l;
|
||||
return {};
|
||||
}
|
||||
|
||||
static bool has(const Frontmatter& fm, const std::string& key) {
|
||||
return fm.fields.count(key) > 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 1 — No frontmatter
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_no_frontmatter") {
|
||||
const std::string content = "# Just a markdown file\n\nSome body text.\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE_FALSE(fm.has_frontmatter);
|
||||
REQUIRE(fm.body == content);
|
||||
REQUIRE(fm.fields.empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 2 — Simple key:value
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_simple_key_value") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"title: Hello\n"
|
||||
"status: pending\n"
|
||||
"---\n"
|
||||
"Body here.\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("title")) == "Hello");
|
||||
REQUIRE(str(fm.fields.at("status")) == "pending");
|
||||
REQUIRE(fm.body == "Body here.\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 3 — Quoted string (double and single quotes)
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_quoted_strings") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"description: \"foo bar\"\n"
|
||||
"note: 'baz qux'\n"
|
||||
"id: \"0109\"\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("description")) == "foo bar");
|
||||
REQUIRE(str(fm.fields.at("note")) == "baz qux");
|
||||
REQUIRE(str(fm.fields.at("id")) == "0109");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 4 — Inline list
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_inline_list") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"tags: [meta, cpp, imgui]\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
const auto v = lst(fm.fields.at("tags"));
|
||||
REQUIRE(v.size() == 3);
|
||||
REQUIRE(v[0] == "meta");
|
||||
REQUIRE(v[1] == "cpp");
|
||||
REQUIRE(v[2] == "imgui");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 5 — Multiline list
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_multiline_list") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"domain:\n"
|
||||
" - meta\n"
|
||||
" - cpp-stack\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
const auto v = lst(fm.fields.at("domain"));
|
||||
REQUIRE(v.size() == 2);
|
||||
REQUIRE(v[0] == "meta");
|
||||
REQUIRE(v[1] == "cpp-stack");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 6 — Body after frontmatter
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_body_after_frontmatter") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"key: v\n"
|
||||
"---\n"
|
||||
"body text\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("key")) == "v");
|
||||
REQUIRE(fm.body == "body text\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 7 — Empty list
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_empty_inline_list") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"blocks: []\n"
|
||||
"depends: []\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(lst(fm.fields.at("blocks")).empty());
|
||||
REQUIRE(lst(fm.fields.at("depends")).empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 8 — Trailing comments stripped
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_strips_trailing_comment") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"key: value # this is a comment\n"
|
||||
"other: hello # another\n"
|
||||
"---\n";
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE(str(fm.fields.at("key")) == "value");
|
||||
REQUIRE(str(fm.fields.at("other")) == "hello");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 9 — Real issue sample (issue 0109)
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_CASE("parses_real_issue_0109") {
|
||||
const std::string content =
|
||||
"---\n"
|
||||
"id: \"0109\"\n"
|
||||
"title: \"App skill_tree: mapa interactivo de issues+flows en anillos concentricos por estado (roadmap)\"\n"
|
||||
"status: in-progress\n"
|
||||
"type: epic\n"
|
||||
"domain:\n"
|
||||
" - meta\n"
|
||||
" - cpp-stack\n"
|
||||
"scope: cross-stack\n"
|
||||
"priority: media\n"
|
||||
"depends: []\n"
|
||||
"blocks: []\n"
|
||||
"related:\n"
|
||||
" - \"0069\"\n"
|
||||
" - \"0085\"\n"
|
||||
"created: 2026-05-17\n"
|
||||
"updated: 2026-05-17\n"
|
||||
"tags:\n"
|
||||
" - skill-tree\n"
|
||||
" - roadmap\n"
|
||||
" - meta\n"
|
||||
" - cpp\n"
|
||||
" - imgui\n"
|
||||
" - gamification\n"
|
||||
"---\n"
|
||||
"\n"
|
||||
"# Body\n";
|
||||
|
||||
auto fm = parse_md_frontmatter(content);
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE_FALSE(fm.fields.empty());
|
||||
|
||||
REQUIRE(str(fm.fields.at("id")) == "0109");
|
||||
REQUIRE(str(fm.fields.at("status")) == "in-progress");
|
||||
|
||||
const auto domain = lst(fm.fields.at("domain"));
|
||||
REQUIRE(domain.size() == 2);
|
||||
REQUIRE(domain[0] == "meta");
|
||||
REQUIRE(domain[1] == "cpp-stack");
|
||||
|
||||
const auto tags = lst(fm.fields.at("tags"));
|
||||
REQUIRE(tags.size() == 6);
|
||||
REQUIRE(tags[0] == "skill-tree");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 10 — Golden run: parse ALL dev/issues/*.md and dev/flows/*.md
|
||||
//
|
||||
// Requires the compile-time definition FN_TEST_REPO_ROOT pointing to the
|
||||
// root of the fn_registry repo (set by CMakeLists.txt via
|
||||
// target_compile_definitions(... PRIVATE FN_TEST_REPO_ROOT="...") ).
|
||||
//
|
||||
// The test reads every .md file it finds, parses it, and verifies:
|
||||
// 1. No crash / no exception.
|
||||
// 2. If the file starts with `---`, has_frontmatter == true.
|
||||
// 3. fields is not empty.
|
||||
// 4. Cumulative parse_errors == 0.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#ifndef FN_TEST_REPO_ROOT
|
||||
#define FN_TEST_REPO_ROOT ""
|
||||
#endif
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
TEST_CASE("parses_real_issues_golden") {
|
||||
const std::string repo_root = FN_TEST_REPO_ROOT;
|
||||
if (repo_root.empty()) {
|
||||
WARN("FN_TEST_REPO_ROOT not set — skipping golden run");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> md_files;
|
||||
for (const auto& dir : {"dev/issues", "dev/flows"}) {
|
||||
const auto p = std::filesystem::path(repo_root) / dir;
|
||||
if (!std::filesystem::is_directory(p)) continue;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(p)) {
|
||||
if (entry.path().extension() == ".md")
|
||||
md_files.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE_FALSE(md_files.empty());
|
||||
|
||||
int parse_errors = 0;
|
||||
for (const auto& path : md_files) {
|
||||
std::ifstream f(path);
|
||||
REQUIRE(f.is_open());
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
const std::string content = ss.str();
|
||||
|
||||
Frontmatter fm;
|
||||
// Must not throw
|
||||
REQUIRE_NOTHROW(fm = parse_md_frontmatter(content));
|
||||
|
||||
if (content.rfind("---", 0) == 0) {
|
||||
// File starts with `---`: expect frontmatter was found
|
||||
INFO("File: " << path.string());
|
||||
REQUIRE(fm.has_frontmatter);
|
||||
REQUIRE_FALSE(fm.fields.empty());
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(parse_errors == 0);
|
||||
}
|
||||
Reference in New Issue
Block a user