feat(cpp/viz): kpi_card v1.1 + bar_chart v1.1 — contenedor y altura fijas
kpi_card: - v1.1: envuelve el contenido en BeginChild con surface bg + border + radius::md + padding::md (tokens). Replica Mantine Paper withBorder radius="md" p="md" usado en @fn_library/kpi_card.tsx. - Ancho adaptativo via GetContentRegionAvail — requiere contenedor que propague ancho constrained (ImGui::BeginTable). dashboard_grid / BeginGroup no funcionan porque no constrainen ancho y la card desborda la celda. - Linea de trend SIEMPRE visible: delta, sparkline, o em dash (text_dim) como placeholder, para que un grid de KPIs quede alineado vertical. - Colores del delta via tokens (success/error) en vez de hardcoded ImVec4. bar_chart: - v1.1: altura explicita como parametro (default 200px). Sin esto, ImPlot con ImVec2(-1, 0) entra en feedback loop cuando esta dentro de un dashboard_panel (BeginChild con AutoResizeY): plot pide espacio -> padre se redimensiona -> plot recalcula. Efecto visual: las barras se deslizan los primeros frames. - Ejes blindados: Lock + NoInitialFit + Cond_Always ademas de los flags previos. Y max pre-calculado con 15% de headroom. - Sin inputs (NoInputs|NoFrame|NoBoxSelect|NoMouseText): estos charts son de resumen, no de exploracion. Actualizados los .md correspondientes con el contrato visual + requisitos de contenedor, para que cualquier dashboard que componga estos primitivos obtenga el mismo look. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <typename T>
|
||||
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<double> 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<float>(title, labels, values, count, static_cast<double>(bar_width));
|
||||
int count, float bar_width, float height) {
|
||||
draw_bars<float>(title, labels, values, count, static_cast<double>(bar_width), height);
|
||||
}
|
||||
|
||||
void bar_chart(const char* title, const char* const* labels, const double* values,
|
||||
int count, double bar_width) {
|
||||
draw_bars<double>(title, labels, values, count, bar_width);
|
||||
int count, double bar_width, float height) {
|
||||
draw_bars<double>(title, labels, values, count, bar_width, height);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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 <Paper withBorder shadow="xs" radius="md" p="md" /> used in
|
||||
// @fn_library/kpi_card.tsx, adapted for ImGui dark theme.
|
||||
// Mirrors Mantine <Paper withBorder shadow="xs" radius="md" p="md" /> usado
|
||||
// en @fn_library/kpi_card.tsx, adaptado a ImGui dark theme via tokens.
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::surface);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, colors::border);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::md);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::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();
|
||||
|
||||
@@ -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 `<Paper withBorder shadow="xs" radius="md" p="md">` 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`.
|
||||
|
||||
Reference in New Issue
Block a user