diff --git a/examples/smoke/widgets_demo.cpp b/examples/smoke/widgets_demo.cpp index d73f2ab..11968b1 100644 --- a/examples/smoke/widgets_demo.cpp +++ b/examples/smoke/widgets_demo.cpp @@ -61,6 +61,8 @@ #include #include #include +#include +#include #endif #ifdef IMGUIX_DEMO @@ -461,6 +463,8 @@ class WidgetsController : public ImGuiX::Controller { if (ImGui::CollapsingHeader(u8"Metrics / Plot")) { ImGuiX::Widgets::DemoMetricsPlot(); ImGuiX::Widgets::DemoMetricsPlotSet(); + ImGuiX::Widgets::DemoDualMetricBarsPlot(); + ImGuiX::Widgets::DemoDualMetricBarsPlotSet(); } } # endif diff --git a/include/imguix/widgets/plot/DualMetricBarsData.hpp b/include/imguix/widgets/plot/DualMetricBarsData.hpp new file mode 100644 index 0000000..28e1380 --- /dev/null +++ b/include/imguix/widgets/plot/DualMetricBarsData.hpp @@ -0,0 +1,33 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_DATA_HPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_DATA_HPP_INCLUDED + +/// \file DualMetricBarsData.hpp +/// \brief Data for DualMetricBarsPlot. + +#include +#include + +#include + +namespace ImGuiX::Widgets { + + /// \brief Data for a dual-metric grouped bar chart. + /// \invariant labels.size() == metric1.size() == metric2.size(). + struct DualMetricBarsData { + std::string title; ///< Plot title. + std::string label_x; ///< X axis label. + std::string label_y1; ///< Left Y axis label. + std::string label_y2; ///< Right Y axis label. + std::string metric1_name; ///< Name for left metric. + std::string metric2_name; ///< Name for right metric. + std::vector labels; ///< Category labels. + std::vector metric1; ///< Left metric values. + std::vector metric2; ///< Right metric values. + ImVec4 metric1_color{0.16f, 0.66f, 0.13f, 1.0f}; ///< Left metric color. + ImVec4 metric2_color{0.90f, 0.24f, 0.24f, 1.0f}; ///< Right metric color. + }; + +} // namespace ImGuiX::Widgets + +#endif // _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_DATA_HPP_INCLUDED diff --git a/include/imguix/widgets/plot/DualMetricBarsPlot.hpp b/include/imguix/widgets/plot/DualMetricBarsPlot.hpp new file mode 100644 index 0000000..c49520c --- /dev/null +++ b/include/imguix/widgets/plot/DualMetricBarsPlot.hpp @@ -0,0 +1,56 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_PLOT_HPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_PLOT_HPP_INCLUDED + +/// \file DualMetricBarsPlot.hpp +/// \brief Render grouped bars with two metrics and two Y axes. + +#include + +#include +#include + +#include + +namespace ImGuiX::Widgets { + + /// \brief Configuration for DualMetricBarsPlot. + struct DualMetricBarsPlotConfig { + const char* id = "DualMetricBarsPlot"; ///< Plot identifier. + bool force_y1_percent = false; ///< Clamp left axis to [0,100]. + const char* metric1_fmt = nullptr; ///< Format for left annotations. + const char* metric2_fmt = "%.0f"; ///< Format for right annotations. + float y_indent = 0.0f; ///< Top indent for Y limits. + float bar_width = 0.4f; ///< Width of each bar. + ImVec4 drag_line_color{1.0f,1.0f,1.0f,0.5f}; ///< Crosshair color. + }; + + /// \brief Runtime state for DualMetricBarsPlot. + struct DualMetricBarsPlotState { + bool show_annotations = false; ///< Show annotations above bars. + bool show_legend = true; ///< Show legend. + int update_counter = 0; ///< Auto-fit counter. + }; + + /// \brief Render dual-metric grouped bars. + /// \param data Input data arrays. + /// \param state Runtime state (modified). + /// \param cfg Plot configuration. + /// \thread_safety Not thread-safe. + void DualMetricBarsPlot( + const DualMetricBarsData& data, + DualMetricBarsPlotState& state, + const DualMetricBarsPlotConfig& cfg = {} + ); + +# ifdef IMGUIX_DEMO + /// \brief Engineering demo for DualMetricBarsPlot. + /// \thread_safety Not thread-safe. + inline void DemoDualMetricBarsPlot(); +# endif + +} // namespace ImGuiX::Widgets + +#include "DualMetricBarsPlot.ipp" + +#endif // _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_PLOT_HPP_INCLUDED diff --git a/include/imguix/widgets/plot/DualMetricBarsPlot.ipp b/include/imguix/widgets/plot/DualMetricBarsPlot.ipp new file mode 100644 index 0000000..3409e84 --- /dev/null +++ b/include/imguix/widgets/plot/DualMetricBarsPlot.ipp @@ -0,0 +1,151 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_PLOT_IPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_PLOT_IPP_INCLUDED + +#include +#include +#include + +namespace ImGuiX::Widgets { + + inline void DualMetricBarsPlot( + const DualMetricBarsData& data, + DualMetricBarsPlotState& state, + const DualMetricBarsPlotConfig& cfg + ) { + ImGui::PushID(cfg.id); + + const size_t N = data.labels.size(); + const bool valid = + data.metric1.size() == N && data.metric2.size() == N; + + const float fit_x = 1.f / float(std::max(1, N)); + ImPlot::PushStyleVar(ImPlotStyleVar_FitPadding, ImVec2(fit_x, 0.f)); + if (ImPlot::BeginPlot(data.title.c_str())) { + ImPlot::SetupAxes(data.label_x.c_str(), data.label_y1.c_str()); + ImPlot::SetupAxis(ImAxis_Y2, data.label_y2.c_str(), + ImPlotAxisFlags_AuxDefault); + + std::vector positions(N); + std::vector ticks(N); + for (size_t i = 0; i < N; ++i) { + positions[i] = double(i); + ticks[i] = data.labels[i].empty() ? "" : data.labels[i].c_str(); + } + if (N > 0) { + ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), int(N), ticks.data()); + } + ImPlot::SetupAxisLimits(ImAxis_X1, -1.0, double(N)); + + double max1 = 0.0, max2 = 0.0; + if (valid) { + for (size_t i = 0; i < N; ++i) { + max1 = std::max(max1, data.metric1[i]); + max2 = std::max(max2, data.metric2[i]); + } + } + double y1_max = cfg.force_y1_percent ? 100.0 + : std::max(100.0, max1) + cfg.y_indent; + double y2_max = max2 + cfg.y_indent; + ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, y1_max); + ImPlot::SetupAxisLimits(ImAxis_Y2, 0.0, y2_max); + + if (state.show_legend) { + ImPlot::SetupLegend(ImPlotLocation_South, ImPlotLegendFlags_Horizontal); + } + + if (valid && N > 0) { + const float off1 = -cfg.bar_width * 0.5f; + const float off2 = cfg.bar_width * 0.5f; + + ImPlot::SetAxes(ImAxis_X1, ImAxis_Y1); + ImPlot::SetNextFillStyle(data.metric1_color); + ImPlot::PlotBars(data.metric1_name.c_str(), + data.metric1.data(), + int(N), cfg.bar_width, off1); + + ImPlot::SetAxes(ImAxis_X1, ImAxis_Y2); + ImPlot::SetNextFillStyle(data.metric2_color); + ImPlot::PlotBars(data.metric2_name.c_str(), + data.metric2.data(), + int(N), cfg.bar_width, off2); + + const char* fmt1 = cfg.metric1_fmt ? cfg.metric1_fmt + : (cfg.force_y1_percent ? "%.1f%%" : "%.2f"); + const char* fmt2 = cfg.metric2_fmt; + bool hov1 = ImPlot::IsLegendEntryHovered(data.metric1_name.c_str()); + bool hov2 = ImPlot::IsLegendEntryHovered(data.metric2_name.c_str()); + bool show1 = state.show_annotations && (!hov2 || hov1); + bool show2 = state.show_annotations && (!hov1 || hov2); + char buf[32]; + ImVec4 col1 = data.metric1_color; col1.w = 0.5f; + ImVec4 col2 = data.metric2_color; col2.w = 0.5f; + for (size_t i = 0; i < N; ++i) { + double px = positions[i]; + if (show1) { + std::snprintf(buf, sizeof buf, fmt1, data.metric1[i]); + ImPlot::Annotation( + px + off1, + data.metric1[i], + col1, + ImVec2(-5, -5), + false, + "%s", + buf + ); + } + if (show2) { + std::snprintf(buf, sizeof buf, fmt2, data.metric2[i]); + ImPlot::Annotation( + px + off2, + data.metric2[i], + col2, + ImVec2(5, -5), + false, + "%s", + buf + ); + } + } + } + + if (ImPlot::IsPlotHovered()) { + ImPlotPoint mouse = ImPlot::GetPlotMousePos(); + static ImPlotDragToolFlags flags = + ImPlotDragToolFlags_None | ImPlotDragToolFlags_NoInputs; + ImPlot::DragLineX(1, &mouse.x, cfg.drag_line_color, 1, flags); + ImPlot::DragLineY(2, &mouse.y, cfg.drag_line_color, 1, flags); + } + + ImPlot::EndPlot(); + } + ImPlot::PopStyleVar(); + ImGui::PopID(); + } + +# ifdef IMGUIX_DEMO + inline void DemoDualMetricBarsPlot() { + static DualMetricBarsData data; + static DualMetricBarsPlotState state; + static bool init = false; + if (!init) { + data.title = "DualMetricBarsPlot Demo"; + data.label_x = "Strategy"; + data.label_y1 = "Winrate"; + data.label_y2 = "Trades"; + data.metric1_name = "Winrate"; + data.metric2_name = "Trades"; + data.labels = {"test-2024","test-2025"}; + data.metric1 = {16.7,50.0}; + data.metric2 = {100,100}; + init = true; + } + DualMetricBarsPlotConfig cfg; + cfg.force_y1_percent = true; + DualMetricBarsPlot(data, state, cfg); + } +# endif + +} // namespace ImGuiX::Widgets + +#endif // _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_PLOT_IPP_INCLUDED diff --git a/include/imguix/widgets/plot/DualMetricBarsSet.hpp b/include/imguix/widgets/plot/DualMetricBarsSet.hpp new file mode 100644 index 0000000..4c7cbdb --- /dev/null +++ b/include/imguix/widgets/plot/DualMetricBarsSet.hpp @@ -0,0 +1,42 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_HPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_HPP_INCLUDED + +/// \file DualMetricBarsSet.hpp +/// \brief Render selectable DualMetricBarsData sets. + +#include +#include +#include // for CalcMetricsDndWidth + +namespace ImGuiX::Widgets { + + /// \brief Runtime state for DualMetricBarsPlotSet. + /// \invariant selected.size() == data.sets.size(). + struct DualMetricBarsSetState { + std::vector selected; ///< Selection flags per set. + DualMetricBarsPlotState plot_state; ///< Inner plot state. + }; + + /// \brief Render selectable collection of DualMetricBarsData. + /// \param data Input data sets. + /// \param state Runtime state (modified). + /// \param cfg Plot configuration. + /// \thread_safety Not thread-safe. + void DualMetricBarsPlotSet( + const DualMetricBarsSetData& data, + DualMetricBarsSetState& state, + const DualMetricBarsPlotConfig& cfg = {} + ); + +# ifdef IMGUIX_DEMO + /// \brief Engineering demo for DualMetricBarsPlotSet. + /// \thread_safety Not thread-safe. + inline void DemoDualMetricBarsPlotSet(); +# endif + +} // namespace ImGuiX::Widgets + +#include "DualMetricBarsSet.ipp" + +#endif // _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_HPP_INCLUDED diff --git a/include/imguix/widgets/plot/DualMetricBarsSet.ipp b/include/imguix/widgets/plot/DualMetricBarsSet.ipp new file mode 100644 index 0000000..2176b57 --- /dev/null +++ b/include/imguix/widgets/plot/DualMetricBarsSet.ipp @@ -0,0 +1,99 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_IPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_IPP_INCLUDED + +#include + +namespace ImGuiX::Widgets { + + inline void DualMetricBarsPlotSet( + const DualMetricBarsSetData& data, + DualMetricBarsSetState& state, + const DualMetricBarsPlotConfig& cfg + ) { + ImGui::PushID(cfg.id); + + const float list_w = CalcMetricsDndWidth(); + const float item_h = ImGui::GetTextLineHeightWithSpacing(); + + if (state.selected.size() != data.sets.size()) { + state.selected.assign(data.sets.size(), false); + state.plot_state.update_counter = 0; + } + + ImGui::BeginGroup(); + { + ImGui::BeginChild("##left", ImVec2(list_w, 200), ImGuiChildFlags_Borders); + if (ImGui::Button("Select All")) { + std::fill(state.selected.begin(), state.selected.end(), true); + state.plot_state.update_counter = 0; + } + ImGui::SameLine(); + if (ImGui::Button("Reset")) { + std::fill(state.selected.begin(), state.selected.end(), false); + state.plot_state.update_counter = 0; + } + ImGui::Separator(); + for (size_t i = 0; i < data.sets.size(); ++i) { + bool sel = state.selected[i]; + if (ImGui::Selectable( + data.sets[i].title.c_str(), + sel, + 0, + ImVec2(0, item_h) + )) { + state.selected[i] = !sel; + state.plot_state.update_counter = 0; + } + } + ImGui::EndChild(); + } + ImGui::SameLine(); + + size_t first = data.sets.size(); + for (size_t i = 0; i < state.selected.size(); ++i) { + if (state.selected[i]) { first = i; break; } + } + if (first < data.sets.size()) { + DualMetricBarsPlot(data.sets[first], state.plot_state, cfg); + } else { + DualMetricBarsData empty; + empty.title = data.sets.empty() ? "" : data.sets[0].title; + DualMetricBarsPlot(empty, state.plot_state, cfg); + } + + ImGui::EndGroup(); + ImGui::PopID(); + } + +# ifdef IMGUIX_DEMO + inline void DemoDualMetricBarsPlotSet() { + static DualMetricBarsSetData data; + static DualMetricBarsSetState state; + static bool init = false; + if (!init) { + DualMetricBarsData a; + a.title = "Strategy/Result"; + a.label_x = "Strategy"; + a.label_y1 = "Winrate"; + a.label_y2 = "Trades"; + a.metric1_name = "Winrate"; + a.metric2_name = "Trades"; + a.labels = {"A","B"}; + a.metric1 = {42.0,55.0}; + a.metric2 = {120,80}; + data.sets.push_back(a); + DualMetricBarsData b = a; + b.title = "Strategy/Result 2"; + b.metric1 = {30.0,20.0}; + b.metric2 = {90,110}; + data.sets.push_back(b); + init = true; + } + DualMetricBarsPlotSet(data, state); + } +# endif + +} // namespace ImGuiX::Widgets + +#endif // _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_IPP_INCLUDED diff --git a/include/imguix/widgets/plot/DualMetricBarsSetData.hpp b/include/imguix/widgets/plot/DualMetricBarsSetData.hpp new file mode 100644 index 0000000..91cd091 --- /dev/null +++ b/include/imguix/widgets/plot/DualMetricBarsSetData.hpp @@ -0,0 +1,21 @@ +#pragma once +#ifndef _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_DATA_HPP_INCLUDED +#define _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_DATA_HPP_INCLUDED + +/// \file DualMetricBarsSetData.hpp +/// \brief Collection of DualMetricBarsData for DualMetricBarsPlotSet. + +#include + +#include + +namespace ImGuiX::Widgets { + + /// \brief Data sets for DualMetricBarsPlotSet. + struct DualMetricBarsSetData { + std::vector sets; ///< Data series collection. + }; + +} // namespace ImGuiX::Widgets + +#endif // _IMGUIX_WIDGETS_PLOT_DUAL_METRIC_BARS_SET_DATA_HPP_INCLUDED