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:
committed by
Copybara-Service
parent
2e3205229c
commit
9cd89eec2f
@@ -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();
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user