feat(cpp/viz): static-plot primitive + tooltips + rotated labels + card compacta

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 21:31:00 +02:00
parent c974eaa604
commit 1a6e3cbeaf
20 changed files with 582 additions and 156 deletions
+6 -1
View File
@@ -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() {
+4 -3
View File
@@ -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
+132 -27
View File
@@ -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 <cmath>
#include <vector>
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<ImWchar>(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 <typename T>
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<double>(values[i]) > y_max) y_max = static_cast<double>(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<double> 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<double> 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<double>(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<const char*> 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<int>(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<double>(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<double>(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();
}
}
+16 -17
View File
@@ -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
```
+27 -9
View File
@@ -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 <typename T>
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<float>(title, values, count, bins, height);
}
void histogram(const char* title, const double* values, int count, int bins, float height) {
draw_hist<double>(title, values, count, bins, height);
}
+7 -3
View File
@@ -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);
+13 -9
View File
@@ -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`).
+13 -6
View File
@@ -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);
+5 -5
View File
@@ -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 `<Paper withBorder shadow="xs" radius="md" p="md">` 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 `<Paper withBorder radius="md" p="sm">` 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`.
+35 -7
View File
@@ -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 <typename T>
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<double>(y_max) - static_cast<double>(y_min);
if (dy < 1e-9) dy = 1.0;
double y_lo = static_cast<double>(y_min) - dy * 0.05;
double y_hi = static_cast<double>(y_max) + dy * 0.05;
const ImVec2 plot_size(-1.0f, height > 0.0f ? height : 200.0f);
if (ImPlot::BeginPlot(title, plot_size, plot_static::kPlotFlags)) {
ImPlot::SetupAxes(nullptr, nullptr, plot_static::kAxisFlags, plot_static::kAxisFlags);
ImPlot::SetupAxisLimits(ImAxis_X1, static_cast<double>(x_min),
static_cast<double>(x_max), ImPlotCond_Always);
ImPlot::SetupAxisLimits(ImAxis_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<float>(title, xs, ys, count, height);
}
void line_plot(const char* title, const double* xs, const double* ys, int count, float height) {
draw_line<double>(title, xs, ys, count, height);
}
+7 -5
View File
@@ -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);
+15 -9
View File
@@ -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`.
+88 -21
View File
@@ -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<double>(-radius), "%.1f", 90.0);
} else {
float r = (radius > 0.0f) ? radius : 0.4f;
ImPlot::PlotPieChart(labels, values, count, 0.5, 0.5, static_cast<double>(r), "%.1f", 90.0);
#include <cmath>
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 <typename T>
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<double>(values[i]) / total) * 360.0;
if (offset >= acc && offset < acc + sweep) return i;
acc += sweep;
}
return count - 1;
}
template <typename T>
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<double>(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<T>(values, count, total, mp.x, mp.y,
0.5, 0.5, outer);
if (idx >= 0) {
double v = static_cast<double>(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<float>(title, labels, values, count, static_cast<double>(radius), height);
}
void pie_chart(const char* title, const char* const* labels, const double* values,
int count, double radius, float height) {
draw_pie<double>(title, labels, values, count, radius, height);
}
+11 -4
View File
@@ -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);
+22 -12
View File
@@ -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
```
+56
View File
@@ -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
+62
View File
@@ -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.
+40 -7
View File
@@ -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 <typename T>
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<double>(x_max) - static_cast<double>(x_min);
double dy = static_cast<double>(y_max) - static_cast<double>(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<double>(x_min) - dx * 0.05,
static_cast<double>(x_max) + dx * 0.05,
ImPlotCond_Always);
ImPlot::SetupAxisLimits(ImAxis_Y1,
static_cast<double>(y_min) - dy * 0.05,
static_cast<double>(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<float>(title, xs, ys, count, height);
}
void scatter_plot(const char* title, const double* xs, const double* ys, int count, float height) {
draw_scatter<double>(title, xs, ys, count, height);
}
+7 -3
View File
@@ -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);
+16 -8
View File
@@ -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`.