diff --git a/cpp/functions/viz/bar_chart.cpp b/cpp/functions/viz/bar_chart.cpp index 774f539b..989a6985 100644 --- a/cpp/functions/viz/bar_chart.cpp +++ b/cpp/functions/viz/bar_chart.cpp @@ -5,12 +5,19 @@ namespace { -// Plot bars con ejes pineados (no se mueven entre frames) y labels categoricos. -// ImPlot por defecto auto-fitea Y en cada frame, lo que provoca oscilacion visual. -// Lo resolvemos calculando y_max una vez y forzandolo con ImPlotCond_Always. +// 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. template void draw_bars(const char* title, const char* const* labels, const T* values, - int count, double bar_width) { + int count, double bar_width, float height) { if (count <= 0) return; double y_max = 0.0; @@ -21,13 +28,21 @@ void draw_bars(const char* title, const char* const* labels, const T* values, y_max *= 1.15; // 15% headroom sobre la barra mas alta const ImPlotFlags plot_flags = - ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMouseText; - const ImPlotAxisFlags x_flags = - ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock | ImPlotAxisFlags_NoGridLines; - const ImPlotAxisFlags y_flags = - ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock; + ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoMouseText + | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame; - if (ImPlot::BeginPlot(title, ImVec2(-1, 0), plot_flags)) { + const ImPlotAxisFlags x_flags = + ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock + | ImPlotAxisFlags_NoInitialFit | ImPlotAxisFlags_NoGridLines + | ImPlotAxisFlags_NoHighlight; + + const ImPlotAxisFlags y_flags = + ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_Lock + | ImPlotAxisFlags_NoInitialFit | ImPlotAxisFlags_NoHighlight; + + 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; @@ -45,11 +60,11 @@ void draw_bars(const char* title, const char* const* labels, const T* values, } // namespace void bar_chart(const char* title, const char* const* labels, const float* values, - int count, float bar_width) { - draw_bars(title, labels, values, count, static_cast(bar_width)); + int count, float bar_width, float height) { + draw_bars(title, labels, values, count, static_cast(bar_width), height); } void bar_chart(const char* title, const char* const* labels, const double* values, - int count, double bar_width) { - draw_bars(title, labels, values, count, bar_width); + int count, double bar_width, float height) { + draw_bars(title, labels, values, count, bar_width, height); } diff --git a/cpp/functions/viz/bar_chart.h b/cpp/functions/viz/bar_chart.h index 693e5973..e732975e 100644 --- a/cpp/functions/viz/bar_chart.h +++ b/cpp/functions/viz/bar_chart.h @@ -1,6 +1,10 @@ #pragma once -// Renders a vertical bar chart using ImPlot. +// Renders a vertical bar chart using ImPlot with locked axes so it doesn't +// reflow between frames. Pass an explicit height so it doesn't enter a +// feedback loop with AutoResizeY containers. // Call within an ImGui frame. -void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width = 0.67f); -void bar_chart(const char* title, const char* const* labels, const double* values, int count, double bar_width = 0.67); +void bar_chart(const char* title, const char* const* labels, const float* values, + int count, float bar_width = 0.67f, float height = 200.0f); +void bar_chart(const char* title, const char* const* labels, const double* values, + int count, double bar_width = 0.67, float height = 200.0f); diff --git a/cpp/functions/viz/bar_chart.md b/cpp/functions/viz/bar_chart.md index b374434e..fef696d8 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.0.0" +version: "1.1.0" purity: pure -signature: "void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width)" -description: "Renderiza un grafico de barras verticales usando ImPlot dentro de un frame ImGui" -tags: [implot, chart, visualization, gpu, bar] +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] uses_functions: [] uses_types: [] returns: [] @@ -21,7 +21,7 @@ file_path: "cpp/functions/viz/bar_chart.cpp" framework: imgui params: - name: title - desc: "Titulo del grafico de barras" + desc: "Titulo del grafico de barras (tambien se usa como id interno del plot)" - name: labels desc: "Array de etiquetas para el eje X, una por barra" - name: values @@ -29,12 +29,28 @@ params: - name: count desc: "Numero de barras (longitud de labels y values)" - name: bar_width - desc: "Ancho de cada barra como fraccion del espacio disponible (default 0.67)" -output: "Renderiza el grafico de barras en el frame ImGui actual" + 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" --- # bar_chart -Wrapper atomico sobre `ImPlot::PlotBars` con configuracion automatica de etiquetas en el eje X. +Barras verticales con ImPlot, pensado para dashboards estaticos (resumenes, KPIs). Diseno: + +- **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. Debe llamarse dentro del render callback de `fn::run_app`. + +## 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 +``` diff --git a/cpp/functions/viz/kpi_card.cpp b/cpp/functions/viz/kpi_card.cpp index f3090aaa..d9f8c79e 100644 --- a/cpp/functions/viz/kpi_card.cpp +++ b/cpp/functions/viz/kpi_card.cpp @@ -10,15 +10,16 @@ void kpi_card(const char* label, float value, float delta_percent, using namespace fn_tokens; // Card container — surface bg, border, rounded, padding. - // Mirrors Mantine used in - // @fn_library/kpi_card.tsx, adapted for ImGui dark theme. + // Mirrors Mantine usado + // en @fn_library/kpi_card.tsx, adaptado a ImGui dark theme via tokens. ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface); ImGui::PushStyleColor(ImGuiCol_Border, colors::border); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md); ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::md)); - // Unique child id per label so multiple cards in the same window don't collide. + // Unique child id por label para que multiples cards en la misma ventana + // no colisionen. char child_id[96]; std::snprintf(child_id, sizeof(child_id), "##kpi_%s", label); @@ -28,41 +29,50 @@ void kpi_card(const char* label, float value, float delta_percent, ImGui::BeginChild(child_id, ImVec2(width, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - // Label — muted (Mantine "dimmed" text) + // Label — muted ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); ImGui::TextUnformatted(label); ImGui::PopStyleColor(); - // Value — scaled up (Mantine fw={700} fontSize=1.875rem) + // Value — scaled up. El format controla el sufijo (ej: "%.0f%%" para + // porcentajes, "%.0f" para enteros, "$%.2f" para moneda). ImGui::SetWindowFontScale(1.8f); char value_buf[64]; std::snprintf(value_buf, sizeof(value_buf), format, value); ImGui::TextUnformatted(value_buf); ImGui::SetWindowFontScale(1.0f); - // Delta badge — only render when meaningful + // Delta / trend — SIEMPRE se reserva la linea aunque no haya tendencia, + // para que todas las cards tengan la misma altura. Cuando no hay delta + // ni history, se muestra un guion en text_dim para mantener el ritmo + // visual sin hacer ruido con "+0.0%". const bool has_delta = delta_percent != 0.0f; const bool has_history = history != nullptr && history_count > 0; - if (has_delta || has_history) { + if (has_delta) { const bool positive = delta_percent >= 0.0f; const ImVec4 delta_color = positive ? colors::success : colors::error; - - if (has_delta) { - char delta_buf[32]; - if (positive) { - std::snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent); - } else { - std::snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent); - } - ImGui::PushStyleColor(ImGuiCol_Text, delta_color); - ImGui::TextUnformatted(delta_buf); - ImGui::PopStyleColor(); + char delta_buf[32]; + if (positive) { + std::snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xb2 +%.1f%%", delta_percent); + } else { + std::snprintf(delta_buf, sizeof(delta_buf), "\xe2\x96\xbc %.1f%%", delta_percent); } + ImGui::PushStyleColor(ImGuiCol_Text, delta_color); + ImGui::TextUnformatted(delta_buf); + ImGui::PopStyleColor(); if (has_history) { sparkline(label, history, history_count, delta_color, 120.0f, 24.0f); } + } else if (has_history) { + // Sin delta pero con historia: sparkline en primary (neutro). + sparkline(label, history, history_count, colors::primary, 120.0f, 24.0f); + } else { + // Placeholder para preservar altura de la card. + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted("\xe2\x80\x94"); // em dash + ImGui::PopStyleColor(); } ImGui::EndChild(); diff --git a/cpp/functions/viz/kpi_card.md b/cpp/functions/viz/kpi_card.md index 0838f0a3..1e2518cb 100644 --- a/cpp/functions/viz/kpi_card.md +++ b/cpp/functions/viz/kpi_card.md @@ -3,12 +3,12 @@ name: kpi_card kind: component lang: cpp domain: viz -version: "1.0.0" +version: "1.1.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"] +description: "Card de KPI con valor grande, delta porcentual y sparkline historico. Contenedor con surface bg, borde y radius via tokens (Mantine Paper equivalente)." +tags: [imgui, kpi, card, dashboard, metrics, sparkline, tokens] +uses_functions: ["sparkline_cpp_viz", "tokens_cpp_core"] uses_types: [] returns: [] returns_optional: false @@ -64,8 +64,9 @@ ImGui::Columns(1); ## Notas -- El ancho total del grupo es aproximadamente 150px, apto para grids de 2-4 columnas. +- **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`. +- 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. -- 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. +- 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`.