#include "viz/bar_chart.h" #include "viz/plot_static.h" #include "imgui.h" #include "imgui_internal.h" #include "implot.h" #include #include 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(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 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(values[i]) > y_max) y_max = static_cast(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 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(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 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(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(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(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(title, labels, values, count, static_cast(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(title, labels, values, count, bar_width, height); }