mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-21 01:01:43 +00:00
2K was sufficient for simulation because it's possible to pause time, but isn't quite enough for looking at real robot data. 20K points is 400 seconds at 50 Hz which should make pausing plots much more useful. As every point is looped over, this does increase CPU utilization somewhat but doesn't seem to have much of an impact for typical use cases. Increasing this further will necessitate some greater optimizations (e.g. an initial cull using binary search).
1008 lines
31 KiB
C++
1008 lines
31 KiB
C++
// Copyright (c) FIRST and other WPILib contributors.
|
|
// Open Source Software; you can modify and/or share it under the terms of
|
|
// the WPILib BSD license file in the root directory of this project.
|
|
|
|
#include "glass/other/Plot.h"
|
|
|
|
#include <stdint.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#if defined(__GNUC__)
|
|
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
|
#endif
|
|
|
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
|
#include <imgui.h>
|
|
#include <imgui_stdlib.h>
|
|
#include <implot.h>
|
|
#include <implot_internal.h>
|
|
#include <wpi/Signal.h>
|
|
#include <wpi/SmallString.h>
|
|
#include <wpi/SmallVector.h>
|
|
#include <wpi/StringExtras.h>
|
|
#include <wpi/timestamp.h>
|
|
|
|
#include "glass/Context.h"
|
|
#include "glass/DataSource.h"
|
|
#include "glass/Storage.h"
|
|
#include "glass/support/ColorSetting.h"
|
|
#include "glass/support/EnumSetting.h"
|
|
#include "glass/support/ExtraGuiWidgets.h"
|
|
|
|
using namespace glass;
|
|
|
|
static constexpr int kAxisCount = 3;
|
|
|
|
namespace {
|
|
class PlotView;
|
|
|
|
struct PlotSeriesRef {
|
|
PlotView* view;
|
|
size_t plotIndex;
|
|
size_t seriesIndex;
|
|
};
|
|
|
|
class PlotSeries {
|
|
explicit PlotSeries(Storage& storage, int yAxis = 0);
|
|
|
|
public:
|
|
PlotSeries(Storage& storage, std::string_view id);
|
|
PlotSeries(Storage& storage, DataSource* source, int yAxis = 0);
|
|
|
|
const std::string& GetId() const { return m_id; }
|
|
|
|
void CheckSource();
|
|
|
|
void SetSource(DataSource* source);
|
|
DataSource* GetSource() const { return m_source; }
|
|
|
|
enum Action { kNone, kMoveUp, kMoveDown, kDelete };
|
|
Action EmitPlot(PlotView& view, double now, size_t i, size_t plotIndex);
|
|
void EmitSettings(size_t i);
|
|
void EmitDragDropPayload(PlotView& view, size_t i, size_t plotIndex);
|
|
|
|
const char* GetName() const;
|
|
|
|
int GetYAxis() const { return m_yAxis; }
|
|
void SetYAxis(int yAxis) { m_yAxis = yAxis; }
|
|
|
|
void SetColor(const ImVec4& color) { m_color.SetColor(color); }
|
|
|
|
private:
|
|
bool IsDigital() const {
|
|
return m_digital.GetValue() == kDigital ||
|
|
(m_digital.GetValue() == kAuto && m_source && m_source->IsDigital());
|
|
}
|
|
void AppendValue(double value, uint64_t time);
|
|
|
|
// source linkage
|
|
DataSource* m_source = nullptr;
|
|
wpi::sig::ScopedConnection m_sourceCreatedConn;
|
|
wpi::sig::ScopedConnection m_newValueConn;
|
|
std::string& m_id;
|
|
|
|
// user settings
|
|
std::string& m_name;
|
|
int& m_yAxis;
|
|
static constexpr float kDefaultColor[4] = {0.0, 0.0, 0.0, IMPLOT_AUTO};
|
|
ColorSetting m_color;
|
|
EnumSetting m_marker;
|
|
float& m_weight;
|
|
|
|
enum Digital { kAuto, kDigital, kAnalog };
|
|
EnumSetting m_digital;
|
|
int& m_digitalBitHeight;
|
|
int& m_digitalBitGap;
|
|
|
|
// value storage
|
|
static constexpr int kMaxSize = 20000;
|
|
static constexpr double kTimeGap = 0.05;
|
|
std::atomic<int> m_size = 0;
|
|
std::atomic<int> m_offset = 0;
|
|
ImPlotPoint m_data[kMaxSize];
|
|
};
|
|
|
|
class Plot {
|
|
public:
|
|
explicit Plot(Storage& storage);
|
|
|
|
void DragDropTarget(PlotView& view, size_t i, bool inPlot);
|
|
void EmitPlot(PlotView& view, double now, bool paused, size_t i);
|
|
void EmitSettings(size_t i);
|
|
|
|
const std::string& GetName() const { return m_name; }
|
|
|
|
std::vector<std::unique_ptr<PlotSeries>> m_series;
|
|
std::vector<std::unique_ptr<Storage>>& m_seriesStorage;
|
|
|
|
// Returns base height; does not include actual plot height if auto-sized.
|
|
int GetAutoBaseHeight(bool* isAuto, size_t i);
|
|
|
|
void SetAutoHeight(int height) {
|
|
if (m_autoHeight) {
|
|
m_height = height;
|
|
}
|
|
}
|
|
|
|
private:
|
|
void EmitSettingsLimits(int axis);
|
|
void DragDropAccept(PlotView& view, size_t i, int yAxis);
|
|
|
|
bool m_paused = false;
|
|
|
|
std::string& m_name;
|
|
bool& m_visible;
|
|
bool& m_showPause;
|
|
bool& m_lockPrevX;
|
|
bool& m_legend;
|
|
bool& m_legendOutside;
|
|
bool& m_legendHorizontal;
|
|
int& m_legendLocation;
|
|
bool& m_crosshairs;
|
|
bool& m_antialiased;
|
|
bool& m_mousePosition;
|
|
bool& m_yAxis2;
|
|
bool& m_yAxis3;
|
|
float& m_viewTime;
|
|
bool& m_autoHeight;
|
|
int& m_height;
|
|
struct PlotAxis {
|
|
PlotAxis(Storage& storage, int num);
|
|
|
|
std::string& label;
|
|
double& min;
|
|
double& max;
|
|
bool& lockMin;
|
|
bool& lockMax;
|
|
bool apply = false;
|
|
bool& autoFit;
|
|
bool& logScale;
|
|
bool& invert;
|
|
bool& opposite;
|
|
bool& gridLines;
|
|
bool& tickMarks;
|
|
bool& tickLabels;
|
|
};
|
|
std::vector<PlotAxis> m_axis;
|
|
ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
|
|
};
|
|
|
|
class PlotView : public View {
|
|
public:
|
|
PlotView(PlotProvider* provider, Storage& storage);
|
|
|
|
void Display() override;
|
|
|
|
void MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex);
|
|
|
|
void MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
|
|
size_t fromSeriesIndex, size_t toPlotIndex,
|
|
size_t toSeriesIndex, int yAxis = -1);
|
|
|
|
PlotProvider* m_provider;
|
|
std::vector<std::unique_ptr<Storage>>& m_plotsStorage;
|
|
std::vector<std::unique_ptr<Plot>> m_plots;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
PlotSeries::PlotSeries(Storage& storage, int yAxis)
|
|
: m_id{storage.GetString("id")},
|
|
m_name{storage.GetString("name")},
|
|
m_yAxis{storage.GetInt("yAxis", 0)},
|
|
m_color{storage.GetFloatArray("color", kDefaultColor)},
|
|
m_marker{storage.GetString("marker"),
|
|
0,
|
|
{"None", "Circle", "Square", "Diamond", "Up", "Down", "Left",
|
|
"Right", "Cross", "Plus", "Asterisk"}},
|
|
m_weight{storage.GetFloat("weight", IMPLOT_AUTO)},
|
|
m_digital{
|
|
storage.GetString("digital"), kAuto, {"Auto", "Digital", "Analog"}},
|
|
m_digitalBitHeight{storage.GetInt("digitalBitHeight", 8)},
|
|
m_digitalBitGap{storage.GetInt("digitalBitGap", 4)} {
|
|
m_yAxis = yAxis;
|
|
}
|
|
|
|
PlotSeries::PlotSeries(Storage& storage, std::string_view id)
|
|
: PlotSeries{storage, 0} {
|
|
m_id = id;
|
|
if (DataSource* source = DataSource::Find(id)) {
|
|
SetSource(source);
|
|
return;
|
|
}
|
|
CheckSource();
|
|
}
|
|
|
|
PlotSeries::PlotSeries(Storage& storage, DataSource* source, int yAxis)
|
|
: PlotSeries{storage, yAxis} {
|
|
SetSource(source);
|
|
m_id = source->GetId();
|
|
}
|
|
|
|
void PlotSeries::CheckSource() {
|
|
if (!m_newValueConn.connected() && !m_sourceCreatedConn.connected()) {
|
|
m_source = nullptr;
|
|
m_sourceCreatedConn = DataSource::sourceCreated.connect_connection(
|
|
[this](const char* id, DataSource* source) {
|
|
if (m_id == id) {
|
|
SetSource(source);
|
|
m_sourceCreatedConn.disconnect();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void PlotSeries::SetSource(DataSource* source) {
|
|
m_source = source;
|
|
|
|
// add initial value
|
|
AppendValue(source->GetValue(), 0);
|
|
|
|
m_newValueConn = source->valueChanged.connect_connection(
|
|
[this](double value, uint64_t time) { AppendValue(value, time); });
|
|
}
|
|
|
|
void PlotSeries::AppendValue(double value, uint64_t timeUs) {
|
|
double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
|
|
if (IsDigital()) {
|
|
if (m_size < kMaxSize) {
|
|
m_data[m_size] = ImPlotPoint{time, value};
|
|
++m_size;
|
|
} else {
|
|
m_data[m_offset] = ImPlotPoint{time, value};
|
|
m_offset = (m_offset + 1) % kMaxSize;
|
|
}
|
|
} else {
|
|
// as an analog graph draws linear lines in between each value,
|
|
// insert duplicate value if "long" time between updates so it
|
|
// looks appropriately flat
|
|
if (m_size < kMaxSize) {
|
|
if (m_size > 0) {
|
|
if ((time - m_data[m_size - 1].x) > kTimeGap) {
|
|
m_data[m_size] = ImPlotPoint{time, m_data[m_size - 1].y};
|
|
++m_size;
|
|
}
|
|
}
|
|
m_data[m_size] = ImPlotPoint{time, value};
|
|
++m_size;
|
|
} else {
|
|
if (m_offset == 0) {
|
|
if ((time - m_data[kMaxSize - 1].x) > kTimeGap) {
|
|
m_data[m_offset] = ImPlotPoint{time, m_data[kMaxSize - 1].y};
|
|
++m_offset;
|
|
}
|
|
} else {
|
|
if ((time - m_data[m_offset - 1].x) > kTimeGap) {
|
|
m_data[m_offset] = ImPlotPoint{time, m_data[m_offset - 1].y};
|
|
m_offset = (m_offset + 1) % kMaxSize;
|
|
}
|
|
}
|
|
m_data[m_offset] = ImPlotPoint{time, value};
|
|
m_offset = (m_offset + 1) % kMaxSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
const char* PlotSeries::GetName() const {
|
|
if (!m_name.empty()) {
|
|
return m_name.c_str();
|
|
}
|
|
if (m_newValueConn.connected()) {
|
|
auto& sourceName = m_source->GetName();
|
|
if (!sourceName.empty()) {
|
|
return sourceName.c_str();
|
|
}
|
|
}
|
|
return m_id.c_str();
|
|
}
|
|
|
|
PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
|
|
size_t plotIndex) {
|
|
CheckSource();
|
|
|
|
char label[128];
|
|
std::snprintf(label, sizeof(label), "%s###name%d_%d", GetName(),
|
|
static_cast<int>(i), static_cast<int>(plotIndex));
|
|
|
|
int size = m_size;
|
|
int offset = m_offset;
|
|
|
|
// need to have last value at current time, so need to create fake last value
|
|
// we handle the offset logic ourselves to avoid wrap issues with size + 1
|
|
struct GetterData {
|
|
double now;
|
|
double zeroTime;
|
|
ImPlotPoint* data;
|
|
int size;
|
|
int offset;
|
|
};
|
|
GetterData getterData = {now, GetZeroTime() * 1.0e-6, m_data, size, offset};
|
|
auto getter = [](void* data, int idx) {
|
|
auto d = static_cast<GetterData*>(data);
|
|
if (idx == d->size) {
|
|
return ImPlotPoint{
|
|
d->now - d->zeroTime,
|
|
d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
|
|
}
|
|
ImPlotPoint* point;
|
|
if (d->offset + idx < d->size) {
|
|
point = &d->data[d->offset + idx];
|
|
} else {
|
|
point = &d->data[d->offset + idx - d->size];
|
|
}
|
|
return ImPlotPoint{point->x - d->zeroTime, point->y};
|
|
};
|
|
|
|
if (m_color.GetColorFloat()[3] == IMPLOT_AUTO) {
|
|
SetColor(ImPlot::GetColormapColor(i));
|
|
}
|
|
ImPlot::SetNextLineStyle(m_color.GetColor(), m_weight);
|
|
if (IsDigital()) {
|
|
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
|
|
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
|
|
ImPlot::PlotDigitalG(label, getter, &getterData, size + 1);
|
|
ImPlot::PopStyleVar();
|
|
ImPlot::PopStyleVar();
|
|
} else {
|
|
if (ImPlot::GetCurrentPlot()->YAxis(m_yAxis).Enabled) {
|
|
ImPlot::SetAxis(ImAxis_Y1 + m_yAxis);
|
|
} else {
|
|
ImPlot::SetAxis(ImAxis_Y1);
|
|
}
|
|
ImPlot::SetNextMarkerStyle(m_marker.GetValue() - 1);
|
|
ImPlot::PlotLineG(label, getter, &getterData, size + 1);
|
|
}
|
|
|
|
// DND source for PlotSeries
|
|
if (ImPlot::BeginDragDropSourceItem(label)) {
|
|
EmitDragDropPayload(view, i, plotIndex);
|
|
ImPlot::EndDragDropSource();
|
|
}
|
|
|
|
// Show full source name tooltip
|
|
if (!m_name.empty() && ImPlot::IsLegendEntryHovered(label)) {
|
|
ImGui::SetTooltip("%s", m_id.c_str());
|
|
}
|
|
|
|
// Edit settings via popup
|
|
Action rv = kNone;
|
|
if (ImPlot::BeginLegendPopup(label)) {
|
|
ImGui::TextUnformatted(m_id.c_str());
|
|
if (ImGui::Button("Close")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::Text("Edit series name:");
|
|
ImGui::InputText("##editname", &m_name);
|
|
if (ImGui::Button("Move Up")) {
|
|
ImGui::CloseCurrentPopup();
|
|
rv = kMoveUp;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Move Down")) {
|
|
ImGui::CloseCurrentPopup();
|
|
rv = kMoveDown;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Delete")) {
|
|
ImGui::CloseCurrentPopup();
|
|
rv = kDelete;
|
|
}
|
|
EmitSettings(i);
|
|
ImPlot::EndLegendPopup();
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
|
|
size_t plotIndex) {
|
|
PlotSeriesRef ref = {&view, plotIndex, i};
|
|
ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
|
|
ImGui::TextUnformatted(GetName());
|
|
}
|
|
|
|
void PlotSeries::EmitSettings(size_t i) {
|
|
// Line color
|
|
{
|
|
m_color.ColorEdit3("Color", ImGuiColorEditFlags_NoInputs);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Default")) {
|
|
SetColor(ImPlot::GetColormapColor(i));
|
|
}
|
|
}
|
|
|
|
// Line weight
|
|
{
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f");
|
|
}
|
|
|
|
// Digital
|
|
{
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
m_digital.Combo("Digital");
|
|
}
|
|
|
|
if (IsDigital()) {
|
|
// Bit Height
|
|
{
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
|
|
ImGui::InputInt("Bit Height", &m_digitalBitHeight);
|
|
}
|
|
|
|
// Bit Gap
|
|
{
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
|
|
ImGui::InputInt("Bit Gap", &m_digitalBitGap);
|
|
}
|
|
} else {
|
|
// Y-axis
|
|
{
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 4);
|
|
static const char* const options[] = {"1", "2", "3"};
|
|
ImGui::Combo("Y-Axis", &m_yAxis, options, 3);
|
|
}
|
|
|
|
// Marker
|
|
{
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8);
|
|
m_marker.Combo("Marker");
|
|
}
|
|
}
|
|
}
|
|
|
|
Plot::PlotAxis::PlotAxis(Storage& storage, int num)
|
|
: label{storage.GetString("label")},
|
|
min{storage.GetDouble("min", 0)},
|
|
max{storage.GetDouble("max", 1)},
|
|
lockMin{storage.GetBool("lockMin", false)},
|
|
lockMax{storage.GetBool("lockMax", false)},
|
|
autoFit{storage.GetBool("autoFit", false)},
|
|
logScale{storage.GetBool("logScale", false)},
|
|
invert{storage.GetBool("invert", false)},
|
|
opposite{storage.GetBool("opposite", num != 0)},
|
|
gridLines{storage.GetBool("gridLines", num == 0)},
|
|
tickMarks{storage.GetBool("tickMarks", true)},
|
|
tickLabels{storage.GetBool("tickLabels", true)} {}
|
|
|
|
Plot::Plot(Storage& storage)
|
|
: m_seriesStorage{storage.GetChildArray("series")},
|
|
m_name{storage.GetString("name")},
|
|
m_visible{storage.GetBool("visible", true)},
|
|
m_showPause{storage.GetBool("showPause", true)},
|
|
m_lockPrevX{storage.GetBool("lockPrevX", false)},
|
|
m_legend{storage.GetBool("legend", true)},
|
|
m_legendOutside{storage.GetBool("legendOutside", false)},
|
|
m_legendHorizontal{storage.GetBool("legendHorizontal", false)},
|
|
m_legendLocation{
|
|
storage.GetInt("legendLocation", ImPlotLocation_NorthWest)},
|
|
m_crosshairs{storage.GetBool("crosshairs", false)},
|
|
m_antialiased{storage.GetBool("antialiased", false)},
|
|
m_mousePosition{storage.GetBool("mousePosition", true)},
|
|
m_yAxis2{storage.GetBool("yaxis2", false)},
|
|
m_yAxis3{storage.GetBool("yaxis3", false)},
|
|
m_viewTime{storage.GetFloat("viewTime", 10)},
|
|
m_autoHeight{storage.GetBool("autoHeight", true)},
|
|
m_height{storage.GetInt("height", 300)} {
|
|
auto& axesStorage = storage.GetChildArray("axis");
|
|
axesStorage.resize(kAxisCount);
|
|
for (int i = 0; i < kAxisCount; ++i) {
|
|
if (!axesStorage[i]) {
|
|
axesStorage[i] = std::make_unique<Storage>();
|
|
}
|
|
m_axis.emplace_back(*axesStorage[i], i);
|
|
}
|
|
|
|
// loop over series
|
|
for (auto&& v : m_seriesStorage) {
|
|
m_series.emplace_back(
|
|
std::make_unique<PlotSeries>(*v, v->ReadString("id")));
|
|
}
|
|
}
|
|
|
|
void Plot::DragDropAccept(PlotView& view, size_t i, int yAxis) {
|
|
if (const ImGuiPayload* payload =
|
|
ImGui::AcceptDragDropPayload("DataSource")) {
|
|
auto source = *static_cast<DataSource**>(payload->Data);
|
|
// don't add duplicates unless it's onto a different Y axis
|
|
auto it =
|
|
std::find_if(m_series.begin(), m_series.end(), [=](const auto& elem) {
|
|
return elem->GetId() == source->GetId() &&
|
|
(yAxis == -1 || elem->GetYAxis() == yAxis);
|
|
});
|
|
if (it == m_series.end()) {
|
|
m_seriesStorage.emplace_back(std::make_unique<Storage>());
|
|
m_series.emplace_back(std::make_unique<PlotSeries>(
|
|
*m_seriesStorage.back(), source, yAxis == -1 ? 0 : yAxis));
|
|
}
|
|
} else if (const ImGuiPayload* payload =
|
|
ImGui::AcceptDragDropPayload("PlotSeries")) {
|
|
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
|
view.MovePlotSeries(ref->view, ref->plotIndex, ref->seriesIndex, i,
|
|
m_series.size(), yAxis);
|
|
} else if (const ImGuiPayload* payload =
|
|
ImGui::AcceptDragDropPayload("Plot")) {
|
|
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
|
view.MovePlot(ref->view, ref->plotIndex, i);
|
|
}
|
|
}
|
|
|
|
void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) {
|
|
if (inPlot) {
|
|
if (ImPlot::BeginDragDropTargetPlot() ||
|
|
ImPlot::BeginDragDropTargetLegend()) {
|
|
DragDropAccept(view, i, -1);
|
|
ImPlot::EndDragDropTarget();
|
|
}
|
|
for (int y = 0; y < kAxisCount; ++y) {
|
|
if (ImPlot::GetCurrentPlot()->YAxis(y).Enabled &&
|
|
ImPlot::BeginDragDropTargetAxis(ImAxis_Y1 + y)) {
|
|
DragDropAccept(view, i, y);
|
|
ImPlot::EndDragDropTarget();
|
|
}
|
|
}
|
|
} else if (ImGui::BeginDragDropTarget()) {
|
|
DragDropAccept(view, i, -1);
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
}
|
|
|
|
void Plot::EmitPlot(PlotView& view, double now, bool paused, size_t i) {
|
|
if (!m_visible) {
|
|
return;
|
|
}
|
|
|
|
bool lockX = (i != 0 && m_lockPrevX);
|
|
|
|
if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause")) {
|
|
m_paused = !m_paused;
|
|
}
|
|
|
|
char label[128];
|
|
std::snprintf(label, sizeof(label), "%s###plot%d", m_name.c_str(),
|
|
static_cast<int>(i));
|
|
ImPlotFlags plotFlags = (m_legend ? 0 : ImPlotFlags_NoLegend) |
|
|
(m_crosshairs ? ImPlotFlags_Crosshairs : 0) |
|
|
(m_antialiased ? ImPlotFlags_AntiAliased : 0) |
|
|
(m_mousePosition ? 0 : ImPlotFlags_NoMouseText);
|
|
|
|
if (ImPlot::BeginPlot(label, ImVec2(-1, m_height), plotFlags)) {
|
|
// setup legend
|
|
if (m_legend) {
|
|
ImPlotLegendFlags legendFlags =
|
|
(m_legendOutside ? ImPlotLegendFlags_Outside : 0) |
|
|
(m_legendHorizontal ? ImPlotLegendFlags_Horizontal : 0);
|
|
ImPlot::SetupLegend(m_legendLocation, legendFlags);
|
|
}
|
|
|
|
// setup x axis
|
|
ImPlot::SetupAxis(ImAxis_X1, nullptr, ImPlotAxisFlags_NoMenus);
|
|
if (lockX) {
|
|
ImPlot::SetupAxisLimits(ImAxis_X1, view.m_plots[i - 1]->m_xaxisRange.Min,
|
|
view.m_plots[i - 1]->m_xaxisRange.Max,
|
|
ImGuiCond_Always);
|
|
} else {
|
|
// also force-pause plots if overall timing is paused
|
|
double zeroTime = GetZeroTime() * 1.0e-6;
|
|
ImPlot::SetupAxisLimits(
|
|
ImAxis_X1, now - zeroTime - m_viewTime, now - zeroTime,
|
|
(paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always);
|
|
}
|
|
|
|
// setup y axes
|
|
for (int i = 0; i < kAxisCount; ++i) {
|
|
if ((i == 1 && !m_yAxis2) || (i == 2 && !m_yAxis3)) {
|
|
continue;
|
|
}
|
|
ImPlotAxisFlags flags =
|
|
(m_axis[i].lockMin ? ImPlotAxisFlags_LockMin : 0) |
|
|
(m_axis[i].lockMax ? ImPlotAxisFlags_LockMax : 0) |
|
|
(m_axis[i].autoFit ? ImPlotAxisFlags_AutoFit : 0) |
|
|
(m_axis[i].logScale ? ImPlotAxisFlags_AutoFit : 0) |
|
|
(m_axis[i].invert ? ImPlotAxisFlags_Invert : 0) |
|
|
(m_axis[i].opposite ? ImPlotAxisFlags_Opposite : 0) |
|
|
(m_axis[i].gridLines ? 0 : ImPlotAxisFlags_NoGridLines) |
|
|
(m_axis[i].tickMarks ? 0 : ImPlotAxisFlags_NoTickMarks) |
|
|
(m_axis[i].tickLabels ? 0 : ImPlotAxisFlags_NoTickLabels);
|
|
ImPlot::SetupAxis(
|
|
ImAxis_Y1 + i,
|
|
m_axis[i].label.empty() ? nullptr : m_axis[i].label.c_str(), flags);
|
|
ImPlot::SetupAxisLimits(
|
|
ImAxis_Y1 + i, m_axis[i].min, m_axis[i].max,
|
|
m_axis[i].apply ? ImGuiCond_Always : ImGuiCond_Once);
|
|
m_axis[i].apply = false;
|
|
}
|
|
|
|
ImPlot::SetupFinish();
|
|
|
|
for (size_t j = 0; j < m_series.size(); ++j) {
|
|
switch (m_series[j]->EmitPlot(view, now, j, i)) {
|
|
case PlotSeries::kMoveUp:
|
|
if (j > 0) {
|
|
std::swap(m_seriesStorage[j - 1], m_seriesStorage[j]);
|
|
std::swap(m_series[j - 1], m_series[j]);
|
|
}
|
|
break;
|
|
case PlotSeries::kMoveDown:
|
|
if (j < (m_series.size() - 1)) {
|
|
std::swap(m_seriesStorage[j], m_seriesStorage[j + 1]);
|
|
std::swap(m_series[j], m_series[j + 1]);
|
|
}
|
|
break;
|
|
case PlotSeries::kDelete:
|
|
m_seriesStorage.erase(m_seriesStorage.begin() + j);
|
|
m_series.erase(m_series.begin() + j);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
DragDropTarget(view, i, true);
|
|
m_xaxisRange = ImPlot::GetPlotLimits().X;
|
|
|
|
ImPlotPlot* plot = ImPlot::GetCurrentPlot();
|
|
ImPlot::EndPlot();
|
|
|
|
// copy plot settings back to storage
|
|
m_legend = (plot->Flags & ImPlotFlags_NoLegend) == 0;
|
|
m_crosshairs = (plot->Flags & ImPlotFlags_Crosshairs) != 0;
|
|
m_antialiased = (plot->Flags & ImPlotFlags_AntiAliased) != 0;
|
|
m_legendOutside =
|
|
(plot->Items.Legend.Flags & ImPlotLegendFlags_Outside) != 0;
|
|
m_legendHorizontal =
|
|
(plot->Items.Legend.Flags & ImPlotLegendFlags_Horizontal) != 0;
|
|
m_legendLocation = plot->Items.Legend.Location;
|
|
|
|
for (int i = 0; i < kAxisCount; ++i) {
|
|
if ((i == 1 && !m_yAxis2) || (i == 2 && !m_yAxis3)) {
|
|
continue;
|
|
}
|
|
auto flags = plot->Axes[ImAxis_Y1 + i].Flags;
|
|
m_axis[i].lockMin = (flags & ImPlotAxisFlags_LockMin) != 0;
|
|
m_axis[i].lockMax = (flags & ImPlotAxisFlags_LockMax) != 0;
|
|
m_axis[i].autoFit = (flags & ImPlotAxisFlags_AutoFit) != 0;
|
|
m_axis[i].logScale = (flags & ImPlotAxisFlags_LogScale) != 0;
|
|
m_axis[i].invert = (flags & ImPlotAxisFlags_Invert) != 0;
|
|
m_axis[i].opposite = (flags & ImPlotAxisFlags_Opposite) != 0;
|
|
m_axis[i].gridLines = (flags & ImPlotAxisFlags_NoGridLines) == 0;
|
|
m_axis[i].tickMarks = (flags & ImPlotAxisFlags_NoTickMarks) == 0;
|
|
m_axis[i].tickLabels = (flags & ImPlotAxisFlags_NoTickLabels) == 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Plot::EmitSettingsLimits(int axis) {
|
|
ImGui::Indent();
|
|
ImGui::PushID(axis);
|
|
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10);
|
|
ImGui::InputText("Label", &m_axis[axis].label);
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
|
|
ImGui::InputDouble("Min", &m_axis[axis].min, 0, 0, "%.3f");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
|
|
ImGui::InputDouble("Max", &m_axis[axis].max, 0, 0, "%.3f");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Apply")) {
|
|
m_axis[axis].apply = true;
|
|
}
|
|
|
|
ImGui::TextUnformatted("Lock Axis");
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Min##minlock", &m_axis[axis].lockMin);
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Max##maxlock", &m_axis[axis].lockMax);
|
|
|
|
ImGui::PopID();
|
|
ImGui::Unindent();
|
|
}
|
|
|
|
void Plot::EmitSettings(size_t i) {
|
|
ImGui::Text("Edit plot name:");
|
|
ImGui::InputText("##editname", &m_name);
|
|
ImGui::Checkbox("Visible", &m_visible);
|
|
ImGui::Checkbox("Show Pause Button", &m_showPause);
|
|
if (i != 0) {
|
|
ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
|
|
}
|
|
ImGui::TextUnformatted("Primary Y-Axis");
|
|
EmitSettingsLimits(0);
|
|
ImGui::Checkbox("2nd Y-Axis", &m_yAxis2);
|
|
if (m_yAxis2) {
|
|
EmitSettingsLimits(1);
|
|
}
|
|
ImGui::Checkbox("3rd Y-Axis", &m_yAxis3);
|
|
if (m_yAxis3) {
|
|
EmitSettingsLimits(2);
|
|
}
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
ImGui::InputFloat("View Time (s)", &m_viewTime, 0.1f, 1.0f, "%.1f");
|
|
ImGui::Checkbox("Auto Height", &m_autoHeight);
|
|
if (!m_autoHeight) {
|
|
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
|
|
if (ImGui::InputInt("Height", &m_height, 10)) {
|
|
if (m_height < 0) {
|
|
m_height = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int Plot::GetAutoBaseHeight(bool* isAuto, size_t i) {
|
|
*isAuto = m_autoHeight;
|
|
|
|
if (!m_visible) {
|
|
return 0;
|
|
}
|
|
|
|
int height = m_autoHeight ? 0 : m_height;
|
|
|
|
// Pause button
|
|
if ((i == 0 || !m_lockPrevX) && m_showPause) {
|
|
height += ImGui::GetFrameHeightWithSpacing();
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
PlotView::PlotView(PlotProvider* provider, Storage& storage)
|
|
: m_provider{provider}, m_plotsStorage{storage.GetChildArray("plots")} {
|
|
// loop over plots
|
|
for (auto&& v : m_plotsStorage) {
|
|
// create plot
|
|
m_plots.emplace_back(std::make_unique<Plot>(*v));
|
|
}
|
|
}
|
|
|
|
void PlotView::Display() {
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::Button("Add plot")) {
|
|
m_plotsStorage.emplace_back(std::make_unique<Storage>());
|
|
m_plots.emplace_back(std::make_unique<Plot>(*m_plotsStorage.back()));
|
|
}
|
|
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
auto& plot = m_plots[i];
|
|
ImGui::PushID(i);
|
|
|
|
char name[64];
|
|
if (!plot->GetName().empty()) {
|
|
std::snprintf(name, sizeof(name), "%s", plot->GetName().c_str());
|
|
} else {
|
|
std::snprintf(name, sizeof(name), "Plot %d", static_cast<int>(i));
|
|
}
|
|
|
|
char label[90];
|
|
std::snprintf(label, sizeof(label), "%s###header%d", name,
|
|
static_cast<int>(i));
|
|
|
|
bool open = ImGui::CollapsingHeader(label);
|
|
|
|
// DND source and target for Plot
|
|
if (ImGui::BeginDragDropSource()) {
|
|
PlotSeriesRef ref = {this, i, 0};
|
|
ImGui::SetDragDropPayload("Plot", &ref, sizeof(ref));
|
|
ImGui::TextUnformatted(name);
|
|
ImGui::EndDragDropSource();
|
|
}
|
|
plot->DragDropTarget(*this, i, false);
|
|
|
|
if (open) {
|
|
if (ImGui::Button("Move Up")) {
|
|
if (i > 0) {
|
|
std::swap(m_plotsStorage[i - 1], m_plotsStorage[i]);
|
|
std::swap(m_plots[i - 1], plot);
|
|
}
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Move Down")) {
|
|
if (i < (m_plots.size() - 1)) {
|
|
std::swap(m_plotsStorage[i], m_plotsStorage[i + 1]);
|
|
std::swap(plot, m_plots[i + 1]);
|
|
}
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Delete")) {
|
|
m_plotsStorage.erase(m_plotsStorage.begin() + i);
|
|
m_plots.erase(m_plots.begin() + i);
|
|
ImGui::PopID();
|
|
continue;
|
|
}
|
|
|
|
plot->EmitSettings(i);
|
|
}
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (m_plots.empty()) {
|
|
if (ImGui::Button("Add plot")) {
|
|
m_plotsStorage.emplace_back(std::make_unique<Storage>());
|
|
m_plots.emplace_back(std::make_unique<Plot>(*m_plotsStorage.back()));
|
|
}
|
|
|
|
// Make "add plot" button a DND target for Plot
|
|
if (!ImGui::BeginDragDropTarget()) {
|
|
return;
|
|
}
|
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("Plot")) {
|
|
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
|
|
MovePlot(ref->view, ref->plotIndex, 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Auto-size plots. This requires two passes: the first pass to get the
|
|
// total height, the second to actually set the height after averaging it
|
|
// across all auto-sized heights.
|
|
int availHeight = ImGui::GetContentRegionAvail().y;
|
|
int numAuto = 0;
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
bool isAuto;
|
|
availHeight -= m_plots[i]->GetAutoBaseHeight(&isAuto, i);
|
|
availHeight -= ImGui::GetStyle().ItemSpacing.y;
|
|
if (isAuto) {
|
|
++numAuto;
|
|
}
|
|
}
|
|
if (numAuto > 0) {
|
|
availHeight /= numAuto;
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
m_plots[i]->SetAutoHeight(availHeight);
|
|
}
|
|
}
|
|
|
|
double now = wpi::Now() * 1.0e-6;
|
|
for (size_t i = 0; i < m_plots.size(); ++i) {
|
|
ImGui::PushID(i);
|
|
m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i);
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
|
|
void PlotView::MovePlot(PlotView* fromView, size_t fromIndex, size_t toIndex) {
|
|
if (fromView == this) {
|
|
if (fromIndex == toIndex) {
|
|
return;
|
|
}
|
|
|
|
auto st = std::move(m_plotsStorage[fromIndex]);
|
|
m_plotsStorage.insert(m_plotsStorage.begin() + toIndex, std::move(st));
|
|
m_plotsStorage.erase(m_plotsStorage.begin() + fromIndex +
|
|
(fromIndex > toIndex ? 1 : 0));
|
|
|
|
auto val = std::move(m_plots[fromIndex]);
|
|
m_plots.insert(m_plots.begin() + toIndex, std::move(val));
|
|
m_plots.erase(m_plots.begin() + fromIndex + (fromIndex > toIndex ? 1 : 0));
|
|
} else {
|
|
auto st = std::move(fromView->m_plotsStorage[fromIndex]);
|
|
m_plotsStorage.insert(m_plotsStorage.begin() + toIndex, std::move(st));
|
|
fromView->m_plotsStorage.erase(fromView->m_plotsStorage.begin() +
|
|
fromIndex);
|
|
|
|
auto val = std::move(fromView->m_plots[fromIndex]);
|
|
m_plots.insert(m_plots.begin() + toIndex, std::move(val));
|
|
fromView->m_plots.erase(fromView->m_plots.begin() + fromIndex);
|
|
}
|
|
}
|
|
|
|
void PlotView::MovePlotSeries(PlotView* fromView, size_t fromPlotIndex,
|
|
size_t fromSeriesIndex, size_t toPlotIndex,
|
|
size_t toSeriesIndex, int yAxis) {
|
|
if (fromView == this && fromPlotIndex == toPlotIndex) {
|
|
// need to handle this specially as the index of the old location changes
|
|
if (fromSeriesIndex != toSeriesIndex) {
|
|
auto& seriesStorage = m_plots[fromPlotIndex]->m_seriesStorage;
|
|
auto st = std::move(seriesStorage[fromSeriesIndex]);
|
|
seriesStorage.insert(seriesStorage.begin() + toSeriesIndex,
|
|
std::move(st));
|
|
seriesStorage.erase(seriesStorage.begin() + fromSeriesIndex +
|
|
(fromSeriesIndex > toSeriesIndex ? 1 : 0));
|
|
|
|
auto& plotSeries = m_plots[fromPlotIndex]->m_series;
|
|
auto val = std::move(plotSeries[fromSeriesIndex]);
|
|
// only set Y-axis if actually set
|
|
if (yAxis != -1) {
|
|
val->SetYAxis(yAxis);
|
|
}
|
|
plotSeries.insert(plotSeries.begin() + toSeriesIndex, std::move(val));
|
|
plotSeries.erase(plotSeries.begin() + fromSeriesIndex +
|
|
(fromSeriesIndex > toSeriesIndex ? 1 : 0));
|
|
}
|
|
} else {
|
|
auto& fromPlot = *fromView->m_plots[fromPlotIndex];
|
|
auto& toPlot = *m_plots[toPlotIndex];
|
|
// always set Y-axis if moving plots
|
|
fromPlot.m_series[fromSeriesIndex]->SetYAxis(yAxis == -1 ? 0 : yAxis);
|
|
|
|
toPlot.m_seriesStorage.insert(
|
|
toPlot.m_seriesStorage.begin() + toSeriesIndex,
|
|
std::move(fromPlot.m_seriesStorage[fromSeriesIndex]));
|
|
fromPlot.m_seriesStorage.erase(fromPlot.m_seriesStorage.begin() +
|
|
fromSeriesIndex);
|
|
|
|
toPlot.m_series.insert(toPlot.m_series.begin() + toSeriesIndex,
|
|
std::move(fromPlot.m_series[fromSeriesIndex]));
|
|
fromPlot.m_series.erase(fromPlot.m_series.begin() + fromSeriesIndex);
|
|
}
|
|
}
|
|
|
|
PlotProvider::PlotProvider(Storage& storage) : WindowManager{storage} {
|
|
storage.SetCustomApply([this] {
|
|
// loop over windows
|
|
for (auto&& windowkv : m_storage.GetChildren()) {
|
|
// get or create window
|
|
auto win = GetOrAddWindow(windowkv.key(), true);
|
|
if (!win) {
|
|
continue;
|
|
}
|
|
|
|
// get or create view
|
|
auto view = static_cast<PlotView*>(win->GetView());
|
|
if (!view) {
|
|
win->SetView(std::make_unique<PlotView>(this, windowkv.value()));
|
|
view = static_cast<PlotView*>(win->GetView());
|
|
}
|
|
}
|
|
});
|
|
storage.SetCustomClear([this] {
|
|
EraseWindows();
|
|
m_storage.EraseChildren();
|
|
});
|
|
}
|
|
|
|
PlotProvider::~PlotProvider() = default;
|
|
|
|
void PlotProvider::DisplayMenu() {
|
|
// use index-based loop due to possible RemoveWindow call
|
|
for (size_t i = 0; i < m_windows.size(); ++i) {
|
|
m_windows[i]->DisplayMenuItem();
|
|
// provide method to destroy the plot window
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::Selectable("Destroy Plot Window")) {
|
|
RemoveWindow(i);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
if (ImGui::MenuItem("New Plot Window")) {
|
|
// this is an inefficient algorithm, but the number of windows is small
|
|
char id[32];
|
|
size_t numWindows = m_windows.size();
|
|
for (size_t i = 0; i <= numWindows; ++i) {
|
|
std::snprintf(id, sizeof(id), "Plot <%d>", static_cast<int>(i));
|
|
bool match = false;
|
|
for (size_t j = i; j < numWindows; ++j) {
|
|
if (m_windows[j]->GetId() == id) {
|
|
match = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!match) {
|
|
break;
|
|
}
|
|
}
|
|
if (auto win = AddWindow(
|
|
id, std::make_unique<PlotView>(this, m_storage.GetChild(id)))) {
|
|
win->SetDefaultSize(700, 400);
|
|
}
|
|
}
|
|
}
|