feat: add C++ ImGui functions for core UI and visualization

Funciones C++/ImGui para dashboards (grid, panel, docking, sidebar, tabs),
visualizaciones (candlestick, gauge, histogram, pie, sparkline, heatmap,
scatter, line, bar, surface3d, kpi, table), grafos (force layout, renderer,
viewport, spatial hash, types) y utilidades (time series buffer, tracy zones,
memory/fps overlay, plot theme).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:10:18 +02:00
parent af9ebd1e0a
commit 0bdf35a461
66 changed files with 4236 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
#include "dashboard_grid.h"
#include <imgui.h>
#include <vector>
// Internal state stack to support nested grids.
namespace {
struct GridState {
int columns;
float spacing;
float col_width;
int counter; // number of dashboard_grid_next() calls so far
};
static std::vector<GridState> g_grid_stack;
} // namespace
void dashboard_grid_begin(int columns, float spacing) {
if (columns < 1) columns = 1;
float available = ImGui::GetContentRegionAvail().x;
float col_width = (available - spacing * static_cast<float>(columns - 1))
/ static_cast<float>(columns);
if (col_width < 1.0f) col_width = 1.0f;
g_grid_stack.push_back({columns, spacing, col_width, 0});
ImGui::BeginGroup();
ImGui::PushItemWidth(col_width);
}
void dashboard_grid_next() {
if (g_grid_stack.empty()) return;
GridState& s = g_grid_stack.back();
ImGui::PopItemWidth();
ImGui::EndGroup();
s.counter++;
if (s.counter % s.columns != 0) {
// Same row: advance horizontally.
ImGui::SameLine(0.0f, s.spacing);
}
// If counter % columns == 0 the next BeginGroup starts a new row automatically.
ImGui::BeginGroup();
ImGui::PushItemWidth(s.col_width);
}
void dashboard_grid_end() {
if (g_grid_stack.empty()) return;
ImGui::PopItemWidth();
ImGui::EndGroup();
g_grid_stack.pop_back();
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
// Dashboard grid — distributes child widgets in N columns.
// Usage:
// dashboard_grid_begin(3);
// // widget 1 (auto placed in col 0)
// dashboard_grid_next();
// // widget 2 (auto placed in col 1)
// dashboard_grid_next();
// // widget 3 (auto placed in col 2, wraps to next row)
// dashboard_grid_next();
// // widget 4 (col 0 of row 2)
// dashboard_grid_end();
void dashboard_grid_begin(int columns = 2, float spacing = 8.0f);
void dashboard_grid_next(); // advance to next cell
void dashboard_grid_end();
+88
View File
@@ -0,0 +1,88 @@
---
name: dashboard_grid
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void dashboard_grid_begin(int columns = 2, float spacing = 8.0f); void dashboard_grid_next(); void dashboard_grid_end()"
description: "Grid de N columnas para distribuir widgets de dashboard automaticamente con spacing uniforme entre columnas"
tags: [imgui, grid, layout, dashboard, responsive]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/dashboard_grid.cpp"
framework: imgui
params:
- name: columns
desc: "Numero de columnas del grid (minimo 1); el ancho disponible se divide uniformemente entre ellas"
- name: spacing
desc: "Espacio horizontal entre columnas en pixels"
output: "Layout de grid aplicado al contenido entre dashboard_grid_begin/end; cada celda recibe un ancho uniforme calculado a partir del espacio disponible"
---
# dashboard_grid
Divide el ancho disponible en N columnas con spacing uniforme y posiciona cada widget en su celda automaticamente. Soporta grids anidados mediante un stack interno de estado.
## Uso
```cpp
dashboard_grid_begin(3, 8.0f);
// Celda 0
dashboard_panel_begin("CPU");
ImGui::Text("%.1f %%", cpu_pct);
dashboard_panel_end();
dashboard_grid_next();
// Celda 1
dashboard_panel_begin("Memory");
ImGui::Text("%.0f MB", mem_mb);
dashboard_panel_end();
dashboard_grid_next();
// Celda 2
dashboard_panel_begin("Disk");
ImGui::Text("%.0f GB", disk_gb);
dashboard_panel_end();
dashboard_grid_end();
```
## Implementacion
### Calculo de ancho
```
col_width = (available_width - spacing * (columns - 1)) / columns
```
`available_width` se obtiene de `ImGui::GetContentRegionAvail().x` en el momento de `dashboard_grid_begin`.
### Mecanica de celdas
Cada celda es un `BeginGroup`/`EndGroup` con `PushItemWidth(col_width)` para que los widgets internos respeten el ancho de columna. Al llamar `dashboard_grid_next`:
1. Se cierra la celda actual (`PopItemWidth`, `EndGroup`).
2. Se incrementa el contador interno.
3. Si `counter % columns != 0` se emite `SameLine(0, spacing)` para continuar en la misma fila.
4. Si `counter % columns == 0` no se emite `SameLine`: ImGui pasa a la siguiente fila automaticamente.
5. Se abre la nueva celda (`BeginGroup`, `PushItemWidth`).
### Grids anidados
El estado (columnas, spacing, col_width, counter) se guarda en `g_grid_stack` (un `std::vector` de structs en un namespace anonimo). Cada llamada a `dashboard_grid_begin` hace `push_back` y `dashboard_grid_end` hace `pop_back`, permitiendo anidar grids sin conflicto.
## Notas
- Llamar `dashboard_grid_next()` entre cada par de widgets, **no** antes del primero ni despues del ultimo.
- El numero de `dashboard_grid_next()` puede ser mayor que `columns - 1`: el grid hace wrap automatico a la siguiente fila.
- Combina bien con `dashboard_panel_begin`/`dashboard_panel_end` para crear dashboards con paneles alineados en cuadricula.
- Si `columns <= 0` se fuerza a 1 para evitar division por cero.
+24
View File
@@ -0,0 +1,24 @@
#include "dashboard_panel.h"
#include <imgui.h>
bool dashboard_panel_begin(const char* title, float min_width, float min_height) {
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.12f, 0.12f, 0.15f, 1.0f));
ImVec2 size(min_width > 0.0f ? min_width : 0.0f,
min_height > 0.0f ? min_height : 0.0f);
ImGui::BeginChild(title, size, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY);
ImGui::TextUnformatted(title);
ImGui::Separator();
return true;
}
void dashboard_panel_end() {
ImGui::EndChild();
ImGui::PopStyleColor(1);
ImGui::PopStyleVar(2);
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// Dashboard panel — a styled child window with title bar.
// Usage:
// if (dashboard_panel_begin("Sales")) {
// line_plot("Revenue", xs, ys, N);
// }
// dashboard_panel_end(); // ALWAYS call, even if begin returned false
//
// Features: title bar with text, rounded corners, subtle border, auto-resize.
// min_width/min_height set minimum size constraints.
bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f);
void dashboard_panel_end();
+57
View File
@@ -0,0 +1,57 @@
---
name: dashboard_panel
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool dashboard_panel_begin(const char* title, float min_width = 200.0f, float min_height = 150.0f); void dashboard_panel_end()"
description: "Contenedor estilizado tipo panel para dashboards con titulo, bordes redondeados y tamaño minimo configurable"
tags: [imgui, panel, container, layout, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/dashboard_panel.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del panel, tambien sirve como ID de ImGui para distinguir multiples paneles"
- name: min_width
desc: "Ancho minimo del panel en pixels (0 = sin restriccion)"
- name: min_height
desc: "Alto minimo del panel en pixels (0 = sin restriccion)"
output: "true si el panel es visible y se debe renderizar contenido; llamar siempre dashboard_panel_end() independientemente del valor de retorno"
---
# dashboard_panel
Panel estilizado para dashboards ImGui. Envuelve un `BeginChild`/`EndChild` con estilos predefinidos: fondo oscuro (`#1F1F26`), bordes redondeados (5 px), borde visible y separador bajo el titulo.
## Uso
```cpp
if (dashboard_panel_begin("Revenue", 300.0f, 200.0f)) {
line_plot("Revenue", xs, ys, N);
}
dashboard_panel_end(); // siempre llamar
```
## Implementacion
- `PushStyleVar` aplica `ChildRounding = 5.0f` y `ChildBorderSize = 1.0f`
- `PushStyleColor` establece el fondo del child a `(0.12, 0.12, 0.15, 1.0)`
- `BeginChild` con `ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY`
- Titulo con `TextUnformatted` seguido de `Separator`
- `dashboard_panel_end` hace `EndChild`, `PopStyleColor(1)`, `PopStyleVar(2)`
## Notas
- El titulo actua como ID de ImGui: dos paneles con el mismo titulo en el mismo frame se comportan como uno solo. Usar `##` para diferenciar IDs si se repite el texto: `"Revenue##panel1"`.
- `AutoResizeY` hace que el panel crezca verticalmente con su contenido; `min_height` establece el piso.
- El patron begin/end es idomatico en ImGui: `end` debe llamarse siempre para hacer pop de los estilos, aunque `begin` retorne false.
+53
View File
@@ -0,0 +1,53 @@
#include "core/docking_layout.h"
#include "imgui_internal.h"
ImGuiID docking_layout(DockPreset preset) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGuiID dockspace_id = ImGui::DockSpaceOverViewport(0, viewport);
static bool initialized = false;
if (initialized) {
return dockspace_id;
}
initialized = true;
if (preset == DockPreset::Default) {
return dockspace_id;
}
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
if (preset == DockPreset::TwoColumns) {
ImGuiID right;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.6f, nullptr, &right);
} else if (preset == DockPreset::ThreeColumns) {
ImGuiID center, right;
ImGuiID left_node;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.33f, &left_node, &center);
ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.5f, nullptr, &right);
} else if (preset == DockPreset::SidebarLeft) {
ImGuiID main;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Left, 0.25f, nullptr, &main);
} else if (preset == DockPreset::SidebarRight) {
ImGuiID sidebar;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Right, 0.25f, &sidebar, nullptr);
} else if (preset == DockPreset::TopBottom) {
ImGuiID bottom;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.6f, nullptr, &bottom);
} else if (preset == DockPreset::Dashboard) {
ImGuiID top, bottom;
ImGui::DockBuilderSplitNode(dockspace_id, ImGuiDir_Up, 0.5f, &top, &bottom);
ImGui::DockBuilderSplitNode(top, ImGuiDir_Left, 0.5f, nullptr, nullptr);
ImGui::DockBuilderSplitNode(bottom, ImGuiDir_Left, 0.5f, nullptr, nullptr);
}
ImGui::DockBuilderFinish(dockspace_id);
return dockspace_id;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "imgui.h"
enum class DockPreset {
Default, // full dockspace, no preset splits
TwoColumns, // left 60% | right 40%
ThreeColumns, // left 33% | center 34% | right 33%
SidebarLeft, // sidebar 25% | main 75%
SidebarRight, // main 75% | sidebar 25%
TopBottom, // top 60% | bottom 40%
Dashboard // top-left | top-right | bottom-left | bottom-right (2x2 grid)
};
// Call once at the beginning of render_fn.
// Returns the dockspace ID (use with ImGui::SetNextWindowDockID if needed).
ImGuiID docking_layout(DockPreset preset = DockPreset::Default);
+64
View File
@@ -0,0 +1,64 @@
---
name: docking_layout
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "ImGuiID docking_layout(DockPreset preset = DockPreset::Default)"
description: "Configura un docking space con presets de layout predefinidos para dashboards"
tags: [imgui, docking, layout, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/docking_layout.cpp"
framework: imgui
params:
- name: preset
desc: "Layout predefinido a aplicar (Default, TwoColumns, ThreeColumns, SidebarLeft, SidebarRight, TopBottom, Dashboard)"
output: "ID del dockspace creado, usable con ImGui::SetNextWindowDockID"
---
# docking_layout
Configura un docking space fullscreen sobre el viewport principal con presets de layout predefinidos.
Usa `DockSpaceOverViewport` para crear el dockspace y, en el primer frame, aplica el preset con `DockBuilderSplitNode`. Llamar una vez al inicio de cada frame, antes de renderizar las ventanas hijas.
## Presets disponibles
| Preset | Layout |
|---|---|
| Default | Dockspace completo sin divisiones |
| TwoColumns | Izquierda 60% / Derecha 40% |
| ThreeColumns | Tres columnas iguales ~33% |
| SidebarLeft | Sidebar 25% / Main 75% |
| SidebarRight | Main 75% / Sidebar 25% |
| TopBottom | Arriba 60% / Abajo 40% |
| Dashboard | Grid 2x2 de cuatro paneles |
## Ejemplo
```cpp
void render_fn() {
ImGuiID dock = docking_layout(DockPreset::SidebarLeft);
ImGui::Begin("Filters");
// controles del sidebar
ImGui::End();
ImGui::Begin("Main");
// contenido principal
ImGui::End();
}
```
## Notas
Requiere que `ImGuiConfigFlags_DockingEnable` este activo en `ImGui::GetIO().ConfigFlags` (habilitado por `app_base.cpp`). El preset se aplica solo en el primer frame (static bool). `imgui_internal.h` es necesario para `DockBuilder*`.
+174
View File
@@ -0,0 +1,174 @@
#include "graph_spatial_hash.h"
#include <cmath>
#include <cstdlib>
#include <cstring>
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static inline int floor_div(float v, float cell) {
return static_cast<int>(std::floor(v / cell));
}
static inline float sq(float x) { return x * x; }
// ---------------------------------------------------------------------------
// Snapshot de posiciones (almacenado internamente en build)
// La interfaz del header no pasa xs/ys a los metodos de query, por lo que
// SpatialHash conserva copias internas para calcular distancias.
// ---------------------------------------------------------------------------
struct SpatialHashSnap {
float* xs = nullptr;
float* ys = nullptr;
float* sizes = nullptr;
int count = 0;
};
// Almacenamos el snapshot en memoria contigua al final del bloque de entries,
// para no añadir campos al header publico. Usamos un puntero opaco en un
// campo reservado. Como el header ya esta fijado, guardamos el snapshot como
// una variable estatica por instancia — aceptable para uso single-threaded
// tipico de ImGui. Para multi-instancia se puede usar un map.
//
// En la practica, la convencion del registry es que SpatialHash se crea una
// vez por frame de ImGui y se usa en el mismo hilo de render.
static SpatialHashSnap g_snap; // snapshot mas reciente
// ---------------------------------------------------------------------------
// SpatialHash
// ---------------------------------------------------------------------------
int SpatialHash::cell_hash(int cx, int cy) const {
unsigned int h = static_cast<unsigned int>(cx) * 73856093u
^ static_cast<unsigned int>(cy) * 19349663u;
return static_cast<int>(h % static_cast<unsigned int>(table_size));
}
SpatialHash::SpatialHash(float cell_size_, int table_size_)
: cell_size(cell_size_)
, table_size(table_size_)
, entry_count(0)
, entry_capacity(256)
{
buckets = static_cast<int*>(std::malloc(
static_cast<std::size_t>(table_size) * sizeof(int)));
entries = static_cast<int*>(std::malloc(
static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));
std::memset(buckets, -1,
static_cast<std::size_t>(table_size) * sizeof(int));
}
SpatialHash::~SpatialHash() {
std::free(buckets);
std::free(entries);
std::free(g_snap.xs);
std::free(g_snap.ys);
std::free(g_snap.sizes);
g_snap = {};
}
void SpatialHash::build(const float* xs, const float* ys, const float* sizes, int count) {
// --- Limpiar tabla ---
std::memset(buckets, -1,
static_cast<std::size_t>(table_size) * sizeof(int));
entry_count = 0;
// --- Snapshot de posiciones para queries ---
if (g_snap.count < count) {
std::free(g_snap.xs);
std::free(g_snap.ys);
std::free(g_snap.sizes);
g_snap.xs = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
g_snap.ys = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
g_snap.sizes = static_cast<float*>(std::malloc(static_cast<std::size_t>(count) * sizeof(float)));
}
std::memcpy(g_snap.xs, xs, static_cast<std::size_t>(count) * sizeof(float));
std::memcpy(g_snap.ys, ys, static_cast<std::size_t>(count) * sizeof(float));
std::memcpy(g_snap.sizes, sizes, static_cast<std::size_t>(count) * sizeof(float));
g_snap.count = count;
// --- Insertar nodos en la tabla hash ---
for (int i = 0; i < count; ++i) {
int cx = floor_div(xs[i], cell_size);
int cy = floor_div(ys[i], cell_size);
int bucket = cell_hash(cx, cy);
// Crecer entries si necesario
if (entry_count >= entry_capacity) {
entry_capacity *= 2;
entries = static_cast<int*>(std::realloc(
entries,
static_cast<std::size_t>(entry_capacity) * 2 * sizeof(int)));
}
// Insertar al frente de la cadena encadenada
int slot = entry_count++;
entries[slot * 2 + 0] = i; // node_index
entries[slot * 2 + 1] = buckets[bucket]; // next_in_chain
buckets[bucket] = slot;
}
}
int SpatialHash::query_nearest(float qx, float qy, float radius, float* out_dist) const {
int best_idx = -1;
float best_d = radius; // umbral: solo aceptamos dentro del radio
int qcx = floor_div(qx, cell_size);
int qcy = floor_div(qy, cell_size);
// Escanear vecindad 3x3 de celdas
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
int bucket = cell_hash(qcx + dx, qcy + dy);
int slot = buckets[bucket];
while (slot != -1) {
int ni = entries[slot * 2 + 0];
float ex = g_snap.xs[ni] - qx;
float ey = g_snap.ys[ni] - qy;
float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
if (dist < best_d) {
best_d = dist;
best_idx = ni;
}
slot = entries[slot * 2 + 1];
}
}
}
if (out_dist && best_idx != -1)
*out_dist = best_d;
return best_idx;
}
int SpatialHash::query_radius(float qx, float qy, float radius, int* out, int max_out) const {
int found = 0;
int qcx = floor_div(qx, cell_size);
int qcy = floor_div(qy, cell_size);
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
int bucket = cell_hash(qcx + dx, qcy + dy);
int slot = buckets[bucket];
while (slot != -1 && found < max_out) {
int ni = entries[slot * 2 + 0];
float ex = g_snap.xs[ni] - qx;
float ey = g_snap.ys[ni] - qy;
float dist = std::sqrt(sq(ex) + sq(ey)) - g_snap.sizes[ni];
if (dist <= radius)
out[found++] = ni;
slot = entries[slot * 2 + 1];
}
}
}
return found;
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
// SpatialHash — grid espacial para hit-testing de nodos en grafos.
// Permite buscar el nodo mas cercano a una posicion en O(1) amortizado.
struct SpatialHash {
float cell_size;
int table_size; // numero de buckets en la tabla hash
// Almacenamiento interno (flat arrays)
int* buckets; // table_size entradas, cada una apunta al primer entry de la cadena
int* entries; // pares [node_index, next]: entries[2*i] = node_idx, entries[2*i+1] = next
int entry_count;
int entry_capacity;
SpatialHash(float cell_size = 20.0f, int table_size = 4096);
~SpatialHash();
SpatialHash(const SpatialHash&) = delete;
SpatialHash& operator=(const SpatialHash&) = delete;
// Reconstruye la tabla desde arrays de posiciones y tamaños de nodos.
// xs[i], ys[i]: posicion del nodo i. sizes[i]: radio del nodo i.
void build(const float* xs, const float* ys, const float* sizes, int count);
// Busca el punto mas cercano dentro de (qx, qy) con radio de busqueda dado.
// Descuenta el tamaño del nodo (circulo): distancia efectiva = dist(centro) - size.
// Retorna el indice del nodo, o -1 si no hay ninguno.
// Si out_dist es no-nulo, escribe la distancia al nodo encontrado.
int query_nearest(float qx, float qy, float radius, float* out_dist = nullptr) const;
// Busca todos los puntos dentro del radio. Escribe indices en out[].
// Retorna el numero de nodos encontrados (hasta max_out).
int query_radius(float qx, float qy, float radius, int* out, int max_out) const;
private:
// Hash de coordenadas de celda enteras a indice de bucket.
int cell_hash(int cx, int cy) const;
};
+71
View File
@@ -0,0 +1,71 @@
---
name: graph_spatial_hash
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "SpatialHash(float cell_size, int table_size)"
description: "Spatial hash grid para busqueda O(1) de puntos por posicion, usado para hit-testing de nodos en grafos"
tags: [spatial, hash, acceleration, graph, query, hittest]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/graph_spatial_hash.cpp"
framework: imgui
params:
- name: cell_size
desc: "Tamaño de cada celda del grid espacial (debe ser >= radio maximo de nodos)"
- name: table_size
desc: "Numero de buckets en la tabla hash (potencia de 2 recomendada para distribucion uniforme)"
output: "Estructura de hash espacial lista para queries de puntos cercanos"
---
# graph_spatial_hash
Spatial hash grid para busqueda eficiente de nodos en grafos por posicion 2D. Util para hit-testing de nodos en editores de grafos basados en ImGui, donde se necesita saber que nodo esta bajo el cursor en cada frame.
## Uso
```cpp
SpatialHash sh(20.0f, 4096);
// Reconstruir cada frame (o cuando cambian posiciones)
sh.build(node_xs, node_ys, node_sizes, node_count);
// Hit-test: nodo mas cercano al cursor
float dist;
int hovered = sh.query_nearest(mouse_x, mouse_y, 30.0f, &dist);
// Seleccion por area: todos los nodos dentro de un radio
int results[256];
int count = sh.query_radius(center_x, center_y, radius, results, 256);
```
## Estructura interna
- `buckets[table_size]`: cada entrada contiene el indice del primer slot de la cadena, o -1 si el bucket esta vacio.
- `entries[2 * entry_capacity]`: pares `[node_index, next_in_chain]` almacenados como flat array. Se expande con `realloc` si el numero de nodos supera la capacidad inicial (256).
- El snapshot de posiciones (`xs`, `ys`, `sizes`) se copia internamente en `build()` para que `query_nearest` y `query_radius` puedan calcular distancias sin recibir los arrays como parametro.
## Hash function
```
hash(cx, cy) = (cx * 73856093 ^ cy * 19349663) % table_size
```
Donde `cx = floor(x / cell_size)`, `cy = floor(y / cell_size)`.
## Notas
- No es thread-safe: disenado para uso single-threaded en el hilo de render de ImGui.
- `build()` debe llamarse cada frame si las posiciones de los nodos cambian.
- `cell_size` debe ser >= al radio maximo de los nodos para que la vecindad 3x3 capture todos los candidatos posibles.
- La distancia efectiva al nodo se calcula como `dist(centro) - size`, de modo que el hit-test funciona correctamente para nodos de distintos tamaños.
- No implementa `operator=` ni copy constructor (deleted) para evitar doble-free de los arrays internos.
+99
View File
@@ -0,0 +1,99 @@
#include "core/memory_overlay.h"
#include "imgui.h"
#ifdef TRACY_ENABLE
#include "tracy/Tracy.hpp"
#endif
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#pragma comment(lib, "psapi.lib")
#else
#include <cstdio>
#endif
#include <cstring>
namespace {
struct MemStats {
long rss_kb = -1; // Resident Set Size
long peak_kb = -1; // Peak RSS
long vsize_kb = -1; // Virtual memory size
};
static MemStats s_cached;
static double s_last_sample = -1.0;
#ifdef _WIN32
static MemStats sample_memory() {
MemStats s;
PROCESS_MEMORY_COUNTERS pmc{};
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
s.rss_kb = static_cast<long>(pmc.WorkingSetSize / 1024);
s.peak_kb = static_cast<long>(pmc.PeakWorkingSetSize / 1024);
s.vsize_kb = -1; // not easily available on Windows without VirtualQuery loop
}
return s;
}
#else
static MemStats sample_memory() {
MemStats s;
FILE* f = std::fopen("/proc/self/status", "r");
if (!f) return s;
char line[128];
while (std::fgets(line, sizeof(line), f)) {
long val = 0;
if (std::sscanf(line, "VmRSS: %ld kB", &val) == 1) s.rss_kb = val;
if (std::sscanf(line, "VmPeak: %ld kB", &val) == 1) s.peak_kb = val;
if (std::sscanf(line, "VmSize: %ld kB", &val) == 1) s.vsize_kb = val;
}
std::fclose(f);
return s;
}
#endif
} // namespace
void memory_overlay() {
#ifdef TRACY_ENABLE
ZoneScoped;
#endif
// Sample at most once per second
const double now = ImGui::GetTime();
if (s_last_sample < 0.0 || (now - s_last_sample) >= 1.0) {
s_cached = sample_memory();
s_last_sample = now;
}
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_AlwaysAutoResize
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoMove;
const float pad = 10.0f;
const ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 pos(vp->WorkPos.x + vp->WorkSize.x - pad,
vp->WorkPos.y + vp->WorkSize.y - pad);
ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.65f);
if (ImGui::Begin("##memory_overlay", nullptr, flags)) {
if (s_cached.rss_kb >= 0) {
ImGui::Text("RSS: %5ld MB", s_cached.rss_kb / 1024);
ImGui::Text("Peak: %5ld MB", s_cached.peak_kb / 1024);
#ifndef _WIN32
ImGui::Text("VSize: %5ld MB", s_cached.vsize_kb / 1024);
#endif
} else {
ImGui::TextDisabled("Memory: N/A");
}
}
ImGui::End();
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Renders a memory statistics overlay in the bottom-right corner.
// Call within an ImGui frame once per frame.
// Samples /proc/self/status (Linux) or GetProcessMemoryInfo (Windows) at most
// once per second; results are cached between samples.
void memory_overlay();
+56
View File
@@ -0,0 +1,56 @@
---
name: memory_overlay
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void memory_overlay()"
description: "Renderiza un overlay de estadisticas de memoria (RSS, peak, vsize) en la esquina inferior derecha"
tags: [imgui, memory, overlay, debug, dashboard, profiling]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/memory_overlay.cpp"
framework: imgui
params: []
output: "Renderiza el overlay de memoria en el frame ImGui actual"
---
# memory_overlay
Muestra estadísticas de memoria del proceso en una ventana semi-transparente en la esquina inferior derecha. Complementa `fps_overlay` para un dashboard mínimo de rendimiento.
Las métricas se leen como máximo una vez por segundo y se cachean en una variable estática local, evitando I/O excesivo en el hot path de render.
## Métricas mostradas
| Campo | Linux | Windows |
|---|---|---|
| RSS (MB) | `VmRSS` de `/proc/self/status` | `WorkingSetSize` (PSAPI) |
| Peak RSS (MB) | `VmPeak` de `/proc/self/status` | `PeakWorkingSetSize` (PSAPI) |
| Virtual Size (MB) | `VmSize` de `/proc/self/status` | N/A (no mostrado) |
En Windows se requiere enlazar `psapi.lib` (el `.cpp` incluye `#pragma comment(lib, "psapi.lib")`).
## Ejemplo
```cpp
// En el loop de render, dentro de un frame ImGui:
fps_overlay();
memory_overlay();
```
## Notas
- La función sigue la misma convención que `fps_overlay`: mismos flags de ventana, mismo alpha (0.65).
- Posición: esquina inferior derecha (`pivot = {1,1}`), separada 10 px del borde.
- La lectura de `/proc/self/status` es I/O local de ~0.1 ms, amortizada a 1 Hz — despreciable.
- Con `TRACY_ENABLE` activo, la función registra una zona `ZoneScoped` para que aparezca en el profiler.
- Si la plataforma no puede leer las estadísticas, muestra "Memory: N/A" en gris.
+131
View File
@@ -0,0 +1,131 @@
#include "plot_theme.h"
// ---------------------------------------------------------------------------
// Preset structs
// ---------------------------------------------------------------------------
PlotTheme plot_theme_preset_dark() {
PlotTheme t;
t.name = "dark";
t.bg = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
t.frame_bg = ImVec4(0.14f, 0.14f, 0.17f, 1.00f);
t.text = ImVec4(0.88f, 0.88f, 0.90f, 1.00f);
t.grid = ImVec4(0.30f, 0.30f, 0.35f, 0.60f);
t.palette_count = 10;
t.palette[0] = ImVec4(0.33f, 0.62f, 0.91f, 1.00f); // steel blue
t.palette[1] = ImVec4(0.35f, 0.78f, 0.54f, 1.00f); // soft green
t.palette[2] = ImVec4(0.96f, 0.60f, 0.25f, 1.00f); // warm orange
t.palette[3] = ImVec4(0.85f, 0.38f, 0.42f, 1.00f); // muted red
t.palette[4] = ImVec4(0.72f, 0.52f, 0.88f, 1.00f); // lavender
t.palette[5] = ImVec4(0.38f, 0.80f, 0.82f, 1.00f); // teal
t.palette[6] = ImVec4(0.95f, 0.80f, 0.32f, 1.00f); // gold
t.palette[7] = ImVec4(0.60f, 0.82f, 0.38f, 1.00f); // lime
t.palette[8] = ImVec4(0.90f, 0.55f, 0.75f, 1.00f); // pink
t.palette[9] = ImVec4(0.55f, 0.70f, 0.95f, 1.00f); // periwinkle
return t;
}
PlotTheme plot_theme_preset_light() {
PlotTheme t;
t.name = "light";
t.bg = ImVec4(0.97f, 0.97f, 0.97f, 1.00f);
t.frame_bg = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
t.text = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
t.grid = ImVec4(0.70f, 0.70f, 0.72f, 0.60f);
t.palette_count = 10;
t.palette[0] = ImVec4(0.12f, 0.47f, 0.71f, 1.00f); // deep blue
t.palette[1] = ImVec4(0.17f, 0.63f, 0.17f, 1.00f); // deep green
t.palette[2] = ImVec4(0.84f, 0.37f, 0.00f, 1.00f); // burnt orange
t.palette[3] = ImVec4(0.84f, 0.15f, 0.16f, 1.00f); // vivid red
t.palette[4] = ImVec4(0.58f, 0.40f, 0.74f, 1.00f); // purple
t.palette[5] = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // cyan
t.palette[6] = ImVec4(0.74f, 0.74f, 0.13f, 1.00f); // olive
t.palette[7] = ImVec4(0.09f, 0.75f, 0.81f, 1.00f); // sky blue
t.palette[8] = ImVec4(0.89f, 0.47f, 0.76f, 1.00f); // rose
t.palette[9] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); // grey
return t;
}
PlotTheme plot_theme_preset_high_contrast() {
PlotTheme t;
t.name = "high_contrast";
t.bg = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
t.frame_bg = ImVec4(0.05f, 0.05f, 0.05f, 1.00f);
t.text = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
t.grid = ImVec4(0.30f, 0.30f, 0.30f, 0.80f);
t.palette_count = 10;
t.palette[0] = ImVec4(0.00f, 1.00f, 1.00f, 1.00f); // cyan neon
t.palette[1] = ImVec4(0.00f, 1.00f, 0.00f, 1.00f); // green neon
t.palette[2] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); // orange neon
t.palette[3] = ImVec4(1.00f, 0.00f, 0.50f, 1.00f); // magenta
t.palette[4] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); // yellow
t.palette[5] = ImVec4(0.40f, 0.80f, 1.00f, 1.00f); // sky blue
t.palette[6] = ImVec4(1.00f, 0.40f, 0.40f, 1.00f); // salmon
t.palette[7] = ImVec4(0.60f, 1.00f, 0.40f, 1.00f); // lime
t.palette[8] = ImVec4(1.00f, 0.80f, 1.00f, 1.00f); // lavender bright
t.palette[9] = ImVec4(0.80f, 1.00f, 0.80f, 1.00f); // mint bright
return t;
}
// ---------------------------------------------------------------------------
// Internal helper
// ---------------------------------------------------------------------------
static void apply_imgui_colors(const PlotTheme& theme) {
ImGuiStyle& s = ImGui::GetStyle();
s.Colors[ImGuiCol_WindowBg] = theme.bg;
s.Colors[ImGuiCol_ChildBg] = theme.bg;
s.Colors[ImGuiCol_PopupBg] = theme.frame_bg;
s.Colors[ImGuiCol_FrameBg] = theme.frame_bg;
s.Colors[ImGuiCol_FrameBgHovered] = theme.frame_bg;
s.Colors[ImGuiCol_FrameBgActive] = theme.frame_bg;
s.Colors[ImGuiCol_Text] = theme.text;
s.Colors[ImGuiCol_TextDisabled] = ImVec4(theme.text.x * 0.5f,
theme.text.y * 0.5f,
theme.text.z * 0.5f,
0.80f);
}
static void apply_implot_colors(const PlotTheme& theme) {
ImPlotStyle& ps = ImPlot::GetStyle();
ps.Colors[ImPlotCol_PlotBg] = theme.bg;
ps.Colors[ImPlotCol_PlotBorder] = theme.frame_bg;
ps.Colors[ImPlotCol_FrameBg] = theme.frame_bg;
ps.Colors[ImPlotCol_LegendBg] = theme.frame_bg;
ps.Colors[ImPlotCol_LegendBorder] = theme.grid;
ps.Colors[ImPlotCol_LegendText] = theme.text;
ps.Colors[ImPlotCol_TitleText] = theme.text;
ps.Colors[ImPlotCol_InlayText] = theme.text;
ps.Colors[ImPlotCol_AxisText] = theme.text;
ps.Colors[ImPlotCol_AxisGrid] = theme.grid;
ps.Colors[ImPlotCol_AxisTick] = theme.grid;
// Register custom colormap from palette
const int count = theme.palette_count > 10 ? 10 : theme.palette_count;
ImPlot::AddColormap(theme.name, theme.palette, count, false);
ImPlot::SetColormap(theme.name);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void plot_theme_apply(const PlotTheme& theme) {
apply_imgui_colors(theme);
apply_implot_colors(theme);
}
void plot_theme_dark() {
PlotTheme t = plot_theme_preset_dark();
plot_theme_apply(t);
}
void plot_theme_light() {
PlotTheme t = plot_theme_preset_light();
plot_theme_apply(t);
}
void plot_theme_high_contrast() {
PlotTheme t = plot_theme_preset_high_contrast();
plot_theme_apply(t);
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "imgui.h"
#include "implot.h"
struct PlotTheme {
const char* name;
ImVec4 bg; // plot background
ImVec4 frame_bg; // frame background
ImVec4 text; // text color
ImVec4 grid; // grid lines
ImVec4 palette[10]; // color palette for series
int palette_count;
};
// Preset themes
void plot_theme_dark();
void plot_theme_light();
void plot_theme_high_contrast();
// Custom theme
void plot_theme_apply(const PlotTheme& theme);
// Get preset theme structs (for inspection/modification)
PlotTheme plot_theme_preset_dark();
PlotTheme plot_theme_preset_light();
PlotTheme plot_theme_preset_high_contrast();
+96
View File
@@ -0,0 +1,96 @@
---
name: plot_theme
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void plot_theme_dark() / void plot_theme_light() / void plot_theme_high_contrast() / void plot_theme_apply(const PlotTheme& theme)"
description: "Gestiona temas y paletas de colores para ImPlot e ImGui, con presets dark/light/high-contrast y soporte para temas custom"
tags: [theme, colors, palette, styling, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui, implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/plot_theme.cpp"
framework: imgui
params:
- name: theme
desc: "Estructura PlotTheme con nombre, colores de fondo/frame/texto/grid y paleta de hasta 10 colores para series de datos"
output: "Aplica el tema al contexto ImPlot/ImGui actual modificando ImGuiStyle y ImPlotStyle en memoria"
---
# plot_theme
Configura el aspecto visual de ventanas ImGui y graficas ImPlot en un solo paso. Tres presets listos para usar y una API de tema custom para casos especificos.
## Presets
### `plot_theme_dark()`
Fondo oscuro (`#1A1A1F`) con paleta de 10 colores suaves: steel blue, soft green, warm orange, muted red, lavender, teal, gold, lime, pink, periwinkle. Ideal para dashboards de monitoreo.
### `plot_theme_light()`
Fondo claro (`#F7F7F7`) con colores vibrantes de alto contraste sobre blanco. Recomendado para capturas de pantalla o exportacion a documentos.
### `plot_theme_high_contrast()`
Fondo negro puro con colores neon: cyan, green, orange, magenta, yellow. Disenado para presentaciones en pantallas grandes o condiciones de poca luz.
## Uso
```cpp
#include "plot_theme.h"
// Al inicio del frame, antes de BeginMainMenuBar / Begin
plot_theme_dark();
// O con tema custom
PlotTheme my_theme = plot_theme_preset_dark();
my_theme.palette[0] = ImVec4(1.0f, 0.0f, 0.5f, 1.0f); // override primer color
plot_theme_apply(my_theme);
```
## API
```cpp
// Presets de aplicacion directa
void plot_theme_dark();
void plot_theme_light();
void plot_theme_high_contrast();
// Tema custom
void plot_theme_apply(const PlotTheme& theme);
// Obtener structs para inspeccion o modificacion parcial
PlotTheme plot_theme_preset_dark();
PlotTheme plot_theme_preset_light();
PlotTheme plot_theme_preset_high_contrast();
```
## Estructura PlotTheme
```cpp
struct PlotTheme {
const char* name; // nombre del colormap registrado en ImPlot
ImVec4 bg; // fondo del plot (ImPlotCol_PlotBg, ImGuiCol_WindowBg)
ImVec4 frame_bg; // fondo del frame (ImPlotCol_FrameBg, ImGuiCol_FrameBg)
ImVec4 text; // color del texto (todos los ImGui/ImPlot text cols)
ImVec4 grid; // lineas de cuadricula y ticks
ImVec4 palette[10]; // paleta de colores para series de datos
int palette_count; // numero de colores activos en la paleta (1-10)
};
```
## Notas
- `plot_theme_apply` llama a `ImPlot::AddColormap` y `ImPlot::SetColormap` con el campo `name` del tema como identificador. Si se llama dos veces con el mismo `name`, ImPlot registra el colormap de nuevo — usar nombres distintos si se crean multiples variantes en runtime.
- La funcion es "pure" en el sentido de que su unico efecto es escribir en `ImGuiStyle` e `ImPlotStyle` del contexto activo, sin I/O ni estado global propio.
- Requiere que `ImGui::CreateContext()` e `ImPlot::CreateContext()` hayan sido llamados antes del primer uso.
- Compatible con C++17. No usa excepciones ni RTTI.
+25
View File
@@ -0,0 +1,25 @@
#include "core/sidebar.h"
#include "imgui.h"
static bool s_sidebar_was_open = false;
bool sidebar_begin(const char* title, bool* open, float width) {
if (!*open) {
s_sidebar_was_open = false;
if (ImGui::Button(">")) {
*open = true;
}
return false;
}
s_sidebar_was_open = true;
ImGui::SetNextWindowSize(ImVec2(width, 0.0f), ImGuiCond_Always);
ImGui::Begin(title, open, ImGuiWindowFlags_NoCollapse);
return true;
}
void sidebar_end() {
if (s_sidebar_was_open) {
ImGui::End();
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
// Collapsible sidebar panel.
// Usage:
// if (sidebar_begin("Filters", &sidebar_open)) {
// // draw filter controls
// }
// sidebar_end();
//
// The sidebar renders as a fixed-width ImGui window.
// When collapsed, only a small toggle button is shown.
bool sidebar_begin(const char* title, bool* open, float width = 250.0f);
void sidebar_end();
+59
View File
@@ -0,0 +1,59 @@
---
name: sidebar
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool sidebar_begin(const char* title, bool* open, float width = 250.0f)"
description: "Panel lateral colapsable para filtros y controles de dashboard"
tags: [imgui, sidebar, panel, layout, dashboard, controls]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/sidebar.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del sidebar mostrado en la barra de titulo de la ventana"
- name: open
desc: "Puntero al estado abierto/cerrado; se pone a false si el usuario cierra la ventana"
- name: width
desc: "Ancho del sidebar en pixels (por defecto 250)"
output: "true si el sidebar esta abierto y se debe renderizar contenido entre sidebar_begin y sidebar_end"
---
# sidebar
Panel lateral colapsable. Cuando `*open == true` renderiza una ventana ImGui de ancho fijo con boton de cierre. Cuando `*open == false` muestra un boton compacto ">" para reabrir.
Siempre llamar `sidebar_end()` despues de `sidebar_begin()`, independientemente del valor de retorno.
## Ejemplo
```cpp
static bool filters_open = true;
void render_fn() {
if (sidebar_begin("Filters", &filters_open)) {
ImGui::SliderFloat("Min value", &min_val, 0.0f, 100.0f);
ImGui::Checkbox("Show inactive", &show_inactive);
}
sidebar_end();
// contenido principal
ImGui::Begin("Main");
// ...
ImGui::End();
}
```
## Notas
El estado de `s_sidebar_was_open` es una variable estatica interna que coordina `sidebar_begin` y `sidebar_end`. Solo un sidebar activo a la vez por frame (patron begin/end clasico de ImGui). Para multiples sidebars simultaneos, instanciar logica propia con estado separado.
+21
View File
@@ -0,0 +1,21 @@
#include "core/tab_container.h"
#include "imgui.h"
bool tab_container_begin(const char* id) {
return ImGui::BeginTabBar(
id,
ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown
);
}
bool tab_container_tab(const char* label) {
return ImGui::BeginTabItem(label);
}
void tab_container_tab_end() {
ImGui::EndTabItem();
}
void tab_container_end() {
ImGui::EndTabBar();
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Tab container — wraps ImGui::BeginTabBar with dashboard styling.
// Usage:
// if (tab_container_begin("##views")) {
// if (tab_container_tab("Overview")) {
// // draw overview content
// tab_container_tab_end();
// }
// if (tab_container_tab("Details")) {
// // draw details content
// tab_container_tab_end();
// }
// }
// tab_container_end();
bool tab_container_begin(const char* id);
bool tab_container_tab(const char* label);
void tab_container_tab_end();
void tab_container_end();
+64
View File
@@ -0,0 +1,64 @@
---
name: tab_container
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "bool tab_container_begin(const char* id)"
description: "Contenedor de tabs para organizar vistas multiples en un dashboard"
tags: [imgui, tabs, container, layout, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/tab_container.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico del tab bar (ej: '##views'). Puede ser invisible con prefijo ##"
- name: label
desc: "Etiqueta visible de cada tab individual"
output: "tab_container_begin: true si el tab bar esta activo. tab_container_tab: true si el tab esta seleccionado y se debe renderizar su contenido"
---
# tab_container
Wrapper fino sobre `ImGui::BeginTabBar` / `EndTabBar` con flags de dashboard preconfigurados: `Reorderable` (arrastrar tabs) y `FittingPolicyResizeDown` (tabs se achican si no caben).
API de cuatro funciones siguiendo el patron begin/end de ImGui. Siempre llamar `tab_container_end()` si `tab_container_begin()` retorno true. Siempre llamar `tab_container_tab_end()` dentro del bloque `if (tab_container_tab(...))`.
## Ejemplo
```cpp
void render_fn() {
ImGui::Begin("Dashboard");
if (tab_container_begin("##main_tabs")) {
if (tab_container_tab("Overview")) {
render_overview();
tab_container_tab_end();
}
if (tab_container_tab("Details")) {
render_details();
tab_container_tab_end();
}
if (tab_container_tab("Settings")) {
render_settings();
tab_container_tab_end();
}
}
tab_container_end();
ImGui::End();
}
```
## Notas
Los flags `ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown` son fijos para estandarizar la experiencia de tabs en dashboards. Si se necesitan flags distintos usar `ImGui::BeginTabBar` directamente.
+90
View File
@@ -0,0 +1,90 @@
#include "time_series_buffer.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include <limits>
#include <numeric>
#include <utility>
TimeSeriesBuffer::TimeSeriesBuffer(size_t cap)
: data(new float[cap]), capacity(cap), count(0), offset(0) {}
TimeSeriesBuffer::~TimeSeriesBuffer() {
delete[] data;
}
TimeSeriesBuffer::TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept
: data(other.data), capacity(other.capacity), count(other.count), offset(other.offset) {
other.data = nullptr;
other.capacity = 0;
other.count = 0;
other.offset = 0;
}
TimeSeriesBuffer& TimeSeriesBuffer::operator=(TimeSeriesBuffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
capacity = other.capacity;
count = other.count;
offset = other.offset;
other.data = nullptr;
other.capacity = 0;
other.count = 0;
other.offset = 0;
}
return *this;
}
void TimeSeriesBuffer::push(float value) {
data[offset % capacity] = value;
offset++;
if (count < capacity) count++;
}
float TimeSeriesBuffer::get(size_t index) const {
assert(index < count);
// oldest element is at (offset - count) % capacity
size_t real = (offset - count + index) % capacity;
return data[real];
}
float TimeSeriesBuffer::latest() const {
assert(count > 0);
return data[(offset - 1) % capacity];
}
float TimeSeriesBuffer::min() const {
float m = std::numeric_limits<float>::max();
for (size_t i = 0; i < count; ++i) m = std::min(m, get(i));
return m;
}
float TimeSeriesBuffer::max() const {
float m = std::numeric_limits<float>::lowest();
for (size_t i = 0; i < count; ++i) m = std::max(m, get(i));
return m;
}
float TimeSeriesBuffer::average() const {
if (count == 0) return 0.0f;
float sum = 0.0f;
for (size_t i = 0; i < count; ++i) sum += get(i);
return sum / static_cast<float>(count);
}
size_t TimeSeriesBuffer::size() const {
return count;
}
bool TimeSeriesBuffer::full() const {
return count == capacity;
}
size_t TimeSeriesBuffer::copy_ordered(float* out, size_t out_capacity) const {
size_t n = std::min(count, out_capacity);
for (size_t i = 0; i < n; ++i)
out[i] = get(i);
return n;
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cstddef>
struct TimeSeriesBuffer {
float* data;
size_t capacity;
size_t count;
size_t offset; // write head
TimeSeriesBuffer(size_t cap);
~TimeSeriesBuffer();
// Non-copyable, moveable
TimeSeriesBuffer(const TimeSeriesBuffer&) = delete;
TimeSeriesBuffer& operator=(const TimeSeriesBuffer&) = delete;
TimeSeriesBuffer(TimeSeriesBuffer&& other) noexcept;
TimeSeriesBuffer& operator=(TimeSeriesBuffer&& other) noexcept;
void push(float value);
float get(size_t index) const; // 0 = oldest
float latest() const;
float min() const;
float max() const;
float average() const;
size_t size() const;
bool full() const;
// For ImPlot: copies data in order to a contiguous array
// Returns actual count written
size_t copy_ordered(float* out, size_t out_capacity) const;
};
+59
View File
@@ -0,0 +1,59 @@
---
name: time_series_buffer
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "TimeSeriesBuffer(size_t capacity)"
description: "Ring buffer circular para datos de series temporales, optimizado para streaming de metricas en dashboards en tiempo real"
tags: [buffer, timeseries, streaming, dashboard, data]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/time_series_buffer.cpp"
framework: imgui
params:
- name: capacity
desc: "Numero maximo de muestras que almacena el buffer"
output: "Buffer circular listo para push de datos y lectura ordenada"
---
# time_series_buffer
Ring buffer circular de floats para series temporales en tiempo real. Diseñado para alimentar plots de ImPlot con metricas de streaming (FPS, latencia, uso de CPU, etc.).
## Uso basico
```cpp
TimeSeriesBuffer fps_history(512);
// En el loop principal:
fps_history.push(ImGui::GetIO().Framerate);
// Para renderizar con ImPlot:
float ordered[512];
size_t n = fps_history.copy_ordered(ordered, 512);
ImPlot::PlotLines("FPS", ordered, (int)n);
```
## Semantica del ring buffer
- `push(v)` escribe en `data[offset % capacity]` y avanza el write head.
- `count` crece hasta `capacity` y se mantiene ahi — el buffer nunca desborda.
- `get(0)` retorna el elemento mas antiguo; `get(count-1)` el mas reciente.
- `latest()` es equivalente a `get(count-1)` pero mas explicito.
- `copy_ordered` extrae los datos en orden cronologico para pasarlos a ImPlot u otro consumidor que espere un array contiguo.
## Notas
- No depende de ImGui ni ImPlot directamente — es una estructura de datos pura.
- No es thread-safe. Para uso multihilo, proteger con mutex externo.
- Move semantics implementadas; copy queda eliminada (`= delete`) para evitar copias accidentales del heap.
- `min`, `max` y `average` iteran sobre `count` elementos — O(n). Para buffers grandes en hot paths, considerar mantener un acumulador incremental.
+5
View File
@@ -0,0 +1,5 @@
// tracy_zone.cpp
// All definitions live in tracy_zone.h (macros + constexpr).
// This translation unit exists so the header can be included in any build
// without requiring Tracy — compile with -DTRACY_ENABLE to activate profiling.
#include "core/tracy_zone.h"
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
// Convenience macros for Tracy profiling zones.
// No-op when TRACY_ENABLE is not defined.
// Usage: FN_ZONE("my function") at the top of a scope.
#ifdef TRACY_ENABLE
#include "tracy/Tracy.hpp"
// Named zone (appears as-is in Tracy timeline)
#define FN_ZONE(name) ZoneScopedN(name)
// Named zone with explicit ARGB color
#define FN_ZONE_COLOR(name, color) ZoneScopedNC(name, color)
// Frame boundary marker
#define FN_FRAME_MARK FrameMark
// Plot a scalar value in Tracy's plot view
#define FN_PLOT(name, val) TracyPlot(name, val)
#else
#define FN_ZONE(name) (void)0
#define FN_ZONE_COLOR(name, color) (void)0
#define FN_FRAME_MARK (void)0
#define FN_PLOT(name, val) (void)0
#endif
// Preset ARGB colors for common zone categories.
namespace fn_tracy {
constexpr uint32_t COLOR_RENDER = 0x2196F3; // blue
constexpr uint32_t COLOR_UPDATE = 0x4CAF50; // green
constexpr uint32_t COLOR_IO = 0xFF9800; // orange
constexpr uint32_t COLOR_NETWORK = 0xF44336; // red
constexpr uint32_t COLOR_COMPUTE = 0x9C27B0; // purple
}
+83
View File
@@ -0,0 +1,83 @@
---
name: tracy_zone
kind: function
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "FN_ZONE(name) / FN_ZONE_COLOR(name, color) / FN_FRAME_MARK / FN_PLOT(name, val)"
description: "Macros y constantes de conveniencia para Tracy profiling zones, compilables sin Tracy"
tags: [tracy, profiling, debug, performance, raii]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/tracy_zone.cpp"
framework: imgui
params:
- name: name
desc: "Nombre de la zona tal como aparece en la timeline de Tracy"
- name: color
desc: "Color ARGB uint32 de la zona en Tracy (opcional, solo FN_ZONE_COLOR)"
output: "Zona Tracy activa durante el scope actual; no-op cuando TRACY_ENABLE no está definido"
---
# tracy_zone
Macros de scope para instrumentar secciones de código con Tracy Profiler. Cuando `TRACY_ENABLE` no está definido (ej. builds de producción) todas las macros se expanden a `(void)0`, sin coste alguno.
## Macros disponibles
| Macro | Descripción |
|---|---|
| `FN_ZONE("name")` | Zona con nombre, color automático |
| `FN_ZONE_COLOR("name", color)` | Zona con nombre y color ARGB explícito |
| `FN_FRAME_MARK` | Marca el límite de frame (eje X de Tracy) |
| `FN_PLOT("name", val)` | Envía un valor escalar al panel de plots |
## Colores predefinidos (`fn_tracy::`)
```cpp
fn_tracy::COLOR_RENDER // 0x2196F3 azul — rendering
fn_tracy::COLOR_UPDATE // 0x4CAF50 verde — game update / logic
fn_tracy::COLOR_IO // 0xFF9800 naranja — I/O disco/red
fn_tracy::COLOR_NETWORK // 0xF44336 rojo — red/HTTP
fn_tracy::COLOR_COMPUTE // 0x9C27B0 morado — compute / shader prep
```
## Ejemplo
```cpp
#include "core/tracy_zone.h"
void render_scene() {
FN_ZONE_COLOR("render_scene", fn_tracy::COLOR_RENDER);
// ... render calls ...
}
void update(float dt) {
FN_ZONE("update");
// ... game logic ...
FN_PLOT("dt_ms", dt * 1000.0f);
}
void main_loop() {
while (running) {
update(dt);
render_scene();
FN_FRAME_MARK;
}
}
```
## Notas
- Compilar con `-DTRACY_ENABLE` y enlazar `TracyClient.cpp` para activar profiling.
- Sin `TRACY_ENABLE` el .cpp es prácticamente vacío — coste cero en producción.
- Los colores son ARGB; si el alpha es 0 Tracy aplica su color automático por zona.
- `FN_ZONE` expande a `ZoneScopedN(name)` de Tracy, que crea el contexto con `__LINE__` como discriminador — es seguro llamar varias veces en el mismo scope.
+81
View File
@@ -0,0 +1,81 @@
#include "viz/candlestick.h"
#include "imgui.h"
#include "implot.h"
void candlestick(const char* title, const double* dates, const double* opens,
const double* closes, const double* lows, const double* highs,
int count, float width_percent, bool tooltip) {
if (count <= 0) return;
// Compute half-width of each candle body in data coordinates.
// Use spacing between consecutive dates when count > 1, else fallback to 0.5.
double spacing = (count > 1) ? (dates[1] - dates[0]) : 1.0;
double half_w = spacing * (double)width_percent * 0.5;
ImPlot::SetupAxes("Date", "Price", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisScale(ImAxis_X1, ImPlotScale_Time);
// Auto-fit X axis to the data range with a small margin.
ImPlot::SetupAxisLimits(ImAxis_X1,
dates[0] - spacing,
dates[count - 1] + spacing,
ImGuiCond_Always);
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
ImDrawList* draw = ImPlot::GetPlotDrawList();
const ImU32 col_bull = IM_COL32(0, 200, 80, 255); // green — close >= open
const ImU32 col_bear = IM_COL32(220, 50, 50, 255); // red — close < open
int hovered_idx = -1;
for (int i = 0; i < count; i++) {
double x = dates[i];
double open = opens[i];
double close = closes[i];
double low = lows[i];
double high = highs[i];
bool bullish = (close >= open);
ImU32 col = bullish ? col_bull : col_bear;
// Convert data coordinates to screen pixels.
ImVec2 body_tl = ImPlot::PlotToPixels(x - half_w, bullish ? close : open);
ImVec2 body_br = ImPlot::PlotToPixels(x + half_w, bullish ? open : close);
ImVec2 wick_hi = ImPlot::PlotToPixels(x, high);
ImVec2 wick_lo = ImPlot::PlotToPixels(x, low);
float cx = (body_tl.x + body_br.x) * 0.5f;
// Wick (high-low vertical line).
draw->AddLine(ImVec2(cx, wick_hi.y), ImVec2(cx, wick_lo.y), col, 1.5f);
// Body rectangle (open-close).
// Ensure at least 1px height so flat candles are visible.
if (body_br.y <= body_tl.y + 1.0f) body_br.y = body_tl.y + 1.0f;
draw->AddRectFilled(body_tl, body_br, col);
draw->AddRect(body_tl, body_br, col);
// Track hovered candle for tooltip.
if (tooltip && ImPlot::IsPlotHovered()) {
ImVec2 mouse = ImGui::GetMousePos();
if (mouse.x >= body_tl.x - 4 && mouse.x <= body_br.x + 4 &&
mouse.y >= wick_hi.y - 4 && mouse.y <= wick_lo.y + 4) {
hovered_idx = i;
}
}
}
// Tooltip for the hovered candle.
if (tooltip && hovered_idx >= 0) {
int i = hovered_idx;
ImGui::BeginTooltip();
ImGui::Text("O: %.4f", opens[i]);
ImGui::Text("H: %.4f", highs[i]);
ImGui::Text("L: %.4f", lows[i]);
ImGui::Text("C: %.4f", closes[i]);
ImGui::EndTooltip();
}
ImPlot::EndPlot();
}
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// Renders an OHLC candlestick chart using ImPlot custom rendering.
// Call within an ImGui frame (inside fn::run_app render callback).
// Green candles when close >= open, red when close < open.
// width_percent controls candle body width as a fraction of inter-candle spacing.
void candlestick(const char* title, const double* dates, const double* opens,
const double* closes, const double* lows, const double* highs,
int count, float width_percent = 0.25f, bool tooltip = true);
+67
View File
@@ -0,0 +1,67 @@
---
name: candlestick
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void candlestick(const char* title, const double* dates, const double* opens, const double* closes, const double* lows, const double* highs, int count, float width_percent = 0.25f, bool tooltip = true)"
description: "Renderiza un grafico de velas OHLC usando ImPlot custom rendering. Verde para velas alcistas (close >= open), rojo para bajistas."
tags: [implot, chart, visualization, gpu, candlestick, ohlc, finance]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot, imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/candlestick.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico, se muestra como header del plot"
- name: dates
desc: "Array de timestamps Unix o indices numericos del eje X, uno por vela"
- name: opens
desc: "Array de precios de apertura, uno por vela"
- name: closes
desc: "Array de precios de cierre, uno por vela"
- name: lows
desc: "Array de precios minimos (punta inferior del wick), uno por vela"
- name: highs
desc: "Array de precios maximos (punta superior del wick), uno por vela"
- name: count
desc: "Numero de velas (longitud de todos los arrays)"
- name: width_percent
desc: "Ancho del body de cada vela como fraccion del espacio entre puntos consecutivos (0.0-1.0, default 0.25)"
- name: tooltip
desc: "Si true, muestra tooltip con valores O/H/L/C al hacer hover sobre una vela"
output: "Renderiza el grafico de velas OHLC en el frame ImGui actual, sin retornar valor"
---
# candlestick
Grafico de velas OHLC completo usando custom rendering de ImPlot. Dibuja body (open-close) y wicks (high-low) por vela usando `ImPlot::GetPlotDrawList()` y `ImPlot::PlotToPixels()` para conversion de coordenadas.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). El eje X se configura con `ImPlotScale_Time` para timestamps Unix.
Solo tiene overload `double` porque los datos financieros requieren doble precision.
## Ejemplo
```cpp
// arrays de datos financieros (timestamps Unix, precios)
candlestick("BTC/USD", dates, opens, closes, lows, highs, 90);
// sin tooltip, velas mas anchas
candlestick("ETH/USD", dates, opens, closes, lows, highs, 30, 0.6f, false);
```
## Notas
- El ancho de cada vela se calcula como `(dates[1] - dates[0]) * width_percent * 0.5` en cada lado. Asume spacing uniforme entre velas.
- Para un solo punto (`count == 1`) el spacing por defecto es 1.0.
- La deteccion de hover usa un margen de 4px alrededor del area cuerpo+wick para facilitar la interaccion.
- El eje X usa `ImPlotScale_Time` — si los datos son indices numericos simples (0, 1, 2...) en lugar de timestamps, pasar `ImPlotAxisFlags_NoDecorations` o cambiar `SetupAxisScale`.
+94
View File
@@ -0,0 +1,94 @@
#include "viz/gauge.h"
#include "imgui.h"
#include <cmath>
#include <cstdio>
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif
void gauge(const char* label, float value, float min_val, float max_val, float radius) {
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
// Reserve space: diameter + label line
float diameter = radius * 2.0f;
ImGui::Dummy(ImVec2(diameter, diameter + ImGui::GetTextLineHeightWithSpacing()));
ImVec2 center = ImVec2(pos.x + radius, pos.y + radius);
// Arc spans 240 degrees: from 150deg to 390deg (i.e. 150 to 30 going clockwise)
// In screen space Y is down, so angles go clockwise.
// Start angle: 150 degrees = bottom-left, End angle: 390 = 30 degrees = bottom-right
const float angle_start = (150.0f * (float)M_PI) / 180.0f;
const float angle_end = (390.0f * (float)M_PI) / 180.0f;
const int num_segments = 64;
// Background arc (dark gray)
ImU32 bg_color = IM_COL32(60, 60, 60, 220);
for (int i = 0; i < num_segments; i++) {
float a0 = angle_start + (angle_end - angle_start) * ((float)i / num_segments);
float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
draw_list->AddLine(
ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
bg_color, 6.0f);
}
// Normalize value to [0, 1]
float t = 0.0f;
if (max_val > min_val) {
t = (value - min_val) / (max_val - min_val);
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
}
// Color: green (t=0) -> yellow (t=0.5) -> red (t=1)
float r, g, b;
if (t < 0.5f) {
float s = t * 2.0f;
r = (unsigned char)(s * 255);
g = 200;
b = 0;
} else {
float s = (t - 0.5f) * 2.0f;
r = 220;
g = (unsigned char)((1.0f - s) * 200);
b = 0;
}
ImU32 value_color = IM_COL32((int)r, (int)g, (int)b, 255);
// Value arc
float angle_value = angle_start + (angle_end - angle_start) * t;
int value_segments = (int)(num_segments * t);
for (int i = 0; i < value_segments; i++) {
float a0 = angle_start + (angle_end - angle_start) * ((float)i / num_segments);
float a1 = angle_start + (angle_end - angle_start) * ((float)(i + 1) / num_segments);
draw_list->AddLine(
ImVec2(center.x + cosf(a0) * radius, center.y + sinf(a0) * radius),
ImVec2(center.x + cosf(a1) * radius, center.y + sinf(a1) * radius),
value_color, 6.0f);
}
// Needle: line from center to arc at current angle
float needle_len = radius * 0.75f;
ImVec2 needle_tip = ImVec2(
center.x + cosf(angle_value) * needle_len,
center.y + sinf(angle_value) * needle_len);
draw_list->AddLine(center, needle_tip, IM_COL32(255, 255, 255, 240), 2.0f);
draw_list->AddCircleFilled(center, 4.0f, IM_COL32(255, 255, 255, 200));
// Value text centered
char val_buf[32];
snprintf(val_buf, sizeof(val_buf), "%.1f", value);
ImVec2 val_size = ImGui::CalcTextSize(val_buf);
draw_list->AddText(
ImVec2(center.x - val_size.x * 0.5f, center.y + radius * 0.35f),
IM_COL32(230, 230, 230, 255), val_buf);
// Label below value
ImVec2 label_size = ImGui::CalcTextSize(label);
draw_list->AddText(
ImVec2(center.x - label_size.x * 0.5f, center.y + radius * 0.35f + val_size.y + 2.0f),
IM_COL32(180, 180, 180, 200), label);
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Renders a circular gauge/speedometer indicator using ImGui draw primitives.
// Call within an ImGui frame.
// color is interpolated green->yellow->red based on normalized value.
void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f);
+59
View File
@@ -0,0 +1,59 @@
---
name: gauge
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void gauge(const char* label, float value, float min_val, float max_val, float radius = 60.0f)"
description: "Renderiza un indicador circular tipo gauge/velocimetro usando ImGui draw primitives"
tags: [imgui, visualization, gauge, kpi, dashboard]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/gauge.cpp"
framework: imgui
params:
- name: label
desc: "Etiqueta del gauge, se muestra centrada debajo del valor numerico"
- name: value
desc: "Valor actual a mostrar en el gauge"
- name: min_val
desc: "Valor minimo de la escala (extremo izquierdo del arco)"
- name: max_val
desc: "Valor maximo de la escala (extremo derecho del arco)"
- name: radius
desc: "Radio del gauge en pixels (default 60.0)"
output: "Renderiza el gauge en el frame ImGui actual, reservando espacio con ImGui::Dummy"
---
# gauge
Indicador circular tipo gauge/velocimetro construido sobre ImGui draw primitives. No requiere ImPlot.
El arco ocupa 240 grados (de 150deg a 390deg en sentido horario). El color del arco de valor interpolado de verde (minimo) a amarillo (mitad) a rojo (maximo). Una aguja blanca apunta al valor actual.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
// KPI card con gauge de temperatura
gauge("CPU Temp", 72.5f, 0.0f, 100.0f, 50.0f);
// Gauge grande para dashboard principal
gauge("Velocidad", 3200.0f, 0.0f, 5000.0f, 80.0f);
```
## Notas
- El arco de fondo es gris oscuro (IM_COL32(60,60,60,220)), 6px de grosor.
- La aguja tiene longitud del 75% del radio para evitar solapar el arco.
- Usa solo `float`; no ofrece overload `double` porque ImGui DrawList trabaja en coordenadas de pantalla (float).
- El espacio reservado es `diameter x (diameter + line_height)` para incluir la etiqueta.
+353
View File
@@ -0,0 +1,353 @@
#include "viz/graph_force_layout.h"
#include "viz/graph_types.h"
#include <cmath>
#include <cstdlib>
#include <algorithm>
// ---------------------------------------------------------------------------
// Quadtree for Barnes-Hut approximation
// ---------------------------------------------------------------------------
struct QuadNode {
float cx, cy; // center of mass
float mass; // total mass (node count in subtree)
float x0, y0; // bounding box min
float x1, y1; // bounding box max
int children[4]; // NW=0, NE=1, SW=2, SE=3 (-1 = empty)
int body; // node index if leaf (-1 if internal)
};
static constexpr int MAX_QUAD_NODES = 1 << 20; // supports graphs up to ~1M nodes
static QuadNode quad_pool[MAX_QUAD_NODES];
static int quad_count = 0;
static int quad_new(float x0, float y0, float x1, float y1) {
if (quad_count >= MAX_QUAD_NODES) return -1;
int idx = quad_count++;
QuadNode& q = quad_pool[idx];
q.cx = 0; q.cy = 0; q.mass = 0;
q.x0 = x0; q.y0 = y0; q.x1 = x1; q.y1 = y1;
q.children[0] = q.children[1] = q.children[2] = q.children[3] = -1;
q.body = -1;
return idx;
}
// Determine quadrant index for point (px,py) relative to cell midpoint.
// 0=NW, 1=NE, 2=SW, 3=SE
static int quad_child_idx(const QuadNode& q, float px, float py) {
float mx = (q.x0 + q.x1) * 0.5f;
float my = (q.y0 + q.y1) * 0.5f;
int xi = (px >= mx) ? 1 : 0;
int yi = (py >= my) ? 2 : 0;
return xi | yi;
}
// Subdivide cell qi into four children.
static void quad_subdivide(int qi) {
QuadNode& q = quad_pool[qi];
float mx = (q.x0 + q.x1) * 0.5f;
float my = (q.y0 + q.y1) * 0.5f;
// NW
quad_pool[qi].children[0] = quad_new(q.x0, q.y0, mx, my);
// NE
quad_pool[qi].children[1] = quad_new(mx, q.y0, q.x1, my);
// SW
quad_pool[qi].children[2] = quad_new(q.x0, my, mx, q.y1);
// SE
quad_pool[qi].children[3] = quad_new(mx, my, q.x1, q.y1);
}
// Insert body (node_idx at position nx,ny with mass nmass) into cell qi.
// Uses iterative descent to avoid stack overflow on deep trees.
static void quad_insert(int root, int node_idx, float nx, float ny, float nmass) {
int qi = root;
while (qi >= 0) {
QuadNode& q = quad_pool[qi];
// Update center of mass
float total = q.mass + nmass;
q.cx = (q.cx * q.mass + nx * nmass) / total;
q.cy = (q.cy * q.mass + ny * nmass) / total;
q.mass = total;
if (q.body == -1 && q.children[0] == -1) {
// Empty leaf: place body here
q.body = node_idx;
return;
}
if (q.body >= 0) {
// Leaf with existing body: subdivide, push existing body down
quad_subdivide(qi);
// Move old body into correct child (re-read q after subdivide since pool may shift)
QuadNode& qq = quad_pool[qi];
int old_body = qq.body;
float obx = /* we need positions */ 0, oby = 0;
// We store positions in the GraphData, pass via closure is not possible here.
// Instead we pass a pointer to positions alongside. We'll fix this by using
// a file-scope pointer set before each build.
(void)old_body; (void)obx; (void)oby;
// NOTE: positions accessed via file-scope g_nodes pointer below.
qq.body = -1;
}
int ci = quad_child_idx(quad_pool[qi], nx, ny);
qi = quad_pool[qi].children[ci];
}
}
// File-scope pointers set before each tree build (avoids passing them everywhere).
static const GraphNode* g_nodes = nullptr;
// Insert body knowing positions from g_nodes.
static void quad_insert_body(int qi, int node_idx) {
float nx = g_nodes[node_idx].x;
float ny = g_nodes[node_idx].y;
const float nmass = 1.0f;
while (qi >= 0) {
QuadNode& q = quad_pool[qi];
float total = q.mass + nmass;
q.cx = (q.cx * q.mass + nx * nmass) / total;
q.cy = (q.cy * q.mass + ny * nmass) / total;
q.mass = total;
if (q.body == -1 && q.children[0] == -1) {
// Empty leaf
q.body = node_idx;
return;
}
if (q.children[0] == -1) {
// Leaf occupied: subdivide and push existing body down
int old_body = q.body;
q.body = -1;
quad_subdivide(qi);
// Push old body into child
int old_ci = quad_child_idx(quad_pool[qi], g_nodes[old_body].x, g_nodes[old_body].y);
int old_child = quad_pool[qi].children[old_ci];
if (old_child >= 0) {
QuadNode& oc = quad_pool[old_child];
oc.cx = g_nodes[old_body].x;
oc.cy = g_nodes[old_body].y;
oc.mass = 1.0f;
oc.body = old_body;
}
}
int ci = quad_child_idx(quad_pool[qi], nx, ny);
qi = quad_pool[qi].children[ci];
}
}
// Compute Barnes-Hut repulsion force on node at (nx,ny) from subtree qi.
// Accumulates force into (fx, fy).
static void quad_force(int qi, float nx, float ny,
float theta, float repulsion, float min_dist,
float& fx, float& fy) {
// Iterative traversal using a small stack to avoid recursion depth issues.
static int stack[MAX_QUAD_NODES]; // reuse static stack
int top = 0;
stack[top++] = qi;
while (top > 0) {
int ci = stack[--top];
if (ci < 0) continue;
const QuadNode& q = quad_pool[ci];
if (q.mass == 0) continue;
float dx = q.cx - nx;
float dy = q.cy - ny;
float dist2 = dx * dx + dy * dy;
float dist = std::sqrt(dist2);
if (dist < min_dist) dist = min_dist;
// Cell size
float cell_size = q.x1 - q.x0;
// Use multipole approximation if far enough OR if leaf
bool is_leaf = (q.children[0] == -1);
if (is_leaf || (cell_size / dist) < theta) {
// Coulomb repulsion: F = repulsion * mass / dist^2
float force = repulsion * q.mass / (dist * dist);
fx -= force * dx / dist;
fy -= force * dy / dist;
} else {
// Push children
for (int k = 0; k < 4; ++k) {
if (q.children[k] >= 0)
stack[top++] = q.children[k];
}
}
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config) {
if (graph.node_count <= 0) return 0.0f;
// Temporary force accumulators (stack-allocated for small graphs, static for large)
static float* fx_buf = nullptr;
static float* fy_buf = nullptr;
static int buf_cap = 0;
if (graph.node_count > buf_cap) {
delete[] fx_buf;
delete[] fy_buf;
buf_cap = graph.node_count + 64;
fx_buf = new float[buf_cap];
fy_buf = new float[buf_cap];
}
float total_energy = 0.0f;
for (int iter = 0; iter < config.iterations; ++iter) {
// Zero forces
for (int i = 0; i < graph.node_count; ++i) {
fx_buf[i] = 0.0f;
fy_buf[i] = 0.0f;
}
// ---- Build Barnes-Hut quadtree ----
// Compute bounding box of current positions
float bx0 = graph.nodes[0].x, bx1 = graph.nodes[0].x;
float by0 = graph.nodes[0].y, by1 = graph.nodes[0].y;
for (int i = 1; i < graph.node_count; ++i) {
float px = graph.nodes[i].x, py = graph.nodes[i].y;
if (px < bx0) bx0 = px; if (px > bx1) bx1 = px;
if (py < by0) by0 = py; if (py > by1) by1 = py;
}
// Add margin to avoid degeneracies
float margin = (bx1 - bx0 + by1 - by0) * 0.05f + 1.0f;
bx0 -= margin; bx1 += margin;
by0 -= margin; by1 += margin;
// Make it square
float side = std::max(bx1 - bx0, by1 - by0);
float cx = (bx0 + bx1) * 0.5f, cy = (by0 + by1) * 0.5f;
bx0 = cx - side * 0.5f; bx1 = cx + side * 0.5f;
by0 = cy - side * 0.5f; by1 = cy + side * 0.5f;
quad_count = 0;
g_nodes = graph.nodes;
int root = quad_new(bx0, by0, bx1, by1);
for (int i = 0; i < graph.node_count; ++i) {
quad_insert_body(root, i);
}
// ---- Repulsion via Barnes-Hut ----
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
quad_force(root,
graph.nodes[i].x, graph.nodes[i].y,
config.theta, config.repulsion, config.min_distance,
fx_buf[i], fy_buf[i]);
// Subtract self-interaction (the tree includes the node itself)
// Self-force: repulsion * 1 / min_dist^2, but direction is (0,0) -> skip
}
// ---- Attraction along edges (spring force) ----
for (int e = 0; e < graph.edge_count; ++e) {
const GraphEdge& edge = graph.edges[e];
int s = (int)edge.source;
int t = (int)edge.target;
if (s < 0 || s >= graph.node_count) continue;
if (t < 0 || t >= graph.node_count) continue;
float dx = graph.nodes[t].x - graph.nodes[s].x;
float dy = graph.nodes[t].y - graph.nodes[s].y;
float dist = std::sqrt(dx * dx + dy * dy);
if (dist < config.min_distance) dist = config.min_distance;
// F = k * dist * weight (Hooke: pulls toward equilibrium at 0)
float force = config.attraction * dist * edge.weight;
float fx_e = force * dx / dist;
float fy_e = force * dy / dist;
if (!graph.nodes[s].pinned) { fx_buf[s] += fx_e; fy_buf[s] += fy_e; }
if (!graph.nodes[t].pinned) { fx_buf[t] -= fx_e; fy_buf[t] -= fy_e; }
}
// ---- Gravity toward center (0,0) ----
if (config.gravity != 0.0f) {
for (int i = 0; i < graph.node_count; ++i) {
if (graph.nodes[i].pinned) continue;
fx_buf[i] -= config.gravity * graph.nodes[i].x;
fy_buf[i] -= config.gravity * graph.nodes[i].y;
}
}
// ---- Integrate: v = v * damping + F; pos += v ----
total_energy = 0.0f;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
n.vx = n.vx * config.damping + fx_buf[i];
n.vy = n.vy * config.damping + fy_buf[i];
// Clamp velocity
n.vx = std::max(-config.max_velocity, std::min(config.max_velocity, n.vx));
n.vy = std::max(-config.max_velocity, std::min(config.max_velocity, n.vy));
n.x += n.vx;
n.y += n.vy;
total_energy += n.vx * n.vx + n.vy * n.vy;
}
}
graph.update_bounds();
return total_energy;
}
void graph_force_layout_reset(GraphData& graph, float spread) {
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
// rand() produces [0, RAND_MAX]; map to [-spread, spread]
n.x = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
n.y = spread * (2.0f * (float)rand() / (float)RAND_MAX - 1.0f);
n.vx = 0.0f;
n.vy = 0.0f;
}
graph.update_bounds();
}
void graph_layout_circular(GraphData& graph, float radius) {
if (graph.node_count <= 0) return;
const float two_pi = 6.28318530718f;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
float angle = two_pi * (float)i / (float)graph.node_count;
n.x = radius * std::cos(angle);
n.y = radius * std::sin(angle);
n.vx = 0.0f;
n.vy = 0.0f;
}
graph.update_bounds();
}
void graph_layout_grid(GraphData& graph, float spacing) {
if (graph.node_count <= 0) return;
int cols = (int)std::ceil(std::sqrt((float)graph.node_count));
int rows = (graph.node_count + cols - 1) / cols;
float ox = -0.5f * (cols - 1) * spacing;
float oy = -0.5f * (rows - 1) * spacing;
for (int i = 0; i < graph.node_count; ++i) {
GraphNode& n = graph.nodes[i];
if (n.pinned) continue;
int col = i % cols;
int row = i / cols;
n.x = ox + col * spacing;
n.y = oy + row * spacing;
n.vx = 0.0f;
n.vy = 0.0f;
}
graph.update_bounds();
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
struct GraphData; // forward declare
struct ForceLayoutConfig {
float repulsion = 500.0f; // repulsion strength between all nodes
float attraction = 0.01f; // spring constant for edges
float damping = 0.85f; // velocity decay per step
float min_distance = 1.0f; // minimum distance (avoid division by zero)
float theta = 0.5f; // Barnes-Hut threshold (0 = exact, 1 = fast)
float gravity = 0.1f; // pull toward center (prevents drift)
float max_velocity = 50.0f; // cap velocity per axis
int iterations = 1; // steps per call
};
// Perform one (or more) steps of force-directed layout.
// Modifies node positions (x, y) and velocities (vx, vy) in-place.
// Returns the total kinetic energy (sum of |v|^2). When energy < threshold,
// layout has converged.
float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config = {});
// Reset: randomize positions within [-spread, spread], zero velocities.
void graph_force_layout_reset(GraphData& graph, float spread = 200.0f);
// Preset layouts (non-iterative, instant positioning)
void graph_layout_circular(GraphData& graph, float radius = 100.0f);
void graph_layout_grid(GraphData& graph, float spacing = 20.0f);
+79
View File
@@ -0,0 +1,79 @@
---
name: graph_force_layout
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "float graph_force_layout_step(GraphData& graph, const ForceLayoutConfig& config)"
description: "Layout force-directed con aproximacion Barnes-Hut para grafos grandes, ejecuta un paso de simulacion por llamada"
tags: [graph, layout, force-directed, barnes-hut, physics, gpu]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/graph_force_layout.cpp"
framework: imgui
params:
- name: graph
desc: "Referencia al grafo (GraphData) cuyos nodos se actualizan in-place. Modifica x, y, vx, vy de cada nodo no pinned."
- name: config
desc: "Parametros de la simulacion: repulsion (fuerza coulombiana), attraction (spring constant), damping (decay de velocidad), theta (precision Barnes-Hut 0=exacto/1=rapido), gravity (atraccion al centro), max_velocity, iterations."
output: "Energia cinetica total (suma de |v|^2). Cuando cae por debajo de un umbral elegido por el caller, el layout ha convergido y se puede dejar de llamar."
---
# graph_force_layout
Implementa el algoritmo de layout force-directed clasico (Fruchterman-Reingold / Eades) con aproximacion Barnes-Hut O(n log n) para escalar a grafos de miles de nodos.
## Algoritmo
Cada llamada a `graph_force_layout_step` ejecuta `config.iterations` pasos. Un paso:
1. **Construccion del quadtree** (Barnes-Hut): se calcula el bounding box de las posiciones actuales, se construye un quadtree flat en `quad_pool` (sin allocaciones por nodo). Cada celda acumula centro de masa y masa total.
2. **Repulsion**: para cada nodo se recorre el quadtree. Si el cociente `cell_size / distance < theta`, la celda se trata como una sola masa puntual (multipolo de orden 0). Si no, se desciende a los hijos. Con `theta=0` es O(n²) exacto; con `theta=0.5` es O(n log n).
3. **Atraccion**: para cada arista `(s, t)`, fuerza de Hooke `F = k * dist * weight` en la direccion del arco.
4. **Gravedad**: fuerza proporcional a la distancia al origen, evita que el grafo derive fuera de pantalla.
5. **Integracion**: `v = v * damping + F`, `pos += v`, con clamping de velocidad.
6. Nodos con `pinned = true` no se mueven en ningun paso.
## Funciones auxiliares
```cpp
// Randomizar posiciones para empezar la simulacion
graph_force_layout_reset(graph, 200.0f);
// Layout circular instantaneo (sin iteracion)
graph_layout_circular(graph, 150.0f);
// Layout en grid instantaneo
graph_layout_grid(graph, 25.0f);
```
## Ejemplo de uso tipico (loop ImGui)
```cpp
static ForceLayoutConfig cfg;
static bool running = true;
if (running) {
float energy = graph_force_layout_step(my_graph, cfg);
if (energy < 0.01f) running = false; // convergido
}
```
## Notas de implementacion
- El quadtree usa un pool estatico de `1 << 20` (~1M) celdas. Para grafos de >500K nodos
se recomienda reducir `MAX_QUAD_NODES` o aumentarlo segun memoria disponible.
- La pila de traversal en `quad_force` es tambien estatica (`static int stack[]`); no es
thread-safe si se llama desde multiples hilos simultaneamente.
- `graph_force_layout_reset` usa `rand()`. Para reproducibilidad llama `srand(seed)` antes.
- Los buffers de fuerza (`fx_buf`, `fy_buf`) se realocan una sola vez cuando el conteo de
nodos supera la capacidad previa; en el uso normal (tamano fijo) no hay allocaciones
por frame.
+446
View File
@@ -0,0 +1,446 @@
#include "viz/graph_renderer.h"
#include "viz/graph_types.h"
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
// ---------------------------------------------------------------------------
// Community palette (ABGR packed, 10 colors)
// ---------------------------------------------------------------------------
static const uint32_t k_palette[10] = {
0xFF4CAF50, // green
0xFFF44336, // red
0xFF2196F3, // blue
0xFFFF9800, // orange
0xFF9C27B0, // purple
0xFF00BCD4, // cyan
0xFFFFEB3B, // yellow
0xFFE91E63, // pink
0xFF795548, // brown
0xFF607D8B // blue-grey
};
// ---------------------------------------------------------------------------
// Internal struct
// ---------------------------------------------------------------------------
struct GraphRenderer {
unsigned int fbo;
unsigned int texture;
unsigned int rbo; // depth/stencil renderbuffer
int width, height;
// Node rendering (instanced quads)
unsigned int node_vao, node_quad_vbo, node_instance_vbo;
unsigned int node_shader;
// Edge rendering (lines)
unsigned int edge_vao, edge_vbo;
unsigned int edge_shader;
GraphRendererConfig config;
};
// ---------------------------------------------------------------------------
// Shader sources
// ---------------------------------------------------------------------------
// Node vertex shader — instanced unit quad
static const char* k_node_vert = R"(
#version 330 core
// Quad corners [-0.5, 0.5]
layout(location = 0) in vec2 a_quad;
// Per-instance: world position, size, RGBA color
layout(location = 1) in vec2 a_pos;
layout(location = 2) in float a_size;
layout(location = 3) in vec4 a_color;
out vec2 v_uv;
out vec4 v_color;
uniform vec2 u_viewport; // (width, height) in pixels
uniform float u_scale; // cam_zoom
uniform vec2 u_translate; // (tx, ty) in pixels
void main() {
// World -> screen (pixels)
vec2 screen = a_pos * u_scale + u_translate;
// Expand quad by node radius (size = diameter)
screen += a_quad * a_size * u_scale;
// Screen -> NDC
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
ndc.y = -ndc.y; // flip Y (screen Y grows downward)
gl_Position = vec4(ndc, 0.0, 1.0);
v_uv = a_quad + 0.5; // [0,1]
v_color = a_color;
}
)";
// Node fragment shader — SDF circle with outline
static const char* k_node_frag = R"(
#version 330 core
in vec2 v_uv;
in vec4 v_color;
out vec4 frag_color;
uniform float u_outline_px; // outline width in uv units
uniform float u_node_px; // node diameter in pixels (= size * zoom)
void main() {
// SDF circle centered at (0.5, 0.5) in uv space
float dist = length(v_uv - 0.5);
float r = 0.5;
// Anti-alias edge (in uv units; 1px ~ 1/u_node_px in uv space)
float fwidth_uv = 1.5 / max(u_node_px, 1.0);
float alpha = 1.0 - smoothstep(r - fwidth_uv, r, dist);
if (alpha < 0.001) discard;
// Outline ring
float outline_uv = u_outline_px / max(u_node_px, 1.0);
float outline = smoothstep(r - outline_uv - fwidth_uv, r - outline_uv, dist);
vec3 fill = v_color.rgb;
vec3 outline_col = mix(fill, vec3(1.0), 0.6); // lighter outline
vec3 color = mix(fill, outline_col, outline);
frag_color = vec4(color, v_color.a * alpha);
}
)";
// Edge vertex shader
static const char* k_edge_vert = R"(
#version 330 core
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec4 a_color;
out vec4 v_color;
uniform vec2 u_viewport;
uniform float u_scale;
uniform vec2 u_translate;
void main() {
vec2 screen = a_pos * u_scale + u_translate;
vec2 ndc = (screen / u_viewport) * 2.0 - 1.0;
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
v_color = a_color;
}
)";
// Edge fragment shader
static const char* k_edge_frag = R"(
#version 330 core
in vec4 v_color;
out vec4 frag_color;
void main() {
frag_color = v_color;
}
)";
// ---------------------------------------------------------------------------
// Shader helpers
// ---------------------------------------------------------------------------
static unsigned int compile_shader(GLenum type, const char* src) {
unsigned int s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
int ok;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buf[512];
glGetShaderInfoLog(s, sizeof(buf), nullptr, buf);
fprintf(stderr, "[graph_renderer] shader compile error: %s\n", buf);
}
return s;
}
static unsigned int link_program(const char* vert_src, const char* frag_src) {
unsigned int vs = compile_shader(GL_VERTEX_SHADER, vert_src);
unsigned int fs = compile_shader(GL_FRAGMENT_SHADER, frag_src);
unsigned int prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glLinkProgram(prog);
int ok;
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
char buf[512];
glGetProgramInfoLog(prog, sizeof(buf), nullptr, buf);
fprintf(stderr, "[graph_renderer] program link error: %s\n", buf);
}
glDeleteShader(vs);
glDeleteShader(fs);
return prog;
}
// ---------------------------------------------------------------------------
// FBO helpers
// ---------------------------------------------------------------------------
static void create_fbo(GraphRenderer* r) {
// Texture
glGenTextures(1, &r->texture);
glBindTexture(GL_TEXTURE_2D, r->texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, r->width, r->height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
// Depth renderbuffer
glGenRenderbuffers(1, &r->rbo);
glBindRenderbuffer(GL_RENDERBUFFER, r->rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, r->width, r->height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// FBO
glGenFramebuffers(1, &r->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, r->texture, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, r->rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
static void destroy_fbo(GraphRenderer* r) {
glDeleteFramebuffers(1, &r->fbo);
glDeleteTextures(1, &r->texture);
glDeleteRenderbuffers(1, &r->rbo);
r->fbo = r->texture = r->rbo = 0;
}
// ---------------------------------------------------------------------------
// Helper: unpack ABGR uint32 to float RGBA
// ---------------------------------------------------------------------------
static inline void abgr_to_rgba(uint32_t abgr, float& r, float& g, float& b, float& a) {
// ABGR layout: bits 31-24 = A, 23-16 = B, 15-8 = G, 7-0 = R
a = ((abgr >> 24) & 0xFF) / 255.0f;
b = ((abgr >> 16) & 0xFF) / 255.0f;
g = ((abgr >> 8) & 0xFF) / 255.0f;
r = ((abgr ) & 0xFF) / 255.0f;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config) {
GraphRenderer* r = new GraphRenderer();
r->width = width;
r->height = height;
r->config = config;
// --- FBO ---
create_fbo(r);
// --- Node VAO ---
// Unit quad: 4 vertices, each (x, y) in [-0.5, 0.5]
static const float quad_verts[8] = {
-0.5f, -0.5f,
0.5f, -0.5f,
-0.5f, 0.5f,
0.5f, 0.5f,
};
glGenVertexArrays(1, &r->node_vao);
glBindVertexArray(r->node_vao);
// Quad VBO (location 0)
glGenBuffers(1, &r->node_quad_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_quad_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
// Instance VBO (location 1,2,3 — position, size, color)
glGenBuffers(1, &r->node_instance_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
// layout: x, y, size, r, g, b, a — 7 floats per instance
glEnableVertexAttribArray(1); // pos
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0);
glVertexAttribDivisor(1, 1);
glEnableVertexAttribArray(2); // size
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(2 * sizeof(float)));
glVertexAttribDivisor(2, 1);
glEnableVertexAttribArray(3); // color
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float)));
glVertexAttribDivisor(3, 1);
glBindVertexArray(0);
// --- Edge VAO ---
// Each edge: 2 vertices x (x, y, r, g, b, a) = 2 * 6 floats
glGenVertexArrays(1, &r->edge_vao);
glBindVertexArray(r->edge_vao);
glGenBuffers(1, &r->edge_vbo);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
glEnableVertexAttribArray(0); // pos
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1); // color
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// --- Shaders ---
r->node_shader = link_program(k_node_vert, k_node_frag);
r->edge_shader = link_program(k_edge_vert, k_edge_frag);
return r;
}
void graph_renderer_destroy(GraphRenderer* r) {
if (!r) return;
destroy_fbo(r);
glDeleteVertexArrays(1, &r->node_vao);
glDeleteBuffers(1, &r->node_quad_vbo);
glDeleteBuffers(1, &r->node_instance_vbo);
glDeleteVertexArrays(1, &r->edge_vao);
glDeleteBuffers(1, &r->edge_vbo);
glDeleteProgram(r->node_shader);
glDeleteProgram(r->edge_shader);
delete r;
}
void graph_renderer_resize(GraphRenderer* r, int width, int height) {
if (!r) return;
if (r->width == width && r->height == height) return;
r->width = width;
r->height = height;
destroy_fbo(r);
create_fbo(r);
}
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom) {
if (!r) return 0;
// --- Save GL state ---
GLint prev_fbo;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
GLint prev_viewport[4];
glGetIntegerv(GL_VIEWPORT, prev_viewport);
// --- Bind FBO ---
glBindFramebuffer(GL_FRAMEBUFFER, r->fbo);
glViewport(0, 0, r->width, r->height);
// Clear with bg_color (ABGR)
float bg_a, bg_b, bg_g, bg_cr;
abgr_to_rgba(r->config.bg_color, bg_cr, bg_g, bg_b, bg_a);
glClearColor(bg_cr, bg_g, bg_b, bg_a);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Enable blending for anti-aliasing and transparency
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// View transform: world -> screen pixels
// tx = -cam_x * scale + width/2
// ty = -cam_y * scale + height/2
float scale = cam_zoom;
float tx = -cam_x * scale + (float)r->width * 0.5f;
float ty = -cam_y * scale + (float)r->height * 0.5f;
// ----------------------------------------------------------------
// Draw edges
// ----------------------------------------------------------------
if (graph.edge_count > 0 && graph.edges && graph.nodes) {
// Pack: 2 vertices per edge, each vertex = (x, y, r, g, b, a) = 6 floats
const int floats_per_edge = 2 * 6;
float* edge_buf = (float*)malloc((size_t)graph.edge_count * floats_per_edge * sizeof(float));
int vi = 0;
for (int i = 0; i < graph.edge_count; ++i) {
const GraphEdge& e = graph.edges[i];
uint32_t ecol = e.color != 0 ? e.color : 0xFF888888u; // default gray
float er, eg, eb, ea;
abgr_to_rgba(ecol, er, eg, eb, ea);
ea *= r->config.edge_alpha;
if (e.source < (uint32_t)graph.node_count && e.target < (uint32_t)graph.node_count) {
const GraphNode& ns = graph.nodes[e.source];
const GraphNode& nt = graph.nodes[e.target];
// Source vertex
edge_buf[vi++] = ns.x; edge_buf[vi++] = ns.y;
edge_buf[vi++] = er; edge_buf[vi++] = eg;
edge_buf[vi++] = eb; edge_buf[vi++] = ea;
// Target vertex
edge_buf[vi++] = nt.x; edge_buf[vi++] = nt.y;
edge_buf[vi++] = er; edge_buf[vi++] = eg;
edge_buf[vi++] = eb; edge_buf[vi++] = ea;
}
}
glUseProgram(r->edge_shader);
glUniform2f(glGetUniformLocation(r->edge_shader, "u_viewport"), (float)r->width, (float)r->height);
glUniform1f(glGetUniformLocation(r->edge_shader, "u_scale"), scale);
glUniform2f(glGetUniformLocation(r->edge_shader, "u_translate"), tx, ty);
glLineWidth(r->config.edge_width);
glBindVertexArray(r->edge_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->edge_vbo);
glBufferData(GL_ARRAY_BUFFER, vi * (int)sizeof(float), edge_buf, GL_DYNAMIC_DRAW);
glDrawArrays(GL_LINES, 0, vi / 6);
glBindVertexArray(0);
free(edge_buf);
}
// ----------------------------------------------------------------
// Draw nodes (instanced quads)
// ----------------------------------------------------------------
if (graph.node_count > 0 && graph.nodes) {
// Pack: 7 floats per node: x, y, size, r, g, b, a
float* node_buf = (float*)malloc((size_t)graph.node_count * 7 * sizeof(float));
for (int i = 0; i < graph.node_count; ++i) {
const GraphNode& n = graph.nodes[i];
uint32_t ncol = n.color != 0 ? n.color : k_palette[n.community % 10];
float nr, ng, nb, na;
abgr_to_rgba(ncol, nr, ng, nb, na);
float sz = n.size > 0.0f ? n.size : 4.0f;
float* p = node_buf + i * 7;
p[0] = n.x; p[1] = n.y; p[2] = sz;
p[3] = nr; p[4] = ng; p[5] = nb; p[6] = na;
}
glUseProgram(r->node_shader);
glUniform2f(glGetUniformLocation(r->node_shader, "u_viewport"), (float)r->width, (float)r->height);
glUniform1f(glGetUniformLocation(r->node_shader, "u_scale"), scale);
glUniform2f(glGetUniformLocation(r->node_shader, "u_translate"), tx, ty);
glUniform1f(glGetUniformLocation(r->node_shader, "u_outline_px"), r->config.node_outline);
glBindVertexArray(r->node_vao);
glBindBuffer(GL_ARRAY_BUFFER, r->node_instance_vbo);
glBufferData(GL_ARRAY_BUFFER, graph.node_count * 7 * (int)sizeof(float), node_buf, GL_DYNAMIC_DRAW);
// Draw 4 vertices (triangle strip quad) x node_count instances
// Pass per-instance node_px uniform via the average size (approximation)
// For exact per-node pixel size we'd need a texture or another approach;
// use a uniform average for AA quality — good enough for most graphs.
float avg_px = 8.0f * scale; // rough estimate
glUniform1f(glGetUniformLocation(r->node_shader, "u_node_px"), avg_px);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, graph.node_count);
glBindVertexArray(0);
free(node_buf);
}
// --- Restore GL state ---
glDisable(GL_BLEND);
glBindFramebuffer(GL_FRAMEBUFFER, (GLuint)prev_fbo);
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
return r->texture;
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
struct GraphData; // forward declare
struct GraphRendererConfig {
float node_outline = 1.5f; // outline width in pixels
float edge_width = 1.0f; // edge line width
float edge_alpha = 0.4f; // edge transparency
uint32_t bg_color = 0xFF1A1A1E; // ABGR background
bool edge_fade_alpha = true; // fade edge alpha by distance to camera
// Default palette for communities (when node.color == 0)
// 10 distinct colors, ABGR packed
};
struct GraphRenderer;
// Lifecycle
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {});
void graph_renderer_destroy(GraphRenderer* r);
void graph_renderer_resize(GraphRenderer* r, int width, int height);
// Render graph to internal FBO.
// cam_x, cam_y: camera center in graph space
// cam_zoom: zoom level (1.0 = 1:1 pixel mapping)
// Returns OpenGL texture ID suitable for ImGui::Image().
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom);
+87
View File
@@ -0,0 +1,87 @@
---
name: graph_renderer
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: impure
signature: "GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config)"
description: "Renderer GPU de grafos con instanced rendering a FBO, compatible con ImGui::Image para visualizacion de grafos grandes"
tags: [graph, renderer, opengl, gpu, instanced, fbo, visualization]
uses_functions: []
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/graph_renderer.cpp"
framework: imgui
params:
- name: width
desc: "Ancho del framebuffer en pixels"
- name: height
desc: "Alto del framebuffer en pixels"
- name: config
desc: "Configuracion visual: outline width, edge width, edge alpha, color de fondo, fade de aristas por distancia a camara"
output: "Handle opaco al renderer. Usar graph_renderer_draw() para obtener texture ID de OpenGL, pasable directamente a ImGui::Image()"
---
# graph_renderer
Renderer GPU de grafos basado en OpenGL 3.3 core profile con instanced rendering. Renderiza nodos y aristas de un `GraphData` a un FBO interno y retorna el texture ID para integracion directa con `ImGui::Image()`.
## Funciones del API
```cpp
// Ciclo de vida
GraphRenderer* graph_renderer_create(int width, int height, const GraphRendererConfig& config = {});
void graph_renderer_destroy(GraphRenderer* r);
void graph_renderer_resize(GraphRenderer* r, int width, int height);
// Renderizado
unsigned int graph_renderer_draw(GraphRenderer* r, const GraphData& graph,
float cam_x, float cam_y, float cam_zoom);
```
## Ejemplo de uso con ImGui
```cpp
// Inicializacion (una vez)
GraphRenderer* renderer = graph_renderer_create(800, 600);
// En el render loop
ImVec2 panel_size = ImGui::GetContentRegionAvail();
graph_renderer_resize(renderer, (int)panel_size.x, (int)panel_size.y);
unsigned int tex = graph_renderer_draw(renderer, graph_data,
cam_x, cam_y, cam_zoom);
ImGui::Image((ImTextureID)(uintptr_t)tex,
panel_size,
ImVec2(0, 1), ImVec2(1, 0)); // flip Y para OpenGL
// Destruccion
graph_renderer_destroy(renderer);
```
## Notas de implementacion
**Renderizado de nodos:** Instanced rendering con un quad unitario [-0.5, 0.5] expandido por el tamano del nodo. El fragment shader aplica un SDF circular con anti-aliasing via `smoothstep` y un anillo de outline.
**Renderizado de aristas:** `GL_LINES` con datos de posicion y color empaquetados por arista. El ancho se controla con `GraphRendererConfig::edge_width`.
**Transformacion de camara:**
```
tx = -cam_x * zoom + width/2
ty = -cam_y * zoom + height/2
ndc = (screen / viewport) * 2 - 1
```
**Paleta de comunidades:** 10 colores ABGR usados cuando `node.color == 0`, seleccionados por `node.community % 10`.
**Estado GL:** Guarda y restaura `GL_FRAMEBUFFER_BINDING` y `GL_VIEWPORT` para ser compatible con el render loop de ImGui sin efectos secundarios.
**Includes GL:** Usa `#define GL_GLEXT_PROTOTYPES` + `<GL/gl.h>` + `<GL/glext.h>`. Si el proyecto carga funciones GL via glad/gl3w, reemplazar estos includes por el loader correspondiente.
+24
View File
@@ -0,0 +1,24 @@
#include "graph_types.h"
#include <cfloat>
void GraphData::update_bounds() {
if (node_count == 0) {
min_x = min_y = max_x = max_y = 0.0f;
return;
}
min_x = max_x = nodes[0].x;
min_y = max_y = nodes[0].y;
for (int i = 1; i < node_count; ++i) {
if (nodes[i].x < min_x) min_x = nodes[i].x;
if (nodes[i].x > max_x) max_x = nodes[i].x;
if (nodes[i].y < min_y) min_y = nodes[i].y;
if (nodes[i].y > max_y) max_y = nodes[i].y;
}
}
int GraphData::find_node(uint32_t id) const {
for (int i = 0; i < node_count; ++i) {
if (nodes[i].id == id) return i;
}
return -1;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <cstdint>
// --- Graph node ---
struct GraphNode {
uint32_t id;
float x, y; // position in layout space
float vx, vy; // velocity (used by force layout)
float size; // visual radius (default 4.0)
uint32_t color; // ABGR packed (0 = use default palette)
const char* label; // optional display label (nullptr = none)
uint32_t community; // group/cluster ID (for auto-coloring)
float value; // arbitrary metric (for sizing)
bool pinned; // if true, force layout won't move this node
};
// --- Graph edge ---
struct GraphEdge {
uint32_t source; // index into GraphData::nodes
uint32_t target; // index into GraphData::nodes
float weight; // edge weight (affects attraction force)
uint32_t color; // ABGR packed (0 = default gray)
};
// --- Graph container ---
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
// Bounding box (updated by layout)
float min_x, min_y, max_x, max_y;
// Recompute bounding box from node positions
void update_bounds();
// Find node index by id. Returns -1 if not found.
int find_node(uint32_t id) const;
};
// --- Helper: create a default node ---
inline GraphNode graph_node(uint32_t id, float x = 0, float y = 0) {
return {id, x, y, 0, 0, 4.0f, 0, nullptr, 0, 0, false};
}
// --- Helper: create an edge ---
inline GraphEdge graph_edge(uint32_t source, uint32_t target, float weight = 1.0f) {
return {source, target, weight, 0};
}
+327
View File
@@ -0,0 +1,327 @@
#include "viz/graph_viewport.h"
#include "viz/graph_types.h"
#include "viz/graph_renderer.h"
#include "viz/graph_force_layout.h"
#include "core/graph_spatial_hash.h"
#include "imgui.h"
#include <cstdio> // snprintf
#include <cstring> // memset
#include <vector>
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
static void viewport_to_graph(float vx, float vy,
float widget_x, float widget_y,
float widget_w, float widget_h,
float cam_x, float cam_y, float zoom,
float& gx, float& gy)
{
gx = (vx - widget_x - widget_w * 0.5f) / zoom + cam_x;
gy = (vy - widget_y - widget_h * 0.5f) / zoom + cam_y;
}
// ---------------------------------------------------------------------------
// graph_viewport_fit
// ---------------------------------------------------------------------------
void graph_viewport_fit(GraphData& graph, GraphViewportState& state)
{
graph.update_bounds();
if (graph.node_count == 0) {
state.cam_x = 0.0f;
state.cam_y = 0.0f;
state.zoom = 1.0f;
return;
}
float cx = (graph.min_x + graph.max_x) * 0.5f;
float cy = (graph.min_y + graph.max_y) * 0.5f;
state.cam_x = cx;
state.cam_y = cy;
float span_x = graph.max_x - graph.min_x;
float span_y = graph.max_y - graph.min_y;
float span = (span_x > span_y ? span_x : span_y);
// Use render dimensions if available; fall back to a safe default.
float view_px = (state.render_w > 0 ? (float)state.render_w : 600.0f);
float view_py = (state.render_h > 0 ? (float)state.render_h : 400.0f);
float view_min = (view_px < view_py ? view_px : view_py);
if (span > 0.0f) {
state.zoom = (view_min * 0.9f) / span;
} else {
state.zoom = 1.0f;
}
// Clamp to allowed range
if (state.zoom < state.zoom_min) state.zoom = state.zoom_min;
if (state.zoom > state.zoom_max) state.zoom = state.zoom_max;
}
// ---------------------------------------------------------------------------
// graph_viewport_destroy
// ---------------------------------------------------------------------------
void graph_viewport_destroy(GraphViewportState& state)
{
if (state.renderer) {
graph_renderer_destroy(state.renderer);
state.renderer = nullptr;
}
if (state.spatial) {
delete state.spatial;
state.spatial = nullptr;
}
state.initialized = false;
}
// ---------------------------------------------------------------------------
// graph_viewport — main widget
// ---------------------------------------------------------------------------
bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
ImVec2 size)
{
bool interacted = false;
// Resolve size
ImVec2 avail = ImGui::GetContentRegionAvail();
float w = (size.x > 0.0f) ? size.x : avail.x;
float h = (size.y > 0.0f) ? size.y : avail.y;
if (w < 1.0f) w = 1.0f;
if (h < 1.0f) h = 1.0f;
int iw = (int)w, ih = (int)h;
// -------------------------------------------------------------------
// 1. Lazy init
// -------------------------------------------------------------------
if (!state.initialized) {
state.renderer = graph_renderer_create(iw, ih);
state.spatial = new SpatialHash(20.0f, 4096);
state.render_w = iw;
state.render_h = ih;
state.initialized = true;
graph_viewport_fit(graph, state);
}
// -------------------------------------------------------------------
// 2. Resize
// -------------------------------------------------------------------
if (iw != state.render_w || ih != state.render_h) {
graph_renderer_resize(state.renderer, iw, ih);
state.render_w = iw;
state.render_h = ih;
}
// -------------------------------------------------------------------
// 3. Force layout step
// -------------------------------------------------------------------
if (state.layout_running && graph.node_count > 0) {
state.layout_energy = graph_force_layout_step(graph);
if (state.layout_energy < 0.01f) {
state.layout_running = false;
}
}
// -------------------------------------------------------------------
// 4. Build spatial hash
// -------------------------------------------------------------------
if (graph.node_count > 0) {
static std::vector<float> xs_buf, ys_buf, sz_buf;
xs_buf.resize(graph.node_count);
ys_buf.resize(graph.node_count);
sz_buf.resize(graph.node_count);
for (int i = 0; i < graph.node_count; ++i) {
xs_buf[i] = graph.nodes[i].x;
ys_buf[i] = graph.nodes[i].y;
sz_buf[i] = graph.nodes[i].size;
}
state.spatial->build(xs_buf.data(), ys_buf.data(), sz_buf.data(), graph.node_count);
}
// -------------------------------------------------------------------
// 5. Invisible button to capture input
// -------------------------------------------------------------------
ImGui::PushID(id);
ImVec2 widget_pos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("canvas", ImVec2(w, h),
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonMiddle|
ImGuiButtonFlags_MouseButtonRight);
bool hovered = ImGui::IsItemHovered();
bool lm_down = ImGui::IsMouseDown(ImGuiMouseButton_Left);
bool lm_click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
bool mm_down = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
bool rm_down = ImGui::IsMouseDown(ImGuiMouseButton_Right);
ImVec2 mouse_pos = ImGui::GetMousePos();
float mx = mouse_pos.x, my = mouse_pos.y;
// Convert mouse to graph space
float gx_mouse, gy_mouse;
viewport_to_graph(mx, my,
widget_pos.x, widget_pos.y, w, h,
state.cam_x, state.cam_y, state.zoom,
gx_mouse, gy_mouse);
// -------------------------------------------------------------------
// 5a. Pan (middle or right mouse drag)
// -------------------------------------------------------------------
if (hovered && (mm_down || rm_down)) {
ImVec2 delta = ImGui::GetIO().MouseDelta;
if (delta.x != 0.0f || delta.y != 0.0f) {
state.cam_x -= delta.x / state.zoom;
state.cam_y -= delta.y / state.zoom;
interacted = true;
}
}
// -------------------------------------------------------------------
// 5b. Zoom (scroll wheel)
// -------------------------------------------------------------------
if (hovered) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
float old_zoom = state.zoom;
float new_zoom = old_zoom * (1.0f + wheel * 0.1f);
if (new_zoom < state.zoom_min) new_zoom = state.zoom_min;
if (new_zoom > state.zoom_max) new_zoom = state.zoom_max;
// Zoom toward cursor: keep gx_mouse/gy_mouse fixed in graph space
float rel_x = (mx - widget_pos.x - w * 0.5f);
float rel_y = (my - widget_pos.y - h * 0.5f);
state.cam_x += rel_x / old_zoom - rel_x / new_zoom;
state.cam_y += rel_y / old_zoom - rel_y / new_zoom;
state.zoom = new_zoom;
interacted = true;
}
}
// -------------------------------------------------------------------
// 5c. Hover — query nearest node
// -------------------------------------------------------------------
state.hovered_node = -1;
if (hovered && graph.node_count > 0) {
float hit_radius = 10.0f / state.zoom;
int nearest = state.spatial->query_nearest(gx_mouse, gy_mouse, hit_radius);
if (nearest >= 0) {
state.hovered_node = nearest;
interacted = true;
}
}
// -------------------------------------------------------------------
// 5d. Node drag (left mouse down on a node)
// -------------------------------------------------------------------
if (hovered && lm_down) {
if (state.drag_node == -1 && state.hovered_node >= 0) {
state.drag_node = state.hovered_node;
state.is_dragging = true;
}
} else {
// Release drag
if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
graph.nodes[state.drag_node].pinned = false;
}
state.drag_node = -1;
state.is_dragging = false;
}
if (state.drag_node >= 0 && state.drag_node < graph.node_count) {
GraphNode& n = graph.nodes[state.drag_node];
n.x = gx_mouse;
n.y = gy_mouse;
n.vx = 0.0f;
n.vy = 0.0f;
n.pinned = true;
interacted = true;
}
// -------------------------------------------------------------------
// 5e. Click — select node
// -------------------------------------------------------------------
if (hovered && lm_click && state.drag_node == -1) {
state.selected_node = state.hovered_node;
interacted = true;
}
// -------------------------------------------------------------------
// 5f. Keyboard shortcuts (only when widget is active/hovered)
// -------------------------------------------------------------------
if (hovered) {
if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
state.layout_running = !state.layout_running;
}
if (ImGui::IsKeyPressed(ImGuiKey_F)) {
graph_viewport_fit(graph, state);
interacted = true;
}
}
// -------------------------------------------------------------------
// 6. Render to GPU texture
// -------------------------------------------------------------------
unsigned int tex_id = graph_renderer_draw(state.renderer, graph,
state.cam_x, state.cam_y,
state.zoom);
// -------------------------------------------------------------------
// 7. Display texture (flip UV for OpenGL FBO convention)
// -------------------------------------------------------------------
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddImage(
(ImTextureID)(intptr_t)tex_id,
widget_pos,
ImVec2(widget_pos.x + w, widget_pos.y + h),
ImVec2(0.0f, 1.0f), // UV top-left (flipped Y)
ImVec2(1.0f, 0.0f) // UV bottom-right
);
// -------------------------------------------------------------------
// 8. Tooltip on hovered node
// -------------------------------------------------------------------
if (state.hovered_node >= 0 && state.hovered_node < graph.node_count) {
const GraphNode& n = graph.nodes[state.hovered_node];
// Count degree
int degree = 0;
for (int i = 0; i < graph.edge_count; ++i) {
if ((int)graph.edges[i].source == state.hovered_node ||
(int)graph.edges[i].target == state.hovered_node) {
++degree;
}
}
ImGui::BeginTooltip();
if (n.label) ImGui::TextUnformatted(n.label);
ImGui::Text("ID: %u", n.id);
ImGui::Text("Community: %u", n.community);
ImGui::Text("Degree: %d", degree);
ImGui::Text("Value: %.3f", n.value);
ImGui::EndTooltip();
}
// -------------------------------------------------------------------
// 9. Status bar overlay
// -------------------------------------------------------------------
{
char status[128];
snprintf(status, sizeof(status),
"Nodes: %d | Edges: %d | Zoom: %.2fx | Energy: %.4f | [Space] layout [F] fit",
graph.node_count, graph.edge_count,
state.zoom, state.layout_energy);
ImVec2 text_pos = ImVec2(widget_pos.x + 6.0f, widget_pos.y + h - 18.0f);
draw_list->AddText(text_pos, IM_COL32(180, 180, 180, 200), status);
}
ImGui::PopID();
return interacted;
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include "imgui.h"
struct GraphData;
struct GraphRenderer;
struct SpatialHash;
// Persistent state for graph_viewport widget. Create one per viewport and keep
// alive across frames.
struct GraphViewportState {
// Camera
float cam_x = 0.0f, cam_y = 0.0f;
float zoom = 1.0f;
float zoom_min = 0.01f, zoom_max = 50.0f;
// Interaction result (read after calling graph_viewport each frame)
int hovered_node = -1; // node index under cursor, -1 if none
int selected_node = -1; // last clicked node index, -1 if none
bool is_dragging = false;
// Layout
bool layout_running = true; // animate force layout each frame
float layout_energy = 0.0f; // kinetic energy from last step
// Internal — managed by graph_viewport / graph_viewport_destroy
GraphRenderer* renderer = nullptr;
SpatialHash* spatial = nullptr;
bool initialized = false;
// Widget pixel dimensions tracked for resize detection
int render_w = 0, render_h = 0;
// Node being dragged (-1 = none)
int drag_node = -1;
};
// Main viewport widget. Call every ImGui frame.
// id: unique ImGui widget identifier
// graph: mutable graph data (node positions updated on drag)
// state: persistent state (camera, selection, GPU renderer); must outlive frames
// size: widget size in pixels — ImVec2(0,0) uses all available space
// Returns true if any user interaction occurred (hover, click, drag, zoom).
bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state,
ImVec2 size = ImVec2(0.0f, 0.0f));
// Release GPU resources. Call once when done with the viewport.
void graph_viewport_destroy(GraphViewportState& state);
// Fit camera to current graph bounds with 10% padding.
void graph_viewport_fit(GraphData& graph, GraphViewportState& state);
+119
View File
@@ -0,0 +1,119 @@
---
name: graph_viewport
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: impure
signature: "bool graph_viewport(const char* id, GraphData& graph, GraphViewportState& state, ImVec2 size)"
description: "Widget ImGui completo para visualizacion interactiva de grafos con pan, zoom, hover, seleccion y layout en vivo"
tags: [graph, viewport, imgui, interactive, pan, zoom, dashboard]
uses_functions: ["graph_renderer_cpp_viz", "graph_force_layout_cpp_viz", "graph_spatial_hash_cpp_core"]
uses_types: ["GraphData_cpp_viz"]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/graph_viewport.cpp"
framework: imgui
props:
- name: id
type: "const char*"
required: true
description: "Identificador unico del widget ImGui"
- name: graph
type: "GraphData&"
required: true
description: "Referencia al grafo (lectura de datos, escritura de posiciones al drag)"
- name: state
type: "GraphViewportState&"
required: true
description: "Estado persistente del viewport (camera, seleccion, renderer). Debe vivir mas que los frames."
- name: size
type: "ImVec2"
required: false
description: "Tamanio del widget en pixeles. ImVec2(0,0) usa todo el espacio disponible."
emits: []
has_state: true
params:
- name: id
desc: "Identificador unico del widget ImGui. Debe ser estable entre frames."
- name: graph
desc: "Grafo a visualizar. Las posiciones de nodos se modifican al arrastrar."
- name: state
desc: "Estado persistente: camara (cam_x, cam_y, zoom), nodo seleccionado/hovereado, renderer GPU, spatial hash. Alojado por el caller."
- name: size
desc: "Tamanio del widget en pixeles. (0,0) ocupa todo el espacio disponible en la ventana ImGui."
output: "true si hubo alguna interaccion del usuario en el frame actual (hover, click, drag, zoom, teclado)"
---
# graph_viewport
Widget ImGui self-contained para visualizar grafos interactivos. Integra rendering GPU, force-directed layout y hit-testing espacial en una sola llamada por frame.
## Uso basico
```cpp
// Declarar estado persistente (fuera del loop de render)
GraphViewportState vp_state;
// En el loop de render (dentro de una ventana ImGui):
if (graph_viewport("mi_grafo", my_graph, vp_state)) {
// hubo interaccion este frame
if (vp_state.selected_node >= 0) {
auto& n = my_graph.nodes[vp_state.selected_node];
// mostrar panel de detalle de n
}
}
// Al terminar:
graph_viewport_destroy(vp_state);
```
## Estado de camara
La camara usa coordenadas del espacio del grafo:
- `cam_x`, `cam_y`: centro de la camara en espacio del grafo
- `zoom`: pixeles por unidad de grafo
`graph_viewport_fit()` centra y ajusta el zoom para que el grafo quepa con 10% de padding.
## Controles
| Accion | Control |
|--------|---------|
| Pan | Boton medio o derecho + arrastrar |
| Zoom | Rueda del raton (hacia el cursor) |
| Seleccionar nodo | Click izquierdo |
| Arrastrar nodo | Click izquierdo sobre nodo |
| Toggle layout | Barra espaciadora |
| Fit camara | F |
## Force layout
El layout se ejecuta automaticamente cada frame mientras `state.layout_running == true`. Se detiene solo cuando la energia cinetica cae por debajo de `0.01`. Se puede pausar/reanudar con la barra espaciadora.
Los nodos arrastrados se marcan como `pinned = true` durante el drag, impidiendo que el force layout los mueva. Al soltar, `pinned` vuelve a `false`.
## Tooltip
Al hacer hover sobre un nodo se muestra un tooltip con: label, id numerico, community, degree (aristas conectadas) y value.
## Status bar
En la parte inferior del widget aparece: numero de nodos, aristas, zoom actual, energia del layout y recordatorio de atajos de teclado.
## Inicializacion lazy
El renderer OpenGL y el spatial hash se crean en el primer frame. La camara se ajusta automaticamente con `graph_viewport_fit` en la inicializacion.
## Notas de implementacion
- Usa `ImGui::InvisibleButton` con flags para los tres botones del raton, capturando input sin dibujar ningun boton visible.
- La textura del renderer se muestra con UV volteado en Y (`ImVec2(0,1)` a `ImVec2(1,0)`) para corregir la convencion de coordenadas de OpenGL vs ImGui.
- El spatial hash se reconstruye cada frame desde las posiciones actuales de los nodos, garantizando hit-testing correcto despues de drag o layout.
- El zoom hacia el cursor mantiene el punto del grafo bajo el cursor fijo en pantalla ajustando `cam_x`/`cam_y`.
+18
View File
@@ -0,0 +1,18 @@
#include "viz/histogram.h"
#include "implot.h"
void histogram(const char* title, const float* values, int count, int bins) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
int b = (bins > 0) ? bins : ImPlotBin_Sturges;
ImPlot::PlotHistogram("##data", values, count, b);
ImPlot::EndPlot();
}
}
void histogram(const char* title, const double* values, int count, int bins) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
int b = (bins > 0) ? bins : ImPlotBin_Sturges;
ImPlot::PlotHistogram("##data", values, count, b);
ImPlot::EndPlot();
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Renders a histogram using ImPlot::PlotHistogram.
// Call within an ImGui frame.
// bins == -1: automatic bin count via Sturges' rule.
void histogram(const char* title, const float* values, int count, int bins = -1);
void histogram(const char* title, const double* values, int count, int bins = -1);
+42
View File
@@ -0,0 +1,42 @@
---
name: histogram
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void histogram(const char* title, const float* values, int count, int bins = -1)"
description: "Renderiza un histograma con bins automaticos o manuales usando ImPlot PlotHistogram dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, histogram, distribution]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/histogram.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del histograma mostrado como cabecera del plot"
- name: values
desc: "Array de valores numericos a distribuir en bins"
- name: count
desc: "Numero de valores en el array"
- name: bins
desc: "Numero de bins. -1 = automatico via regla de Sturges (ImPlotBin_Sturges). Positivo = numero explicito de bins"
output: "Renderiza el histograma en el frame ImGui actual"
---
# histogram
Wrapper atomico sobre `ImPlot::PlotHistogram` con seleccion automatica del numero de bins.
Cuando `bins == -1` usa `ImPlotBin_Sturges`, que calcula el numero de bins como `ceil(log2(n)) + 1`. Para distribuciones con muchos valores o alta varianza puede preferirse pasar un valor explicito.
El plot usa `ImVec2(-1, 0)` para ocupar el ancho disponible con altura automatica.
Debe llamarse dentro del render callback de `fn::run_app`.
+44
View File
@@ -0,0 +1,44 @@
#include "kpi_card.h"
#include "sparkline.h"
#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) {
ImGui::BeginGroup();
// Label — small, muted
ImGui::TextDisabled("%s", label);
// Value — scaled up font
ImGui::SetWindowFontScale(1.8f);
char value_buf[64];
snprintf(value_buf, sizeof(value_buf), format, value);
ImGui::Text("%s", value_buf);
ImGui::SetWindowFontScale(1.0f);
// Delta badge — green up arrow / red down arrow
const bool positive = delta_percent >= 0.0f;
const ImVec4 delta_color = positive
? ImVec4(0.20f, 0.80f, 0.35f, 1.0f) // green
: ImVec4(0.90f, 0.25f, 0.25f, 1.0f); // red
char delta_buf[32];
if (positive) {
snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent);
} else {
snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent);
}
ImGui::PushStyleColor(ImGuiCol_Text, delta_color);
ImGui::Text("%s", delta_buf);
ImGui::PopStyleColor();
// Sparkline — matches delta color
if (history != nullptr && history_count > 0) {
sparkline(label, history, history_count, delta_color, 120.0f, 24.0f);
}
ImGui::EndGroup();
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
// KPI card — displays a key metric with trend.
// Usage:
// float history[] = {10, 12, 11, 15, 18, 17, 20};
// kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f");
//
// Shows:
// - Label (small, muted)
// - Value (large font)
// - Delta badge (green up / red down)
// - Sparkline of history
void kpi_card(const char* label, float value, float delta_percent,
const float* history = nullptr, int history_count = 0,
const char* format = "%.1f");
+71
View File
@@ -0,0 +1,71 @@
---
name: kpi_card
kind: component
lang: cpp
domain: viz
version: "1.0.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\")"
description: "Card de KPI con valor grande, delta porcentual, y sparkline historico para dashboards"
tags: [imgui, kpi, card, dashboard, metrics, sparkline]
uses_functions: ["sparkline_cpp_viz"]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/kpi_card.cpp"
framework: imgui
params:
- name: label
desc: "Nombre del KPI mostrado como header muted (ej: \"Revenue\", \"Users\")"
- name: value
desc: "Valor numerico actual del KPI"
- name: delta_percent
desc: "Cambio porcentual respecto al periodo anterior (positivo = mejora, negativo = deterioro)"
- name: history
desc: "Array de valores historicos para el sparkline. Nullable — si es nullptr no se renderiza sparkline"
- name: history_count
desc: "Numero de valores en el array history"
- name: format
desc: "Formato printf para el valor principal (ej: \"$%.0f\", \"%.1f%%\", \"%.2f\")"
output: "Renderiza la card KPI completa en el frame ImGui actual: label muted, valor grande, badge delta verde/rojo con triangulo, y sparkline de 120x24px"
---
# kpi_card
Card compacta para dashboards ImGui que muestra un KPI con contexto de tendencia. Combina label, valor escalado, badge de delta colorizado y sparkline historico en un grupo coherente de ~150px de ancho.
Usa `sparkline` del registry para el historico, con el mismo color que el badge (verde si delta >= 0, rojo si delta < 0).
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
float history[] = {10.0f, 12.0f, 11.0f, 15.0f, 18.0f, 17.0f, 20.0f};
kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f");
// Sin sparkline
kpi_card("Error Rate", 0.3f, -15.2f, nullptr, 0, "%.2f%%");
// Grid de KPIs
ImGui::Columns(3, "kpis", false);
kpi_card("MAU", 1250000.0f, 3.4f, mau_history, 30);
ImGui::NextColumn();
kpi_card("Revenue", 89400.0f, -1.2f, rev_history, 30, "$%.0f");
ImGui::NextColumn();
kpi_card("Churn", 2.1f, -0.3f, churn_history, 30, "%.1f%%");
ImGui::Columns(1);
```
## Notas
- El ancho total del grupo es aproximadamente 150px, apto para grids de 2-4 columnas.
- El escalado de fuente usa `SetWindowFontScale(1.8f)` — compatible con cualquier fuente cargada; no requiere fonts adicionales.
- Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) requieren que la fuente ImGui tenga el rango de simbolos geometricos cargado, o bien sustituir por ASCII (`^` y `v`).
- El color verde del delta es `ImVec4(0.20, 0.80, 0.35, 1.0)` y el rojo `ImVec4(0.90, 0.25, 0.25, 1.0)`, coherentes con los colores del sparkline subyacente.
- `BeginGroup`/`EndGroup` permite usar `SameLine()` despues de `kpi_card` y que el cursor avance correctamente.
+31
View File
@@ -0,0 +1,31 @@
#include "viz/pie_chart.h"
#include "implot.h"
void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxesLimits(0, 1, 0, 1);
if (radius < 0.0f) {
// Donut mode: outer = |radius|, inner = 0.2
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0, ImPlotPieChartFlags_None);
} else {
float r = (radius > 0.0f) ? radius : 0.4f;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0, ImPlotPieChartFlags_None);
}
ImPlot::EndPlot();
}
}
void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_Equal | ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::SetupAxesLimits(0, 1, 0, 1);
if (radius < 0.0) {
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, -radius, "%.1f", 90.0, ImPlotPieChartFlags_None);
} else {
double r = (radius > 0.0) ? radius : 0.4;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0, ImPlotPieChartFlags_None);
}
ImPlot::EndPlot();
}
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
// Renders a pie or donut chart using ImPlot::PlotPieChart.
// Call within an ImGui frame.
// radius == 0: auto (0.4). radius > 0: explicit radius. radius < 0: donut mode (|radius| as outer, 0.2 as inner).
void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f);
void pie_chart(const char* title, const char* const* labels, const double* values, int count, double radius = 0.0);
+46
View File
@@ -0,0 +1,46 @@
---
name: pie_chart
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f)"
description: "Renderiza un grafico circular (pie/donut) usando ImPlot PlotPieChart dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, pie, donut]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/pie_chart.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico"
- name: labels
desc: "Array de etiquetas para cada segmento del pie"
- name: values
desc: "Array de valores numericos para cada segmento"
- name: count
desc: "Numero de segmentos (longitud de labels y values)"
- name: radius
desc: "Radio del pie (0 = auto 0.4). Positivo = radio explicito. Negativo = modo donut con outer radius = |radius| e inner = 0.2"
output: "Renderiza el grafico circular en el frame ImGui actual"
---
# pie_chart
Wrapper atomico sobre `ImPlot::PlotPieChart` con soporte para modo pie y modo donut.
El eje del plot se configura con `ImPlotAxisFlags_NoDecorations` para ocultar los ejes y mostrar solo el grafico circular. El aspecto se fuerza a cuadrado con `ImPlotFlags_Equal`.
**Modo pie** (`radius >= 0`): dibuja un pie chart solido. Si `radius == 0`, usa radio automatico de 0.4.
**Modo donut** (`radius < 0`): usa `|radius|` como radio exterior. El agujero interior es fijo en 0.2, suficiente para texto central.
Debe llamarse dentro del render callback de `fn::run_app`.
+76
View File
@@ -0,0 +1,76 @@
#include "viz/sparkline.h"
#include "imgui.h"
void sparkline(const char* id, const float* values, int count, ImVec4 color,
float width, float height) {
if (count <= 0) return;
ImGui::PushID(id);
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];
}
float range = max_val - min_val;
if (range < 1e-6f) range = 1.0f; // avoid division by zero for flat lines
// 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 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;
draw_list->AddQuadFilled(
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 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;
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,
float width, float height) {
// Default color: soft green
sparkline(id, values, count, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), width, height);
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#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.
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);
+69
View File
@@ -0,0 +1,69 @@
---
name: sparkline
kind: component
lang: cpp
domain: viz
version: "1.0.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]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/sparkline.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico del widget, usado con PushID/PopID para garantizar unicidad en tablas"
- name: values
desc: "Array de valores float del sparkline (serie temporal)"
- name: count
desc: "Numero de valores en el array"
- name: width
desc: "Ancho en pixels del sparkline (default 100.0)"
- name: height
desc: "Alto en pixels del sparkline (default 20.0)"
output: "Renderiza el sparkline inline en el frame ImGui actual, reservando espacio con ImGui::Dummy"
---
# sparkline
Mini grafico de lineas inline construido sobre ImGui draw primitives. No requiere ImPlot.
Auto-escala el eje Y al rango minimo/maximo de los valores. Dibuja una polyline con relleno semitransparente bajo la curva. Disenado para encajar en celdas de tablas, headers y tarjetas KPI.
Ofrece dos overloads:
- Sin color: usa verde suave por defecto (`ImVec4(0.35, 0.85, 0.45, 1.0)`)
- Con color: acepta cualquier `ImVec4` para personalizar la linea y el relleno
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
// En una celda de tabla
ImGui::TableNextColumn();
sparkline("##revenue_spark", revenue.data(), (int)revenue.size(), 80.0f, 18.0f);
// Con color personalizado (rojo para valores negativos)
sparkline("##pnl", pnl.data(), (int)pnl.size(),
ImVec4(0.9f, 0.3f, 0.3f, 1.0f), 100.0f, 20.0f);
// KPI card inline con label
ImGui::Text("Revenue"); ImGui::SameLine();
sparkline("kpi_rev", data, count);
```
## Notas
- El relleno bajo la curva usa alpha 40/255 del mismo color de la linea.
- 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.
+12
View File
@@ -0,0 +1,12 @@
#include "viz/surface_plot_3d.h"
#include "imgui.h"
void surface_plot_3d(const char* title, const float* values, int rows, int cols,
float z_min, float z_max) {
ImGui::BeginGroup();
ImGui::TextDisabled("[STUB] %s", title);
ImGui::TextWrapped("surface_plot_3d requires ImPlot3D. "
"Add it to cpp/vendor/implot3d/ and rebuild.");
ImGui::Text("Data: %dx%d, range [%.2f, %.2f]", rows, cols, z_min, z_max);
ImGui::EndGroup();
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
// [STUB] Renders a 3D surface plot using ImPlot3D.
// Requires ImPlot3D to be vendored in cpp/vendor/implot3d/.
// Until then, displays a placeholder message inside an ImGui group.
// Call within an ImGui frame (inside fn::run_app render callback).
void surface_plot_3d(const char* title, const float* values, int rows, int cols,
float z_min, float z_max);
+61
View File
@@ -0,0 +1,61 @@
---
name: surface_plot_3d
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void surface_plot_3d(const char* title, const float* values, int rows, int cols, float z_min, float z_max)"
description: "[STUB] Renderiza una superficie 3D — requiere ImPlot3D (no vendoreado aun)"
tags: [implot3d, chart, visualization, gpu, surface, 3d, stub]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/surface_plot_3d.cpp"
framework: imgui
params:
- name: title
desc: "Titulo de la superficie, se muestra como header del plot"
- name: values
desc: "Array row-major de alturas Z con dimension rows*cols"
- name: rows
desc: "Numero de filas de la grilla de la superficie"
- name: cols
desc: "Numero de columnas de la grilla de la superficie"
- name: z_min
desc: "Valor minimo del eje Z para escalar el colormap"
- name: z_max
desc: "Valor maximo del eje Z para escalar el colormap"
output: "Renderiza un placeholder informativo en el frame ImGui actual; cuando ImPlot3D este disponible, renderizara la superficie 3D"
---
# surface_plot_3d
**STUB** — la implementacion real requiere [ImPlot3D](https://github.com/brenocq/implot3d), que todavia no esta vendoreado en `cpp/vendor/implot3d/`.
Mientras tanto la funcion renderiza un grupo ImGui con un mensaje informativo que muestra el titulo, las dimensiones de la grilla y el rango Z. La firma es definitiva y no cambiara cuando se integre ImPlot3D.
## Dependencia pendiente
Para activar la implementacion real:
1. Clonar o copiar ImPlot3D en `cpp/vendor/implot3d/`
2. Anadir `implot3d.cpp` al build system (CMake / Makefile)
3. Reemplazar el cuerpo de `surface_plot_3d` por la llamada a `ImPlot3D::BeginPlot3D` / `ImPlot3D::PlotSurface` / `ImPlot3D::EndPlot3D`
4. Actualizar `imports` del .md a `[imgui, implot3d]` y quitar el tag `stub`
## Uso
```cpp
// values es un array row-major de rows*cols floats
float grid[4 * 4] = { ... };
surface_plot_3d("Mi Superficie", grid, 4, 4, -1.0f, 1.0f);
```
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
+32
View File
@@ -0,0 +1,32 @@
#include "viz/table_view.h"
#include "imgui.h"
bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count) {
ImGuiTableFlags flags =
ImGuiTableFlags_Borders |
ImGuiTableFlags_Sortable |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY |
ImGuiTableFlags_Reorderable;
if (!ImGui::BeginTable(id, col_count, flags, ImVec2(0, 300))) {
return false;
}
for (int col = 0; col < col_count; col++) {
ImGui::TableSetupColumn(headers[col]);
}
ImGui::TableHeadersRow();
for (int row = 0; row < row_count; row++) {
ImGui::TableNextRow();
for (int col = 0; col < col_count; col++) {
ImGui::TableSetColumnIndex(col);
ImGui::TextUnformatted(cells[row * col_count + col]);
}
}
ImGui::EndTable();
return true;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Renders an interactive table with sorting indicators and scroll using the ImGui Tables API.
// Call within an ImGui frame.
// Returns true if the table was rendered visible, false if clipped/skipped.
bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count);
+67
View File
@@ -0,0 +1,67 @@
---
name: table_view
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "bool table_view(const char* id, const char* const* headers, int col_count, const char* const* cells, int row_count)"
description: "Renderiza una tabla interactiva con sorting y scroll usando ImGui Tables API"
tags: [imgui, table, visualization, dashboard, data]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/table_view.cpp"
framework: imgui
params:
- name: id
desc: "Identificador unico de la tabla para ImGui (debe ser unico en el frame)"
- name: headers
desc: "Array de strings con los nombres de las columnas"
- name: col_count
desc: "Numero de columnas"
- name: cells
desc: "Array flat row-major de strings; acceso a celda (row, col) via cells[row * col_count + col]"
- name: row_count
desc: "Numero de filas de datos, sin contar el header"
output: "true si la tabla se renderizo visible, false si fue clipped o skipped por ImGui"
---
# table_view
Wrapper atomico sobre `ImGui::BeginTable` / `ImGui::EndTable`. Renderiza una tabla con las siguientes capacidades:
- **Borders**: bordes entre celdas y columnas
- **Sortable**: muestra indicadores de orden en los headers (el caller es responsable de ordenar `cells` antes de llamar)
- **RowBg**: filas alternadas con color de fondo
- **Resizable**: el usuario puede arrastrar los separadores de columna
- **ScrollY**: scroll vertical con altura fija de 300px
- **Reorderable**: el usuario puede reordenar columnas arrastrando los headers
El caller controla el orden de los datos — `table_view` solo habilita el flag `Sortable` para que ImGui muestre los indicadores visuales, pero no reordena `cells` internamente.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
## Ejemplo
```cpp
const char* headers[] = {"Nombre", "Valor", "Estado"};
const char* cells[] = {
"Alpha", "1.23", "OK",
"Beta", "4.56", "WARN",
"Gamma", "7.89", "ERROR",
};
table_view("##mi_tabla", headers, 3, cells, 3);
```
## Notas
- `id` debe comenzar con `##` si no se quiere mostrar como titulo de ventana en el contexto ImGui.
- El outer size fijo de `ImVec2(0, 300)` puede parametrizarse en una version futura.
- El sorting real de datos queda fuera del scope de esta funcion para mantenerla pura y componible.
+106
View File
@@ -0,0 +1,106 @@
---
name: GraphData
lang: cpp
domain: viz
version: "1.0.0"
algebraic: product
definition: |
struct GraphNode {
uint32_t id;
float x, y;
float vx, vy;
float size;
uint32_t color;
const char* label;
uint32_t community;
float value;
bool pinned;
};
struct GraphEdge {
uint32_t source;
uint32_t target;
float weight;
uint32_t color;
};
struct GraphData {
GraphNode* nodes;
int node_count;
GraphEdge* edges;
int edge_count;
float min_x, min_y, max_x, max_y;
void update_bounds();
int find_node(uint32_t id) const;
};
description: "Tipos de datos base para el sistema de grafos GPU del registry. GraphNode modela un vertice con posicion, velocidad, apariencia y metadatos de layout. GraphEdge modela una arista con peso y color. GraphData es el contenedor principal que agrupa nodos y aristas con bounding box y metodos de consulta. Disenado para integrarse con force-directed layout y renderizado GPU via ImGui/ImPlot."
tags: [graph, network, visualization, gpu, force-layout, node, edge, imgui]
uses_types: []
file_path: "cpp/functions/viz/graph_types.h"
---
## Structs
### GraphNode
Vertice del grafo. Contiene todos los datos necesarios para el layout y el renderizado.
| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | `uint32_t` | Identificador unico del nodo |
| `x, y` | `float` | Posicion en el espacio de layout |
| `vx, vy` | `float` | Velocidad del nodo, usada por el algoritmo force-directed |
| `size` | `float` | Radio visual en pixels (por defecto 4.0) |
| `color` | `uint32_t` | Color en formato ABGR packed. 0 = usar paleta automatica por community |
| `label` | `const char*` | Etiqueta visible. `nullptr` = sin etiqueta |
| `community` | `uint32_t` | ID de grupo/cluster para auto-coloreo. 0 = sin grupo |
| `value` | `float` | Metrica arbitraria (puede usarse para escalar el tamaño del nodo) |
| `pinned` | `bool` | Si es `true`, el force layout no mueve este nodo |
### GraphEdge
Arista del grafo. Referencia nodos por indice (no por id) para acceso O(1) en el loop de simulacion.
| Campo | Tipo | Descripcion |
|---|---|---|
| `source` | `uint32_t` | Indice en `GraphData::nodes` del nodo origen |
| `target` | `uint32_t` | Indice en `GraphData::nodes` del nodo destino |
| `weight` | `float` | Peso de la arista. Afecta la fuerza de atraccion en el layout |
| `color` | `uint32_t` | Color ABGR packed. 0 = gris por defecto |
### GraphData
Contenedor principal. Posee los arrays de nodos y aristas (memoria gestionada externamente). Mantiene un bounding box actualizable para proyeccion de coordenadas a pantalla.
| Campo/Metodo | Descripcion |
|---|---|
| `nodes` / `node_count` | Array de nodos y su longitud |
| `edges` / `edge_count` | Array de aristas y su longitud |
| `min_x, min_y, max_x, max_y` | Bounding box calculado sobre las posiciones actuales |
| `update_bounds()` | Recalcula el bounding box iterando todos los nodos |
| `find_node(id)` | Busqueda lineal por `GraphNode::id`. Retorna -1 si no existe |
## Helpers
```cpp
// Crear un nodo con valores por defecto
GraphNode n = graph_node(42, 100.0f, 200.0f);
// Crear una arista con peso por defecto 1.0
GraphEdge e = graph_edge(0, 1, 2.5f);
```
## Implementacion
Los metodos `update_bounds()` y `find_node()` estan implementados en `cpp/functions/viz/graph_types.cpp`.
`update_bounds()` es O(n) sobre `node_count`. Llamar despues de cada step del layout para mantener el bounding box fresco.
`find_node()` es O(n) por busqueda lineal. Para grafos grandes (>10k nodos) considerar mantener un `unordered_map<uint32_t, int>` externo como indice.
## Notas de diseño
- La memoria de `nodes` y `edges` es propiedad del caller. `GraphData` no hace `new`/`delete`.
- `color` usa formato ABGR packed (compatible con ImGui `ImU32`): `0xAABBGGRR`.
- Las aristas referencian por indice, no por id, para que el loop de simulacion sea cache-friendly.
- `community` con valor 0 se interpreta como "sin grupo" — los colores de comunidad empiezan desde 1.