Files
fn_registry/cpp/functions/viz/bar_chart.cpp
T
egutierrez 3f622561ce 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>
2026-04-24 21:31:00 +02:00

176 lines
6.7 KiB
C++

#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 {
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) {
if (count <= 0) return;
double y_max = 0.0;
for (int i = 0; i < count; i++) {
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;
const float hint_width = ImGui::GetContentRegionAvail().x;
const bool rotate = labels_need_rotation(labels, count, hint_width);
// 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);
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);
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();
}
}
} // namespace
void bar_chart(const char* title, const char* const* labels, const float* values,
int count, float bar_width, float height) {
draw_bars<float>(title, labels, values, count, static_cast<double>(bar_width), height);
}
void bar_chart(const char* title, const char* const* labels, const double* values,
int count, double bar_width, float height) {
draw_bars<double>(title, labels, values, count, bar_width, height);
}