From 3f622561ce71b66a168beb5716adea848982dbae Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 24 Apr 2026 21:31:00 +0200 Subject: [PATCH] feat(cpp/viz): static-plot primitive + tooltips + rotated labels + card compacta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevo primitivo compartido: - cpp/functions/viz/plot_static.h: header-only con flags ImPlotFlags / ImPlotAxisFlags agrupados (NoFrame|NoMenus|NoBoxSelect|NoMouseText + Lock|NoInitialFit|NoHighlight) para visualizacion estatica en dashboards. Lo usan todos los charts de viz/. Charts refactorizados a v1.1 con parametro `height` explicito (rompe el feedback loop con contenedores AutoResizeY que producia vibracion al redimensionar) y ejes pineados con ImPlotCond_Always: - bar_chart v1.2: tooltip al hover (label + valor) + auto-rotacion de labels a 45 cuando no caben horizontalmente (medidos con CalcTextSize vs ancho del plot). Los labels rotados se dibujan manualmente con ImDrawList::PrimQuadUV + ImFontBaked::FindGlyph (API ImGui 1.92+). - pie_chart v1.1: tooltip por slice (detecta cual via atan2 desde centro en sentido CCW matematico, que es como ImPlot dibuja los slices desde angle0=90) con label + valor + porcentaje. Aspect 1:1 mantenido. - line_plot, scatter_plot, histogram v1.1: ejes pineados con limites calculados de min/max + 5% headroom (histogram usa AutoFit por los bins dinamicos, con Lock para bloquear pan/zoom). kpi_card v1.2: card mas compacta — altura 78px (antes 108), font scale 1.4x (antes 1.8x), padding sm (antes md). Apto para densidades altas de KPIs en dashboards. fullscreen_window v0.2: NoScrollbar|NoScrollWithMouse para eliminar el scrollbar fugaz que aparecia cuando el contenido excedia por 1-2px la ventana, reflow de ancho y vibracion visible al redimensionar. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/functions/core/fullscreen_window.cpp | 7 +- cpp/functions/core/fullscreen_window.md | 7 +- cpp/functions/viz/bar_chart.cpp | 159 +++++++++++++++++++---- cpp/functions/viz/bar_chart.md | 33 +++-- cpp/functions/viz/histogram.cpp | 36 +++-- cpp/functions/viz/histogram.h | 10 +- cpp/functions/viz/histogram.md | 22 ++-- cpp/functions/viz/kpi_card.cpp | 19 ++- cpp/functions/viz/kpi_card.md | 10 +- cpp/functions/viz/line_plot.cpp | 42 +++++- cpp/functions/viz/line_plot.h | 12 +- cpp/functions/viz/line_plot.md | 24 ++-- cpp/functions/viz/pie_chart.cpp | 109 +++++++++++++--- cpp/functions/viz/pie_chart.h | 15 ++- cpp/functions/viz/pie_chart.md | 34 +++-- cpp/functions/viz/plot_static.h | 56 ++++++++ cpp/functions/viz/plot_static.md | 62 +++++++++ cpp/functions/viz/scatter_plot.cpp | 47 ++++++- cpp/functions/viz/scatter_plot.h | 10 +- cpp/functions/viz/scatter_plot.md | 24 ++-- 20 files changed, 582 insertions(+), 156 deletions(-) create mode 100644 cpp/functions/viz/plot_static.h create mode 100644 cpp/functions/viz/plot_static.md diff --git a/cpp/functions/core/fullscreen_window.cpp b/cpp/functions/core/fullscreen_window.cpp index 6581e1fe..738e4cac 100644 --- a/cpp/functions/core/fullscreen_window.cpp +++ b/cpp/functions/core/fullscreen_window.cpp @@ -11,7 +11,12 @@ bool fullscreen_window_begin(const char* id) { ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus); + ImGuiWindowFlags_NoNavFocus | + // NoScrollbar evita que aparezca un scrollbar fugaz cuando el + // contenido excede la ventana por 1-2px, lo que provocaria un + // reflow del ancho y vibracion visible al redimensionar. + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse); } void fullscreen_window_end() { diff --git a/cpp/functions/core/fullscreen_window.md b/cpp/functions/core/fullscreen_window.md index 169c7aeb..ecdda5d9 100644 --- a/cpp/functions/core/fullscreen_window.md +++ b/cpp/functions/core/fullscreen_window.md @@ -3,7 +3,7 @@ name: fullscreen_window kind: component lang: cpp domain: core -version: "0.1.0" +version: "0.2.0" purity: pure signature: "bool fullscreen_window_begin(const char* id = \"##fullscreen\"); void fullscreen_window_end()" description: "Ventana ImGui fullscreen sin decoraciones que ocupa todo el viewport, elimina la necesidad de usar el sistema de ventanas interno" @@ -55,8 +55,9 @@ fullscreen_window_end(); - `GetMainViewport()` obtiene el viewport principal (compatible con viewports multi-monitor de ImGui) - `SetNextWindowPos(vp->WorkPos)` posiciona en el area de trabajo (excluye menu bars del OS) - `SetNextWindowSize(vp->WorkSize)` ocupa exactamente el area disponible -- Flags: `NoTitleBar | NoResize | NoMove | NoCollapse | NoBringToFrontOnFocus | NoNavFocus` -- `NoBringToFrontOnFocus` y `NoNavFocus` evitan que la ventana fullscreen robe el foco de ventanas superpuestas +- Flags: `NoTitleBar | NoResize | NoMove | NoCollapse | NoBringToFrontOnFocus | NoNavFocus | NoScrollbar | NoScrollWithMouse` +- `NoBringToFrontOnFocus` y `NoNavFocus` evitan que la ventana fullscreen robe el foco de ventanas superpuestas. +- **v0.2**: `NoScrollbar | NoScrollWithMouse` evitan que aparezca un scrollbar fugaz cuando el contenido excede la ventana por 1-2px (problema tipico con altura dinamica basada en `GetContentRegionAvail`). Sin esto, durante un resize el scrollbar puede aparecer/desaparecer y el ancho del contenido reflows → vibracion visible en dashboards con charts ImPlot. ## Notas diff --git a/cpp/functions/viz/bar_chart.cpp b/cpp/functions/viz/bar_chart.cpp index 989a6985..7b13b0f0 100644 --- a/cpp/functions/viz/bar_chart.cpp +++ b/cpp/functions/viz/bar_chart.cpp @@ -1,20 +1,86 @@ #include "viz/bar_chart.h" +#include "viz/plot_static.h" +#include "imgui.h" +#include "imgui_internal.h" #include "implot.h" +#include #include namespace { -// Render bars con ejes fijos y tamano de plot explicito. Dos cosas clave: -// -// 1) Limites pineados con ImPlotCond_Always + Lock: ImPlot por defecto -// auto-fitea Y cada frame, lo que provoca oscilacion visual. -// -// 2) Altura explicita (height > 0 -> ImVec2(-1, height)): sin esto, ImPlot -// toma el espacio disponible del contenedor. Si el contenedor es un -// BeginChild con AutoResizeY (como dashboard_panel), aparece un feedback -// loop: plot pide espacio al padre -> padre se redimensiona al plot -> -// plot recalcula -> las barras "se deslizan" en los primeros frames. +constexpr float kPI = 3.14159265358979323846f; + +// Dibuja `text` rotado `angle` rad alrededor de `pivot` usando el font atlas +// actual. Origen = primer glyph anclado en pivot; el texto avanza en la +// direccion (cos(angle), sin(angle)) en coords de pantalla (Y abajo). +void draw_text_rotated(ImDrawList* dl, ImFont* font, float font_size, + ImVec2 pivot, float angle, ImU32 col, const char* text) { + if (!text || !*text) return; + // En ImGui >= 1.92 los glyphs viven en ImFontBaked, no en ImFont. Hay + // que pedir el baked al tamano deseado y usar sus coords (ya en px). + ImFontBaked* baked = font->GetFontBaked(font_size); + if (!baked) return; + + const float cos_a = std::cos(angle); + const float sin_a = std::sin(angle); + + dl->PushTexture(font->OwnerAtlas->TexRef); + + float pen_x = 0.0f; + for (const char* p = text; *p; ) { + unsigned int c = 0; + int len = ImTextCharFromUtf8(&c, p, nullptr); + if (len <= 0) break; + p += len; + if (c < 0x20) continue; + + ImFontGlyph* g = baked->FindGlyph(static_cast(c)); + if (!g) continue; + + if (g->Visible) { + const float x0 = pen_x + g->X0; + const float y0 = g->Y0; + const float x1 = pen_x + g->X1; + const float y1 = g->Y1; + + auto rot = [&](float x, float y) -> ImVec2 { + return ImVec2(pivot.x + x * cos_a - y * sin_a, + pivot.y + x * sin_a + y * cos_a); + }; + + const ImVec2 p0 = rot(x0, y0); + const ImVec2 p1 = rot(x1, y0); + const ImVec2 p2 = rot(x1, y1); + const ImVec2 p3 = rot(x0, y1); + + dl->PrimReserve(6, 4); + dl->PrimQuadUV(p0, p1, p2, p3, + ImVec2(g->U0, g->V0), ImVec2(g->U1, g->V0), + ImVec2(g->U1, g->V1), ImVec2(g->U0, g->V1), + col); + } + pen_x += g->AdvanceX; + } + + dl->PopTexture(); +} + +// Mide el ancho total de los labels como si fueran horizontales + padding, +// y decide si caben en `plot_width`. Si no caben, el chart los rotara 45 +// grados en vez de mostrarlos horizontales (ver draw_bars). +bool labels_need_rotation(const char* const* labels, int count, float plot_width) { + if (count <= 1 || plot_width <= 1.0f) return false; + float total = 0.0f; + constexpr float pad = 12.0f; + for (int i = 0; i < count; i++) { + total += ImGui::CalcTextSize(labels[i]).x + pad; + } + return total > plot_width; +} + +// Barras verticales con ejes pineados, altura explicita, tooltip al pasar el +// mouse y labels rotados a 45 grados cuando no caben horizontalmente. template void draw_bars(const char* title, const char* const* labels, const T* values, int count, double bar_width, float height) { @@ -25,34 +91,73 @@ void draw_bars(const char* title, const char* const* labels, const T* values, if (static_cast(values[i]) > y_max) y_max = static_cast(values[i]); } if (y_max <= 0.0) y_max = 1.0; - y_max *= 1.15; // 15% headroom sobre la barra mas alta + y_max *= 1.15; - const ImPlotFlags plot_flags = - ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMouseText - | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame; + const float hint_width = ImGui::GetContentRegionAvail().x; + const bool rotate = labels_need_rotation(labels, count, hint_width); - const ImPlotAxisFlags x_flags = - ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock - | ImPlotAxisFlags_NoInitialFit | ImPlotAxisFlags_NoGridLines - | ImPlotAxisFlags_NoHighlight; + // Reservamos ~48px abajo para los labels rotados (aprox 34px verticales + // + padding). Sin rotacion, el plot ocupa toda la altura disponible. + const float total_h = height > 0.0f ? height : 200.0f; + const float label_reserve = rotate ? 48.0f : 0.0f; + const ImVec2 plot_size(-1.0f, total_h - label_reserve); - const ImPlotAxisFlags y_flags = - ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock - | ImPlotAxisFlags_NoInitialFit | ImPlotAxisFlags_NoHighlight; + std::vector positions(count); + for (int i = 0; i < count; i++) positions[i] = i; - const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f); - - if (ImPlot::BeginPlot(title, plot_size, plot_flags)) { - std::vector positions(count); - for (int i = 0; i < count; i++) positions[i] = i; + if (ImPlot::BeginPlot(title, plot_size, plot_static::kPlotFlags)) { + const ImPlotAxisFlags x_flags = plot_static::kAxisFlags | ImPlotAxisFlags_NoGridLines; + const ImPlotAxisFlags y_flags = plot_static::kAxisFlags; ImPlot::SetupAxes(nullptr, nullptr, x_flags, y_flags); ImPlot::SetupAxisLimits(ImAxis_X1, -0.5, static_cast(count) - 0.5, ImPlotCond_Always); ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, y_max, ImPlotCond_Always); - ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels); + + if (rotate) { + // Tick positions con labels vacios: ImPlot dibuja ticks pero no + // texto. Los labels los dibujamos a mano rotados mas abajo. + std::vector empty(count, ""); + ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, empty.data()); + } else { + ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels); + } ImPlot::PlotBars("##data", values, count, bar_width); + + // Tooltip + if (ImPlot::IsPlotHovered()) { + ImPlotPoint mp = ImPlot::GetPlotMousePos(); + int idx = static_cast(std::round(mp.x)); + if (idx >= 0 && idx < count + && std::fabs(mp.x - idx) <= bar_width * 0.5) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(labels[idx]); + ImGui::Separator(); + ImGui::Text("%.0f", static_cast(values[idx])); + ImGui::EndTooltip(); + } + } + + // Labels rotados: solo si decidimos rotar. Los dibujamos usando el + // drawlist de la VENTANA (no el del plot) para que no se clipen al + // rectangulo del plot y puedan extenderse en el margen inferior. + if (rotate) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImFont* font = ImGui::GetFont(); + const float font_size = ImGui::GetFontSize(); + const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text); + const float angle = kPI * 0.25f; // 45 grados en pantalla (Y abajo) + + for (int i = 0; i < count; i++) { + ImVec2 px = ImPlot::PlotToPixels(ImPlotPoint(static_cast(i), 0.0)); + // Desplazamos el pivote un pelin abajo del eje X para que el + // label no pise los ticks. + ImVec2 pivot(px.x, px.y + 6.0f); + draw_text_rotated(dl, font, font_size, pivot, angle, col, labels[i]); + } + } + ImPlot::EndPlot(); } } diff --git a/cpp/functions/viz/bar_chart.md b/cpp/functions/viz/bar_chart.md index fef696d8..a5c096fa 100644 --- a/cpp/functions/viz/bar_chart.md +++ b/cpp/functions/viz/bar_chart.md @@ -3,11 +3,11 @@ name: bar_chart kind: component lang: cpp domain: viz -version: "1.1.0" +version: "1.2.0" purity: pure signature: "void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width = 0.67f, float height = 200.0f)" -description: "Renderiza un grafico de barras verticales con ImPlot, ejes pineados (Lock + Cond_Always) y altura explicita para evitar feedback loops visuales" -tags: [implot, chart, visualization, gpu, bar, locked-axes] +description: "Barras verticales ImPlot con ejes pineados, altura explicita, tooltip al hover y auto-rotacion 45 de labels cuando no caben horizontales" +tags: [implot, chart, visualization, gpu, bar, tooltip, rotated-labels, locked-axes] uses_functions: [] uses_types: [] returns: [] @@ -21,36 +21,35 @@ file_path: "cpp/functions/viz/bar_chart.cpp" framework: imgui params: - name: title - desc: "Titulo del grafico de barras (tambien se usa como id interno del plot)" + desc: "Titulo / id interno del plot" - name: labels desc: "Array de etiquetas para el eje X, una por barra" - name: values - desc: "Array de valores numericos para la altura de cada barra" + desc: "Array de valores numericos (altura de cada barra)" - name: count desc: "Numero de barras (longitud de labels y values)" - name: bar_width desc: "Ancho de cada barra como fraccion del hueco de celda (default 0.67)" - name: height - desc: "Altura del plot en pixeles (default 200). Explicita para evitar feedback loops con contenedores AutoResizeY" -output: "Renderiza el grafico de barras en el frame ImGui actual con ejes X/Y pineados" + desc: "Altura del plot en pixeles (default 200). Explicita para evitar feedback loops con AutoResizeY" +output: "Renderiza barras, tooltip al hover con label+valor, y si los labels horizontales no caben los dibuja rotados 45 grados" --- # bar_chart -Barras verticales con ImPlot, pensado para dashboards estaticos (resumenes, KPIs). Diseno: +Barras verticales ImPlot pensadas para dashboards. Tres cosas no triviales: -- **Ejes pineados** — `ImPlotCond_Always` + `ImPlotAxisFlags_Lock` + `ImPlotAxisFlags_NoInitialFit`. Sin esto, ImPlot auto-fitea cada frame y las barras oscilan los primeros frames. -- **Y max con 15% de headroom** — calculado a partir de `values` una vez, mas estetico que el fit apretado de ImPlot. -- **Altura explicita** — `ImVec2(-1, height)` en vez de `ImVec2(-1, 0)`. Dentro de un `dashboard_panel` (BeginChild con AutoResizeY), un height=0 crea un feedback loop: el plot pide espacio al padre, el padre se redimensiona al plot, el plot recalcula y asi. El efecto visual es que las barras se deslizan de lado al abrir la ventana. Con height fija no hay loop. -- **Sin inputs** — `ImPlotFlags_NoInputs | NoFrame | NoBoxSelect | NoMouseText`. Pensado para visualizacion, no exploracion. +1. **Ejes pineados** — `plot_static::kPlotFlags` + `kAxisFlags` (Lock + NoInitialFit + Cond_Always) con `y_max` pre-calculado + 15% headroom. Sin esto ImPlot auto-fitea cada frame y las barras oscilan. +2. **Tooltip** — si `IsPlotHovered()`, detecta la barra bajo el cursor (`round(mouse.x)` con tolerancia `bar_width/2`) y muestra `label` + valor. +3. **Labels auto-rotados** — mide la suma de `CalcTextSize(label) + 12px` contra el ancho del plot; si no caben, dibuja los labels rotados 45° manualmente con `ImDrawList::PrimQuadUV` + glyphs del font atlas (`ImFontBaked::FindGlyph` — API ImGui 1.92+). Reserva 48px abajo del plot para los labels rotados. Si caben se usan los ticks horizontales normales de ImPlot. -Debe llamarse dentro del render callback de `fn::run_app`. +Altura explicita (`height`) rompe el feedback loop con contenedores `AutoResizeY` (ver `viz/plot_static.h`). ## Ejemplo ```cpp -const char* langs[] = {"go", "py", "ts", "sh", "cpp"}; -float counts[] = {412, 187, 94, 63, 36}; -bar_chart("Functions by lang", langs, counts, 5); // height por defecto 200 -bar_chart("Compact", langs, counts, 5, 0.8f, 120.0f); // compacto +const char* domains[] = {"core", "finance", "cybersecurity", "datascience", "infra"}; +float counts[] = {412, 187, 94, 63, 36}; +bar_chart("##domains", domains, counts, 5); // horizontal si cabe +bar_chart("##domains", domains, counts, 5, 0.8f, 240); // rotated si no cabe ``` diff --git a/cpp/functions/viz/histogram.cpp b/cpp/functions/viz/histogram.cpp index 3665fb8f..21702745 100644 --- a/cpp/functions/viz/histogram.cpp +++ b/cpp/functions/viz/histogram.cpp @@ -1,18 +1,36 @@ #include "viz/histogram.h" +#include "viz/plot_static.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; +namespace { + +template +void draw_hist(const char* title, const T* values, int count, int bins, float height) { + if (count <= 0) return; + + int b = (bins > 0) ? bins : ImPlotBin_Sturges; + const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f); + + // PlotHistogram necesita auto-fit para decidir limites segun los bins + // calculados, asi que en histograma permitimos el primer fit pero + // bloqueamos pan/zoom posterior via Lock. + const ImPlotAxisFlags axis_flags = + ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock + | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_AutoFit; + + if (ImPlot::BeginPlot(title, plot_size, plot_static::kPlotFlags)) { + ImPlot::SetupAxes(nullptr, nullptr, axis_flags, axis_flags); 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(); - } +} // namespace + +void histogram(const char* title, const float* values, int count, int bins, float height) { + draw_hist(title, values, count, bins, height); +} + +void histogram(const char* title, const double* values, int count, int bins, float height) { + draw_hist(title, values, count, bins, height); } diff --git a/cpp/functions/viz/histogram.h b/cpp/functions/viz/histogram.h index 1f89270f..7d9db2c9 100644 --- a/cpp/functions/viz/histogram.h +++ b/cpp/functions/viz/histogram.h @@ -1,7 +1,11 @@ #pragma once -// Renders a histogram using ImPlot::PlotHistogram. +// Renders a histogram using ImPlot::PlotHistogram con ejes pineados +// (ver viz/plot_static.h). // 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); +// height > 0: altura del plot en pixeles (default 200). +void histogram(const char* title, const float* values, int count, + int bins = -1, float height = 200.0f); +void histogram(const char* title, const double* values, int count, + int bins = -1, float height = 200.0f); diff --git a/cpp/functions/viz/histogram.md b/cpp/functions/viz/histogram.md index 3a5b7974..12382731 100644 --- a/cpp/functions/viz/histogram.md +++ b/cpp/functions/viz/histogram.md @@ -3,11 +3,11 @@ name: histogram kind: component lang: cpp domain: viz -version: "1.0.0" +version: "1.1.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] +signature: "void histogram(const char* title, const float* values, int count, int bins = -1, float height = 200.0f)" +description: "Histograma con bins automaticos, ejes lock (con AutoFit para bins dinamicos) y altura explicita" +tags: [implot, chart, visualization, gpu, histogram, distribution, locked-axes] uses_functions: [] uses_types: [] returns: [] @@ -21,13 +21,15 @@ file_path: "cpp/functions/viz/histogram.cpp" framework: imgui params: - name: title - desc: "Titulo del histograma mostrado como cabecera del plot" + desc: "Titulo del histograma / id interno" - 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" + desc: "Numero de bins. -1 = automatico via regla de Sturges (ImPlotBin_Sturges). Positivo = numero explicito" + - name: height + desc: "Altura del plot en pixeles (default 200). Explicita para evitar feedback loops con AutoResizeY" output: "Renderiza el histograma en el frame ImGui actual" --- @@ -35,8 +37,10 @@ output: "Renderiza el histograma en el frame ImGui actual" 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. +## v1.1 -El plot usa `ImVec2(-1, 0)` para ocupar el ancho disponible con altura automatica. +- **Altura explicita** (`height`). +- **Ejes con Lock + AutoFit**: a diferencia de bar_chart/line_plot/scatter_plot, histogram necesita el primer fit para decidir limites segun los bins calculados internamente por ImPlot. `AutoFit` hace ese ajuste pero `Lock` bloquea pan/zoom posterior. +- Resto de flags estaticos via `plot_static::kPlotFlags`. -Debe llamarse dentro del render callback de `fn::run_app`. +Cuando `bins == -1` usa `ImPlotBin_Sturges` (`ceil(log2(n)) + 1`). diff --git a/cpp/functions/viz/kpi_card.cpp b/cpp/functions/viz/kpi_card.cpp index d9f8c79e..b414329b 100644 --- a/cpp/functions/viz/kpi_card.cpp +++ b/cpp/functions/viz/kpi_card.cpp @@ -16,7 +16,7 @@ void kpi_card(const char* label, float value, float delta_percent, ImGui::PushStyleColor(ImGuiCol_Border, colors::border); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md); ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::md)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::sm, spacing::sm)); // Unique child id por label para que multiples cards en la misma ventana // no colisionen. @@ -26,17 +26,24 @@ void kpi_card(const char* label, float value, float delta_percent, float width = ImGui::GetContentRegionAvail().x; if (width < 1.0f) width = 1.0f; - ImGui::BeginChild(child_id, ImVec2(width, 0), - ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); + // Altura fija (no AutoResizeY) para que: + // (a) todas las cards de un grid queden alineadas visualmente, + // (b) no haya recalculo de layout por card en cada resize de la ventana. + // 78px alcanza para: label (~14px) + value (~22px con escala x1.4) + trend + // (~14px) + padding sm*2 (~16px) ≈ 66px, +12px de aire. + constexpr float card_height = 78.0f; + ImGui::BeginChild(child_id, ImVec2(width, card_height), + ImGuiChildFlags_Borders, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); // Label — muted ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); ImGui::TextUnformatted(label); ImGui::PopStyleColor(); - // Value — scaled up. El format controla el sufijo (ej: "%.0f%%" para - // porcentajes, "%.0f" para enteros, "$%.2f" para moneda). - ImGui::SetWindowFontScale(1.8f); + // Value — escala compacta 1.4x, proporcional a una card de 78px. + // El format controla el sufijo (ej: "%.0f%%" para porcentajes). + ImGui::SetWindowFontScale(1.4f); char value_buf[64]; std::snprintf(value_buf, sizeof(value_buf), format, value); ImGui::TextUnformatted(value_buf); diff --git a/cpp/functions/viz/kpi_card.md b/cpp/functions/viz/kpi_card.md index 1e2518cb..0eaa9cbc 100644 --- a/cpp/functions/viz/kpi_card.md +++ b/cpp/functions/viz/kpi_card.md @@ -3,7 +3,7 @@ name: kpi_card kind: component lang: cpp domain: viz -version: "1.1.0" +version: "1.2.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. Contenedor con surface bg, borde y radius via tokens (Mantine Paper equivalente)." @@ -64,9 +64,9 @@ ImGui::Columns(1); ## Notas -- **v1.1**: la card ahora se renderiza dentro de un `BeginChild` con `surface` bg, `border` y `radius::md` de `fn_tokens` — replica el `` de `@fn_library/kpi_card.tsx`. +- **v1.1**: la card se renderiza dentro de un `BeginChild` con `surface` bg, `border` y `radius::md` de `fn_tokens` — replica el `` del frontend. +- **v1.2**: altura fija 78px (antes 108px) + font scale `1.4x` (antes `1.8x`) + padding `spacing::sm` (antes `md`). Mas compacta para densidades altas de KPIs. `NoScrollbar|NoScrollWithMouse` ademas de altura fija para evitar lag al redimensionar. - El ancho se adapta al contenedor padre via `GetContentRegionAvail().x`. Para que ocupe exactamente una celda usar `ImGui::BeginTable` — `BeginGroup` / `dashboard_grid` no propagan ancho constrained y la card desbordaria la celda. -- La altura es siempre la misma (label + value + linea de trend), aunque no haya delta ni history (se muestra un em dash en `text_dim` como placeholder) para que un grid de KPIs quede alineado. -- El escalado de fuente usa `SetWindowFontScale(1.8f)` — compatible con cualquier fuente cargada; no requiere fonts adicionales. +- La linea de trend siempre se reserva (delta, sparkline o em dash placeholder en `text_dim`) para que un grid de KPIs quede alineado vertical. - Los caracteres UTF-8 del triangulo (`▲` U+25B2 y `▼` U+25BC) y del em dash (`—` U+2014) requieren que la fuente ImGui tenga el rango de simbolos geometricos / puntuacion general cargado. -- Los colores del delta vienen de `fn_tokens::colors::{success, error}` y el placeholder del em dash usa `text_dim`. +- Colores: delta usa `fn_tokens::colors::{success, error}`, placeholder em dash usa `text_dim`, label usa `text_muted`. diff --git a/cpp/functions/viz/line_plot.cpp b/cpp/functions/viz/line_plot.cpp index 4ee7646d..33bfe46c 100644 --- a/cpp/functions/viz/line_plot.cpp +++ b/cpp/functions/viz/line_plot.cpp @@ -1,16 +1,44 @@ #include "viz/line_plot.h" +#include "viz/plot_static.h" #include "implot.h" -void line_plot(const char* title, const float* xs, const float* ys, int count) { - if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { +namespace { + +template +void draw_line(const char* title, const T* xs, const T* ys, int count, float height) { + if (count <= 0) return; + + T x_min = xs[0], x_max = xs[0]; + T y_min = ys[0], y_max = ys[0]; + for (int i = 1; i < count; i++) { + if (xs[i] < x_min) x_min = xs[i]; + if (xs[i] > x_max) x_max = xs[i]; + if (ys[i] < y_min) y_min = ys[i]; + if (ys[i] > y_max) y_max = ys[i]; + } + double dy = static_cast(y_max) - static_cast(y_min); + if (dy < 1e-9) dy = 1.0; + double y_lo = static_cast(y_min) - dy * 0.05; + double y_hi = static_cast(y_max) + dy * 0.05; + + const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f); + + if (ImPlot::BeginPlot(title, plot_size, plot_static::kPlotFlags)) { + ImPlot::SetupAxes(nullptr, nullptr, plot_static::kAxisFlags, plot_static::kAxisFlags); + ImPlot::SetupAxisLimits(ImAxis_X1, static_cast(x_min), + static_cast(x_max), ImPlotCond_Always); + ImPlot::SetupAxisLimits(ImAxis_Y1, y_lo, y_hi, ImPlotCond_Always); ImPlot::PlotLine("##data", xs, ys, count); ImPlot::EndPlot(); } } -void line_plot(const char* title, const double* xs, const double* ys, int count) { - if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { - ImPlot::PlotLine("##data", xs, ys, count); - ImPlot::EndPlot(); - } +} // namespace + +void line_plot(const char* title, const float* xs, const float* ys, int count, float height) { + draw_line(title, xs, ys, count, height); +} + +void line_plot(const char* title, const double* xs, const double* ys, int count, float height) { + draw_line(title, xs, ys, count, height); } diff --git a/cpp/functions/viz/line_plot.h b/cpp/functions/viz/line_plot.h index 50e31929..7f9a24f6 100644 --- a/cpp/functions/viz/line_plot.h +++ b/cpp/functions/viz/line_plot.h @@ -1,8 +1,10 @@ #pragma once -// Renders a 2D line plot using ImPlot. +// Renders a 2D line plot using ImPlot con ejes pineados (ver viz/plot_static.h). // Call within an ImGui frame (inside fn::run_app render callback). -void line_plot(const char* title, const float* xs, const float* ys, int count); - -// Overload with double precision. -void line_plot(const char* title, const double* xs, const double* ys, int count); +// height > 0: altura del plot en pixeles (default 200) — explicita para +// evitar feedback loops con contenedores AutoResizeY. +void line_plot(const char* title, const float* xs, const float* ys, int count, + float height = 200.0f); +void line_plot(const char* title, const double* xs, const double* ys, int count, + float height = 200.0f); diff --git a/cpp/functions/viz/line_plot.md b/cpp/functions/viz/line_plot.md index 4b436717..1516d16a 100644 --- a/cpp/functions/viz/line_plot.md +++ b/cpp/functions/viz/line_plot.md @@ -3,11 +3,11 @@ name: line_plot kind: component lang: cpp domain: viz -version: "1.0.0" +version: "1.1.0" purity: pure -signature: "void line_plot(const char* title, const float* xs, const float* ys, int count)" -description: "Renderiza un grafico de lineas 2D usando ImPlot dentro de un frame ImGui" -tags: [implot, chart, visualization, gpu, line] +signature: "void line_plot(const char* title, const float* xs, const float* ys, int count, float height = 200.0f)" +description: "Line plot 2D con ImPlot, ejes pineados y altura explicita para no vibrar al redimensionar" +tags: [implot, chart, visualization, gpu, line, locked-axes] uses_functions: [] uses_types: [] returns: [] @@ -21,20 +21,26 @@ file_path: "cpp/functions/viz/line_plot.cpp" framework: imgui params: - name: title - desc: "Titulo del grafico, se muestra como header del plot" + desc: "Titulo del grafico / id interno del plot" - name: xs desc: "Array de coordenadas X" - name: ys desc: "Array de coordenadas Y" - name: count desc: "Numero de puntos en los arrays xs/ys" -output: "Renderiza el grafico de lineas en el frame ImGui actual" + - name: height + desc: "Altura del plot en pixeles (default 200). Explicita para evitar feedback loops con contenedores AutoResizeY" +output: "Renderiza la linea en el frame ImGui actual con ejes pineados" --- # line_plot -Wrapper atomico sobre `ImPlot::PlotLine`. Renderiza un grafico de lineas 2D con los datos proporcionados. +Wrapper atomico sobre `ImPlot::PlotLine` configurado para visualizacion estatica. -Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo). +## v1.1 -Soporta `float` y `double` precision. +- **Altura explicita** (`height`) — evita vibracion en contenedores con `AutoResizeY`. +- **Ejes pineados** con `plot_static::kAxisFlags` + `ImPlotCond_Always` calculados a partir de los extremos de `xs`/`ys` con 5% de headroom en Y. +- **Sin inputs, sin auto-fit** — ver `viz/plot_static.h`. + +Soporta `float` y `double`. diff --git a/cpp/functions/viz/pie_chart.cpp b/cpp/functions/viz/pie_chart.cpp index 11d464c6..02a2bf3e 100644 --- a/cpp/functions/viz/pie_chart.cpp +++ b/cpp/functions/viz/pie_chart.cpp @@ -1,30 +1,97 @@ #include "viz/pie_chart.h" +#include "viz/plot_static.h" +#include "imgui.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) { - ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast(-radius), "%.1f", 90.0); - } else { - float r = (radius > 0.0f) ? radius : 0.4f; - ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast(r), "%.1f", 90.0); +#include + +namespace { + +// Localiza que slice del pie esta bajo el cursor. +// Devuelve -1 si el cursor esta fuera del radio del pie. +// +// ImPlot dibuja slices en sentido ANTIHORARIO (matematico) desde angle0=90 +// grados (arriba). atan2(dy, dx) en ejes Y-up devuelve radianes CCW desde +x, +// asi que en coords de datos: +x=0, +y=90 (arriba), -x=180, -y=-90 (abajo). +// El offset desde "arriba" en sentido CCW es angle_deg - 90, normalizado. +template +int slice_at(const T* values, int count, double total, double mouse_x, + double mouse_y, double cx, double cy, double radius) { + double dx = mouse_x - cx; + double dy = mouse_y - cy; + double r = std::sqrt(dx * dx + dy * dy); + if (r > radius) return -1; + + constexpr double kPI = 3.14159265358979323846; + double angle_deg = std::atan2(dy, dx) * 180.0 / kPI; + double offset = angle_deg - 90.0; // desde arriba, sentido CCW + while (offset < 0.0) offset += 360.0; + while (offset >= 360.0) offset -= 360.0; + + double acc = 0.0; + for (int i = 0; i < count; i++) { + double sweep = (static_cast(values[i]) / total) * 360.0; + if (offset >= acc && offset < acc + sweep) return i; + acc += sweep; + } + return count - 1; +} + +template +void draw_pie(const char* title, const char* const* labels, const T* values, + int count, double radius, float height) { + if (count <= 0) return; + + double total = 0.0; + for (int i = 0; i < count; i++) total += static_cast(values[i]); + if (total <= 0.0) return; + + double outer; + if (radius < 0.0) outer = -radius; + else outer = (radius > 0.0) ? radius : 0.4; + + const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f); + + const ImPlotFlags plot_flags = plot_static::kPlotFlags + | ImPlotFlags_Equal + | ImPlotFlags_NoLegend; + + if (ImPlot::BeginPlot(title, plot_size, plot_flags)) { + ImPlot::SetupAxes(nullptr, nullptr, + plot_static::kAxisFlagsHidden, + plot_static::kAxisFlagsHidden); + ImPlot::SetupAxisLimits(ImAxis_X1, 0.0, 1.0, ImPlotCond_Always); + ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 1.0, ImPlotCond_Always); + + ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, outer, "%.0f", 90.0); + + if (ImPlot::IsPlotHovered()) { + ImPlotPoint mp = ImPlot::GetPlotMousePos(); + int idx = slice_at(values, count, total, mp.x, mp.y, + 0.5, 0.5, outer); + if (idx >= 0) { + double v = static_cast(values[idx]); + double pct = 100.0 * v / total; + ImGui::BeginTooltip(); + ImGui::TextUnformatted(labels[idx]); + ImGui::Separator(); + ImGui::Text("%.0f (%.1f%%)", v, pct); + ImGui::EndTooltip(); + } } + 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); - } else { - double r = (radius > 0.0) ? radius : 0.4; - ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, r, "%.1f", 90.0); - } - ImPlot::EndPlot(); - } +} // namespace + +void pie_chart(const char* title, const char* const* labels, const float* values, + int count, float radius, float height) { + draw_pie(title, labels, values, count, static_cast(radius), height); +} + +void pie_chart(const char* title, const char* const* labels, const double* values, + int count, double radius, float height) { + draw_pie(title, labels, values, count, radius, height); } diff --git a/cpp/functions/viz/pie_chart.h b/cpp/functions/viz/pie_chart.h index 9fcec19e..57c742fc 100644 --- a/cpp/functions/viz/pie_chart.h +++ b/cpp/functions/viz/pie_chart.h @@ -1,7 +1,14 @@ #pragma once -// Renders a pie or donut chart using ImPlot::PlotPieChart. +// Renders a pie or donut chart using ImPlot::PlotPieChart, con ejes pineados +// y altura explicita para evitar vibracion al redimensionar. // 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); +// +// radius == 0: auto (0.4). radius > 0: explicit radius. radius < 0: donut +// mode (|radius| as outer, 0.2 as inner). +// height > 0: altura del plot en pixeles (default 200) — explicita para +// evitar feedback loops con contenedores AutoResizeY. +void pie_chart(const char* title, const char* const* labels, const float* values, + int count, float radius = 0.0f, float height = 200.0f); +void pie_chart(const char* title, const char* const* labels, const double* values, + int count, double radius = 0.0, float height = 200.0f); diff --git a/cpp/functions/viz/pie_chart.md b/cpp/functions/viz/pie_chart.md index d6d69872..1fa3e45e 100644 --- a/cpp/functions/viz/pie_chart.md +++ b/cpp/functions/viz/pie_chart.md @@ -3,11 +3,11 @@ name: pie_chart kind: component lang: cpp domain: viz -version: "1.0.0" +version: "1.1.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] +signature: "void pie_chart(const char* title, const char* const* labels, const float* values, int count, float radius = 0.0f, float height = 200.0f)" +description: "Pie/donut chart con ImPlot, ejes pineados, altura explicita y tooltip por slice al pasar el mouse" +tags: [implot, chart, visualization, gpu, pie, donut, tooltip, locked-axes] uses_functions: [] uses_types: [] returns: [] @@ -21,7 +21,7 @@ file_path: "cpp/functions/viz/pie_chart.cpp" framework: imgui params: - name: title - desc: "Titulo del grafico" + desc: "Titulo del grafico (se usa tambien como id interno del plot)" - name: labels desc: "Array de etiquetas para cada segmento del pie" - name: values @@ -29,18 +29,28 @@ params: - 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" + desc: "Radio del pie (0 = auto 0.4). Positivo = radio explicito. Negativo = modo donut con outer radius = |radius|" + - name: height + desc: "Altura del plot en pixeles (default 200). Explicita para evitar feedback loops con contenedores AutoResizeY" +output: "Renderiza el pie en el frame ImGui actual; muestra tooltip con label + valor + % al pasar por encima de un slice" --- # pie_chart -Wrapper atomico sobre `ImPlot::PlotPieChart` con soporte para modo pie y modo donut. +Wrapper atomico sobre `ImPlot::PlotPieChart` configurado para visualizacion estatica en dashboards. Modo pie (`radius >= 0`) o donut (`radius < 0`). -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`. +## v1.1 (2026-04-24) -**Modo pie** (`radius >= 0`): dibuja un pie chart solido. Si `radius == 0`, usa radio automatico de 0.4. +- **Altura explicita** (`height`): necesaria para evitar vibracion en contenedores con `AutoResizeY`. Ver `viz/plot_static.h`. +- **Flags compartidos** desde `plot_static::kPlotFlags` + `kAxisFlagsHidden` (axis decorations off): sin pan/zoom, sin menus, sin auto-fit, sin highlight al hover. +- **Tooltip por slice**: calcula que slice esta bajo el cursor usando `atan2(mouse - center)` (ImPlot dibuja slices en sentido CCW matematico desde angulo 90°, arriba) y muestra `label + valor + porcentaje`. +- Aspect 1:1 mantenido con `ImPlotFlags_Equal` para que el pie no se deforme en paneles rectangulares. -**Modo donut** (`radius < 0`): usa `|radius|` como radio exterior. El agujero interior es fijo en 0.2, suficiente para texto central. +## Ejemplo -Debe llamarse dentro del render callback de `fn::run_app`. +```cpp +const char* labels[] = {"Pure", "Impure"}; +float values[] = {412, 187}; +pie_chart("##purity", labels, values, 2); // pie normal, h=200 +pie_chart("##purity", labels, values, 2, -0.4f, 260.0f); // donut, h=260 +``` diff --git a/cpp/functions/viz/plot_static.h b/cpp/functions/viz/plot_static.h new file mode 100644 index 00000000..ae376a3b --- /dev/null +++ b/cpp/functions/viz/plot_static.h @@ -0,0 +1,56 @@ +#pragma once +#include "implot.h" + +// Configuracion compartida para graficos de visualizacion estatica +// (dashboards de resumen, no exploracion interactiva). +// +// Por que existe esto: +// - ImPlot por defecto permite pan/zoom, auto-fit en el primer frame, +// y reacciona a mouse. En un dashboard eso provoca: +// * barras que "se deslizan" los primeros frames (auto-fit animation) +// * ejes que recalculan tick labels al cambiar rango (vibracion) +// * menus al right-click (ruido) +// - Con estos flags + Lock + Cond_Always el plot queda completamente +// estatico entre frames. +// +// Uso tipico dentro de un chart atomico: +// +// if (ImPlot::BeginPlot(title, ImVec2(-1, height), +// plot_static::kPlotFlags)) { +// ImPlot::SetupAxes(nullptr, nullptr, +// plot_static::kAxisFlags, +// plot_static::kAxisFlags); +// ImPlot::SetupAxisLimits(ImAxis_X1, xmin, xmax, ImPlotCond_Always); +// ImPlot::SetupAxisLimits(ImAxis_Y1, ymin, ymax, ImPlotCond_Always); +// ImPlot::Plot...(...); +// ImPlot::EndPlot(); +// } + +namespace plot_static { + +// Flags del plot: sin frame, sin menu, sin box-select, sin mouse-text overlay. +// NO se usa ImPlotFlags_NoInputs para permitir IsPlotHovered() / hover +// tooltips. El pan/zoom queda bloqueado via ImPlotAxisFlags_Lock en kAxisFlags. +constexpr ImPlotFlags kPlotFlags = + ImPlotFlags_NoFrame + | ImPlotFlags_NoMenus + | ImPlotFlags_NoBoxSelect + | ImPlotFlags_NoMouseText; + +// Flags por eje: bloqueado (pan/zoom imposible), sin fit inicial +// (no anima en el primer frame), sin highlight al pasar el mouse, +// sin menu al right-click. +constexpr ImPlotAxisFlags kAxisFlags = + ImPlotAxisFlags_NoMenus + | ImPlotAxisFlags_Lock + | ImPlotAxisFlags_NoInitialFit + | ImPlotAxisFlags_NoHighlight; + +// Variante para ejes decorativos (pies, heatmaps): sin ticks, sin labels, +// sin grid ni highlight. Combina Lock + NoInitialFit + NoDecorations. +constexpr ImPlotAxisFlags kAxisFlagsHidden = + kAxisFlags + | ImPlotAxisFlags_NoDecorations + | ImPlotAxisFlags_NoGridLines; + +} // namespace plot_static diff --git a/cpp/functions/viz/plot_static.md b/cpp/functions/viz/plot_static.md new file mode 100644 index 00000000..1aa9cb69 --- /dev/null +++ b/cpp/functions/viz/plot_static.md @@ -0,0 +1,62 @@ +--- +name: plot_static +kind: component +lang: cpp +domain: viz +version: "1.0.0" +purity: pure +signature: "namespace plot_static { constexpr ImPlotFlags kPlotFlags; constexpr ImPlotAxisFlags kAxisFlags; constexpr ImPlotAxisFlags kAxisFlagsHidden; }" +description: "Flags compartidos para graficos de visualizacion estatica (dashboards). Sin inputs, sin auto-fit, ejes lock. Header-only, sin .cpp" +tags: [implot, dashboard, static, lock, locked-axes, flags] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [implot] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/viz/plot_static.h" +framework: imgui +params: [] +output: "Header-only con constantes ImPlotFlags/ImPlotAxisFlags agrupadas" +--- + +# plot_static + +Header-only con constantes para configurar cualquier plot de ImPlot como *static visualization* (dashboard mode): sin pan/zoom, sin auto-fit, sin menus, sin highlight al hover. + +Sirve de base para todos los graficos atomicos de `cpp/functions/viz/` (bar_chart, pie_chart, line_plot, scatter_plot, histogram, heatmap, candlestick). Usarlo garantiza que cualquier chart en un dashboard quede congelado entre frames — ImPlot por defecto deja pan/zoom + auto-fit del primer frame, y en un dashboard eso produce: + +- Barras "deslizandose" al abrir la ventana (auto-fit animation). +- Ejes vibrando al redimensionar (ticks recalculan rangos). +- Menus al right-click (ruido). + +## Constantes + +| Constante | Combina | Para | +|-----------|---------|------| +| `kPlotFlags` | `NoInputs | NoFrame | NoMenus | NoBoxSelect | NoMouseText` | Plot canvas puro, sin interaccion | +| `kAxisFlags` | `NoMenus | Lock | NoInitialFit | NoHighlight` | Ejes visibles pero pineados | +| `kAxisFlagsHidden` | `kAxisFlags + NoDecorations + NoGridLines` | Pies, heatmaps (ejes decorativos) | + +## Ejemplo + +```cpp +#include "viz/plot_static.h" + +if (ImPlot::BeginPlot("##chart", ImVec2(-1, 200), plot_static::kPlotFlags)) { + ImPlot::SetupAxes(nullptr, nullptr, + plot_static::kAxisFlags, + plot_static::kAxisFlags); + ImPlot::SetupAxisLimits(ImAxis_X1, 0.0, 100.0, ImPlotCond_Always); + ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, y_max, ImPlotCond_Always); + ImPlot::PlotLine("series", xs, ys, count); + ImPlot::EndPlot(); +} +``` + +## Regla + +**Cualquier grafico nuevo en `cpp/functions/viz/` pensado para dashboards debe usar estos flags.** Si el grafico es interactivo (permite zoom, pan, selection) usar flags de ImPlot directamente en lugar de estos. diff --git a/cpp/functions/viz/scatter_plot.cpp b/cpp/functions/viz/scatter_plot.cpp index f2eca52a..ab6051ab 100644 --- a/cpp/functions/viz/scatter_plot.cpp +++ b/cpp/functions/viz/scatter_plot.cpp @@ -1,16 +1,49 @@ #include "viz/scatter_plot.h" +#include "viz/plot_static.h" #include "implot.h" -void scatter_plot(const char* title, const float* xs, const float* ys, int count) { - if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { +namespace { + +template +void draw_scatter(const char* title, const T* xs, const T* ys, int count, float height) { + if (count <= 0) return; + + T x_min = xs[0], x_max = xs[0]; + T y_min = ys[0], y_max = ys[0]; + for (int i = 1; i < count; i++) { + if (xs[i] < x_min) x_min = xs[i]; + if (xs[i] > x_max) x_max = xs[i]; + if (ys[i] < y_min) y_min = ys[i]; + if (ys[i] > y_max) y_max = ys[i]; + } + double dx = static_cast(x_max) - static_cast(x_min); + double dy = static_cast(y_max) - static_cast(y_min); + if (dx < 1e-9) dx = 1.0; + if (dy < 1e-9) dy = 1.0; + + const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f); + + if (ImPlot::BeginPlot(title, plot_size, plot_static::kPlotFlags)) { + ImPlot::SetupAxes(nullptr, nullptr, plot_static::kAxisFlags, plot_static::kAxisFlags); + ImPlot::SetupAxisLimits(ImAxis_X1, + static_cast(x_min) - dx * 0.05, + static_cast(x_max) + dx * 0.05, + ImPlotCond_Always); + ImPlot::SetupAxisLimits(ImAxis_Y1, + static_cast(y_min) - dy * 0.05, + static_cast(y_max) + dy * 0.05, + ImPlotCond_Always); ImPlot::PlotScatter("##data", xs, ys, count); ImPlot::EndPlot(); } } -void scatter_plot(const char* title, const double* xs, const double* ys, int count) { - if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) { - ImPlot::PlotScatter("##data", xs, ys, count); - ImPlot::EndPlot(); - } +} // namespace + +void scatter_plot(const char* title, const float* xs, const float* ys, int count, float height) { + draw_scatter(title, xs, ys, count, height); +} + +void scatter_plot(const char* title, const double* xs, const double* ys, int count, float height) { + draw_scatter(title, xs, ys, count, height); } diff --git a/cpp/functions/viz/scatter_plot.h b/cpp/functions/viz/scatter_plot.h index fbefac5d..42b457d5 100644 --- a/cpp/functions/viz/scatter_plot.h +++ b/cpp/functions/viz/scatter_plot.h @@ -1,6 +1,10 @@ #pragma once -// Renders a scatter plot using ImPlot. +// Renders a scatter plot using ImPlot con ejes pineados (ver viz/plot_static.h). // Call within an ImGui frame. -void scatter_plot(const char* title, const float* xs, const float* ys, int count); -void scatter_plot(const char* title, const double* xs, const double* ys, int count); +// height > 0: altura del plot en pixeles (default 200) — explicita para +// evitar feedback loops con contenedores AutoResizeY. +void scatter_plot(const char* title, const float* xs, const float* ys, int count, + float height = 200.0f); +void scatter_plot(const char* title, const double* xs, const double* ys, int count, + float height = 200.0f); diff --git a/cpp/functions/viz/scatter_plot.md b/cpp/functions/viz/scatter_plot.md index 2073f0fe..50fe896c 100644 --- a/cpp/functions/viz/scatter_plot.md +++ b/cpp/functions/viz/scatter_plot.md @@ -3,11 +3,11 @@ name: scatter_plot kind: component lang: cpp domain: viz -version: "1.0.0" +version: "1.1.0" purity: pure -signature: "void scatter_plot(const char* title, const float* xs, const float* ys, int count)" -description: "Renderiza un grafico de dispersion usando ImPlot dentro de un frame ImGui" -tags: [implot, chart, visualization, gpu, scatter] +signature: "void scatter_plot(const char* title, const float* xs, const float* ys, int count, float height = 200.0f)" +description: "Scatter plot 2D con ImPlot, ejes pineados y altura explicita para no vibrar al redimensionar" +tags: [implot, chart, visualization, gpu, scatter, locked-axes] uses_functions: [] uses_types: [] returns: [] @@ -21,18 +21,26 @@ file_path: "cpp/functions/viz/scatter_plot.cpp" framework: imgui params: - name: title - desc: "Titulo del grafico scatter" + desc: "Titulo del grafico / id interno" - name: xs desc: "Array de coordenadas X" - name: ys desc: "Array de coordenadas Y" - name: count desc: "Numero de puntos en los arrays xs/ys" -output: "Renderiza el grafico de dispersion en el frame ImGui actual" + - name: height + desc: "Altura del plot en pixeles (default 200). Explicita para evitar feedback loops" +output: "Renderiza el scatter en el frame ImGui actual con ejes pineados" --- # scatter_plot -Wrapper atomico sobre `ImPlot::PlotScatter`. Renderiza un grafico de dispersion 2D. +Wrapper atomico sobre `ImPlot::PlotScatter` configurado para visualizacion estatica. -Debe llamarse dentro del render callback de `fn::run_app`. +## v1.1 + +- **Altura explicita** (`height`). +- **Ejes pineados** (`plot_static::kAxisFlags` + `ImPlotCond_Always`) calculados a partir de min/max de `xs`/`ys` con 5% de headroom en ambos ejes. +- **Sin inputs, sin auto-fit** — ver `viz/plot_static.h`. + +Soporta `float` y `double`.