Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/smoke/widgets_demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
#include <imguix/widgets/plot/PlotOHLCChart.hpp>
#include <imguix/widgets/plot/MetricsPlot.hpp>
#include <imguix/widgets/plot/MetricsPlotSet.hpp>
#include <imguix/widgets/plot/DualMetricBarsPlot.hpp>
#include <imguix/widgets/plot/DualMetricBarsSet.hpp>
#endif

#ifdef IMGUIX_DEMO
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions include/imguix/widgets/plot/DualMetricBarsData.hpp
Original file line number Diff line number Diff line change
@@ -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 <string>
#include <vector>

#include <imgui.h>

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<std::string> labels; ///< Category labels.
std::vector<double> metric1; ///< Left metric values.
std::vector<double> 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
56 changes: 56 additions & 0 deletions include/imguix/widgets/plot/DualMetricBarsPlot.hpp
Original file line number Diff line number Diff line change
@@ -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 <string>

#include <imgui.h>
#include <implot.h>

#include <imguix/widgets/plot/DualMetricBarsData.hpp>

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
151 changes: 151 additions & 0 deletions include/imguix/widgets/plot/DualMetricBarsPlot.ipp
Original file line number Diff line number Diff line change
@@ -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 <algorithm>
#include <cstdio>
#include <vector>

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<size_t>(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<double> positions(N);
std::vector<const char*> ticks(N);
for (size_t i = 0; i < N; ++i) {
positions[i] = double(i);
ticks[i] = data.labels[i].empty() ? "<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
42 changes: 42 additions & 0 deletions include/imguix/widgets/plot/DualMetricBarsSet.hpp
Original file line number Diff line number Diff line change
@@ -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 <imguix/widgets/plot/DualMetricBarsSetData.hpp>
#include <imguix/widgets/plot/DualMetricBarsPlot.hpp>
#include <imguix/widgets/plot/MetricsPlot.hpp> // for CalcMetricsDndWidth

namespace ImGuiX::Widgets {

/// \brief Runtime state for DualMetricBarsPlotSet.
/// \invariant selected.size() == data.sets.size().
struct DualMetricBarsSetState {
std::vector<bool> 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
Loading