1a6e3cbeaf
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>
176 lines
6.7 KiB
C++
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);
|
|
}
|