Make Studio's monitoring charts responsive and full-window

* **Dynamic Full-Window Scaling**: Charts in the Performance and Solver panels now expand to fill the entire pane, automatically arranging side-by-side or stacked based on window orientation to eliminate scrollbars.

  * **Responsive Decorators**: Added reusable C++ ImPlot helper functions that auto-hide titles, legends, and tick labels when the plot surface is small.

  * **Cleaner Visualization**: Refined chart titles (e.g., "CPU msec vs frame", "Convergence vs iter") and removed redundant axis labels for improved scannability.

These changes were based on the plot behavior of //third_party/mujoco/src/experimental/py/sample/implot.py This is an incremental improvement, we can make the plots even nicer in future.

PiperOrigin-RevId: 906351914
Change-Id: I8cb19c19bf807f0107c717066250a71bcf7e325f
This commit is contained in:
Matija Kecman
2026-04-27 07:42:16 -07:00
committed by Copybara-Service
parent 2e3205229c
commit 9cd89eec2f
7 changed files with 162 additions and 39 deletions
+21 -20
View File
@@ -17,12 +17,11 @@
#include <imgui.h>
#include <implot.h>
#include <mujoco/mujoco.h>
#include "experimental/platform/ux/imgui_widgets.h"
namespace mujoco::platform {
SimProfiler::SimProfiler() {
Clear();
}
SimProfiler::SimProfiler() { Clear(); }
void SimProfiler::Clear() {
constexpr int kProfilerMaxFrames = 200;
@@ -88,21 +87,21 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) {
// Solver diagnostics.
mjtNum sqrt_nnz = 0;
int solver_niter = 0;
const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int island=0; island < nisland; island++) {
const int nisland =
data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int island = 0; island < nisland; island++) {
sqrt_nnz += data->solver_nnz[island];
solver_niter += data->solver_niter[island];
}
sqrt_nnz = mju_sqrt(sqrt_nnz);
dim_dof_.erase(dim_dof_.begin());
int nv = (model->opt.enableflags & mjENBL_SLEEP) ? data->nv_awake
: model->nv;
int nv = (model->opt.enableflags & mjENBL_SLEEP) ? data->nv_awake : model->nv;
dim_dof_.push_back(nv);
dim_body_.erase(dim_body_.begin());
int nbody = (model->opt.enableflags & mjENBL_SLEEP) ? data->nbody_awake
: model->nbody;
: model->nbody;
dim_body_.push_back(nbody);
dim_constraint_.erase(dim_constraint_.begin());
@@ -115,16 +114,17 @@ void SimProfiler::Update(const mjModel* model, const mjData* data) {
dim_contact_.push_back(data->ncon);
dim_iteration_.erase(dim_iteration_.begin());
dim_iteration_.push_back(static_cast<float>(solver_niter) / nisland);
dim_iteration_.push_back(static_cast<float>(solver_niter) /
mjMAX(1, nisland));
}
void SimProfiler::CpuTimeGraph() {
if (ImPlot::BeginPlot("CPU Time", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) {
void SimProfiler::CpuTimeGraph(ImVec2 plot_size) {
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("CPU msec vs frame", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "frame", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxis(ImAxis_Y1, "msec", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.2f");
ImPlot_SetupTimeAxis(plot_size, "");
ImPlot_SetupValueAxis(plot_size, "", "%.2f");
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
@@ -143,12 +143,13 @@ void SimProfiler::CpuTimeGraph() {
}
}
void SimProfiler::DimensionsGraph() {
if (ImPlot::BeginPlot("Dimensions", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) {
void SimProfiler::DimensionsGraph(ImVec2 plot_size) {
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Dimensions vs frame", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "frame", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxis(ImAxis_Y1, "count", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f");
ImPlot_SetupTimeAxis(plot_size, "");
ImPlot_SetupValueAxis(plot_size, "", "%.0f");
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
+3 -2
View File
@@ -17,6 +17,7 @@
#include <vector>
#include <imgui.h>
#include <mujoco/mujoco.h>
namespace mujoco::platform {
@@ -33,8 +34,8 @@ class SimProfiler {
void Update(const mjModel* model, const mjData* data);
// Displays the profiling data using ImPlot.
void CpuTimeGraph();
void DimensionsGraph();
void CpuTimeGraph(ImVec2 plot_size = ImVec2(-1, 0));
void DimensionsGraph(ImVec2 plot_size = ImVec2(-1, 0));
private:
std::vector<float> cpu_total_;
+11 -10
View File
@@ -506,7 +506,7 @@ bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera,
auto select = [&](int type, int idx) {
if (ImGui::Selectable(GetCameraName(model, camera, type).c_str(),
(type == idx))) {
(type == idx))) {
return true;
}
return false;
@@ -1148,13 +1148,13 @@ static int GetPlotXLimit(const mjData* data) {
return mjMAX(10, ((max_niter + 9) / 10) * 10);
}
void ConvergenceGui(const mjModel* model, mjData* data) {
void ConvergenceGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
int xlim = GetPlotXLimit(data);
if (ImPlot::BeginPlot("Convergence (log 10)", ImVec2(-1, 0),
ImPlotFlags_NoMouseText)) {
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Convergence (log 10) vs iter", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f");
ImPlot::SetupAxisLimits(ImAxis_Y1, -20, 5, ImPlotCond_Always);
@@ -1211,12 +1211,13 @@ void ConvergenceGui(const mjModel* model, mjData* data) {
}
}
void CountsGui(const mjModel* model, mjData* data) {
void CountsGui(const mjModel* model, mjData* data, ImVec2 plot_size) {
int xlim = GetPlotXLimit(data);
if (ImPlot::BeginPlot("Counts", ImVec2(-1, 0), ImPlotFlags_NoMouseText)) {
ImPlotFlags flags =
ImPlot_SetupPlotFlags(plot_size) | ImPlotFlags_NoMouseText;
if (ImPlot::BeginPlot("Counts vs iter", plot_size, flags)) {
ImPlot::PushStyleVar(ImPlotStyleVar_LineWeight, 2.0f);
ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxis(ImAxis_X1, "", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, xlim, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f");
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 80, ImPlotCond_Always);
+4 -2
View File
@@ -138,10 +138,12 @@ void NoiseGui(const mjModel* model, const mjData* data, float& noise_scale,
float& noise_rate);
// UX for the solver convergence chart.
void ConvergenceGui(const mjModel* model, mjData* data);
void ConvergenceGui(const mjModel* model, mjData* data,
ImVec2 plot_size = ImVec2(-1, 0));
// UX for the solver counts chart.
void CountsGui(const mjModel* model, mjData* data);
void CountsGui(const mjModel* model, mjData* data,
ImVec2 plot_size = ImVec2(-1, 0));
// UX for displaying basic simulation information. Note that the pause state and
// FPS needs to be tracked by the caller and passed here to be displayed.
@@ -14,6 +14,7 @@
#include "experimental/platform/ux/imgui_widgets.h"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <sstream>
@@ -23,6 +24,7 @@
#include <vector>
#include <imgui.h>
#include <implot.h>
#include <mujoco/mujoco.h>
namespace mujoco::platform {
@@ -378,4 +380,72 @@ void MaybeSaveToClipboard(const std::string& contents) {
}
}
ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size) {
ImPlotFlags flags = ImPlotFlags_None;
if (plot_size.x > 0 && plot_size.y > 0) {
const float min_dim = std::min(plot_size.x, plot_size.y);
if (min_dim < 300) {
flags |= ImPlotFlags_NoTitle;
}
if (min_dim < 200) {
flags |= ImPlotFlags_NoLegend;
}
}
return flags;
}
void ImPlot_SetupTimeAxis(ImVec2 plot_size, const char* label,
ImPlotAxisFlags extra_flags) {
ImPlotAxisFlags flags = extra_flags;
if (plot_size.x > 0 && plot_size.x < 300) {
flags |= ImPlotAxisFlags_NoTickLabels;
}
ImPlot::SetupAxis(ImAxis_X1, label, flags);
}
void ImPlot_SetupValueAxis(ImVec2 plot_size, const char* label,
const char* format, ImPlotAxisFlags extra_flags) {
ImPlotAxisFlags flags = extra_flags;
if (plot_size.y > 0 && plot_size.y < 150) {
flags |= ImPlotAxisFlags_NoTickLabels;
}
ImPlot::SetupAxis(ImAxis_Y1, label, flags);
if (format) {
ImPlot::SetupAxisFormat(ImAxis_Y1, format);
}
}
void ImPlot_SetupFixedAxis(ImVec2 plot_size, double y_min, double y_max,
const char* label, const char* format,
const double* tick_values,
const char* const* tick_labels, int n_ticks) {
ImPlotAxisFlags flags = ImPlotAxisFlags_None;
if (plot_size.y > 0 && plot_size.y < 150) {
flags |= ImPlotAxisFlags_NoTickLabels;
}
ImPlot::SetupAxis(ImAxis_Y1, label, flags);
ImPlot::SetupAxisLimits(ImAxis_Y1, y_min, y_max, ImPlotCond_Always);
if (format) {
ImPlot::SetupAxisFormat(ImAxis_Y1, format);
}
if (tick_values && n_ticks > 0) {
ImPlot::SetupAxisTicks(ImAxis_Y1, tick_values, n_ticks, tick_labels);
}
}
ImPlotPairLayout ImPlot_ComputePairLayout() {
ImVec2 avail = ImGui::GetContentRegionAvail();
bool is_wide = avail.x > avail.y;
const float item_spacing = ImGui::GetStyle().ItemSpacing.y;
ImVec2 plot_size(is_wide ? (avail.x - item_spacing) * 0.5f : avail.x,
is_wide ? avail.y : (avail.y - item_spacing) * 0.5f);
return {
plot_size,
is_wide ? ImPlotLayoutDirection::kHorizontal
: ImPlotLayoutDirection::kVertical,
};
}
} // namespace mujoco::platform
@@ -27,6 +27,7 @@
#include <imgui.h>
#include <imgui_internal.h>
#include <misc/cpp/imgui_stdlib.h>
#include <implot.h>
#include <mujoco/mujoco.h>
#include "experimental/platform/ux/enum_utils.h"
@@ -566,6 +567,46 @@ inline void EndBoxSection() { ImGui::EndTable(); }
// Saves the given contents to the clipboard if the clipboard is available.
void MaybeSaveToClipboard(const std::string& contents);
// Returns plot flags with title/legend conditionally hidden when the plot
// area is too small. `plot_size` is the final rendered size of the plot.
ImPlotFlags ImPlot_SetupPlotFlags(ImVec2 plot_size);
// Sets up the X axis as a "time/frame" axis.
// Hides tick labels when the plot is narrow.
// Uses `label` as the axis label (empty string to hide) and auto-fit limits.
void ImPlot_SetupTimeAxis(
ImVec2 plot_size, const char* label = "",
ImPlotAxisFlags extra_flags = ImPlotAxisFlags_AutoFit);
// Sets up a Y axis with auto-fit limits.
// Hides tick labels when the plot is short.
void ImPlot_SetupValueAxis(
ImVec2 plot_size, const char* label = "", const char* format = nullptr,
ImPlotAxisFlags extra_flags = ImPlotAxisFlags_AutoFit);
// Sets up a Y axis with fixed limits and optional explicit ticks.
// Hides tick labels when the plot is short.
void ImPlot_SetupFixedAxis(ImVec2 plot_size, double y_min, double y_max,
const char* label = "", const char* format = nullptr,
const double* tick_values = nullptr,
const char* const* tick_labels = nullptr,
int n_ticks = 0);
enum class ImPlotLayoutDirection {
kHorizontal,
kVertical,
};
struct ImPlotPairLayout {
ImVec2 plot_size; // Size for each individual plot.
ImPlotLayoutDirection direction;
};
// Computes a responsive layout for two plots that share the available
// content region. When the region is wider than tall, the plots are placed
// side-by-side; otherwise they are stacked vertically.
ImPlotPairLayout ImPlot_ComputePairLayout();
} // namespace mujoco::platform
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_UX_IMGUI_WIDGETS_H_
+12 -5
View File
@@ -884,8 +884,12 @@ void App::BuildGui() {
ImGui::SetNextWindowPos(chart_pos, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(chart_size, ImGuiCond_FirstUseEver);
if (ImGui::Begin("Performance", &tmp_.chart_performance)) {
profiler_.CpuTimeGraph();
profiler_.DimensionsGraph();
auto layout = platform::ImPlot_ComputePairLayout();
profiler_.CpuTimeGraph(layout.plot_size);
if (layout.direction == platform::ImPlotLayoutDirection::kHorizontal) {
ImGui::SameLine();
}
profiler_.DimensionsGraph(layout.plot_size);
}
ImGui::End();
}
@@ -894,8 +898,12 @@ void App::BuildGui() {
ImGui::SetNextWindowPos(chart_pos, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(chart_size, ImGuiCond_FirstUseEver);
if (ImGui::Begin("Solver", &tmp_.chart_solver)) {
platform::CountsGui(model(), data());
platform::ConvergenceGui(model(), data());
auto layout = platform::ImPlot_ComputePairLayout();
platform::CountsGui(model(), data(), layout.plot_size);
if (layout.direction == platform::ImPlotLayoutDirection::kHorizontal) {
ImGui::SameLine();
}
platform::ConvergenceGui(model(), data(), layout.plot_size);
}
ImGui::End();
}
@@ -1452,7 +1460,6 @@ void App::ToolBarGui() {
}
ImGui::SetItemTooltip("%s", "Reset");
// Combined (Normal Pause, Viscous Pause, Play) widget and Speed selection.
ImGui::SameLine(0, separator_width);
platform::StepControlGui(model(), &step_control_, tmp_.speed_index);