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:
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user