Files
allwpilib/glass/src/lib/native/cpp/other/Plot.cpp

1074 lines
33 KiB
C++
Raw Normal View History

2020-12-26 14:31:24 -08:00
// 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.
2020-08-14 20:02:35 -07:00
2025-11-07 19:56:21 -05:00
#include "wpi/glass/other/Plot.hpp"
#include <stdint.h>
2020-08-14 20:02:35 -07:00
#include <algorithm>
#include <atomic>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>
2024-09-20 17:43:39 -07:00
#include <utility>
2020-08-14 20:02:35 -07:00
#include <vector>
#include <fmt/format.h>
2025-11-07 19:56:21 -05:00
#include "wpi/util/StringExtras.hpp"
#if defined(__GNUC__)
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#endif
2020-08-14 20:02:35 -07:00
#include <imgui.h>
#include <imgui_stdlib.h>
2020-08-14 20:02:35 -07:00
#include <implot.h>
#include <implot_internal.h>
2025-11-07 19:56:21 -05:00
#include "wpi/util/Signal.h"
#include "wpi/util/SmallString.hpp"
#include "wpi/util/SmallVector.hpp"
#include "wpi/util/timestamp.h"
#include "wpi/glass/Context.hpp"
#include "wpi/glass/DataSource.hpp"
#include "wpi/glass/Storage.hpp"
#include "wpi/glass/support/ColorSetting.hpp"
#include "wpi/glass/support/EnumSetting.hpp"
#include "wpi/glass/support/ExtraGuiWidgets.hpp"
2020-08-14 20:02:35 -07:00
using namespace glass;
2020-08-14 20:02:35 -07:00
static constexpr int kAxisCount = 3;
2020-08-14 20:02:35 -07:00
namespace {
class PlotView;
2020-08-14 20:02:35 -07:00
struct PlotSeriesRef {
PlotView* view;
2020-08-14 20:02:35 -07:00
size_t plotIndex;
size_t seriesIndex;
};
class PlotSeries {
explicit PlotSeries(Storage& storage);
2020-08-14 20:02:35 -07:00
public:
PlotSeries(Storage& storage, std::string_view id);
PlotSeries(Storage& storage, DataSource* source);
PlotSeries(Storage& storage, DataSource* source, int yAxis);
2020-08-14 20:02:35 -07:00
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);
2020-08-14 20:02:35 -07:00
const char* GetName() const;
2020-08-14 20:02:35 -07:00
int GetYAxis() const { return m_yAxis; }
void SetYAxis(int yAxis) { m_yAxis = yAxis; }
void SetColor(const ImVec4& color) { m_color.SetColor(color); }
2020-08-14 20:02:35 -07:00
private:
bool IsDigital() const {
return m_digital.GetValue() == kDigital ||
(m_digital.GetValue() == kAuto && m_source && m_digitalSource);
2020-08-14 20:02:35 -07:00
}
2022-10-08 10:01:31 -07:00
void AppendValue(double value, int64_t time);
2020-08-14 20:02:35 -07:00
// source linkage
DataSource* m_source = nullptr;
bool m_digitalSource = false;
2020-08-14 20:02:35 -07:00
wpi::sig::ScopedConnection m_sourceCreatedConn;
wpi::sig::ScopedConnection m_newValueConn;
std::string& m_id;
2020-08-14 20:02:35 -07:00
// 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;
2020-08-14 20:02:35 -07:00
enum Digital { kAuto, kDigital, kAnalog };
EnumSetting m_digital;
int& m_digitalBitHeight;
int& m_digitalBitGap;
2020-08-14 20:02:35 -07:00
// value storage
static constexpr int kMaxSize = 20000;
2020-08-14 20:02:35 -07:00
static constexpr double kTimeGap = 0.05;
std::atomic<int> m_size = 0;
std::atomic<int> m_offset = 0;
ImPlotPoint m_data[kMaxSize];
};
class Plot {
2020-08-14 20:02:35 -07:00
public:
explicit Plot(Storage& storage);
2020-08-14 20:02:35 -07:00
void DragDropTarget(PlotView& view, size_t i, bool inPlot);
void EmitPlot(PlotView& view, double now, bool paused, size_t i);
2020-08-14 20:02:35 -07:00
void EmitSettings(size_t i);
const std::string& GetName() const { return m_name; }
2020-08-14 20:02:35 -07:00
std::vector<std::unique_ptr<PlotSeries>> m_series;
std::vector<std::unique_ptr<Storage>>& m_seriesStorage;
2020-08-14 20:02:35 -07:00
// 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;
}
}
void SetColor(const ImVec4& color) { m_backgroundColor.SetColor(color); }
2020-08-14 20:02:35 -07:00
private:
void EmitSettingsLimits(int axis);
void DragDropAccept(PlotView& view, size_t i, int yAxis);
2020-08-14 20:02:35 -07:00
bool m_paused = false;
std::string& m_name;
bool& m_visible;
static constexpr float kDefaultBackgroundColor[4] = {0.0, 0.0, 0.0,
IMPLOT_AUTO};
ColorSetting m_backgroundColor;
bool& m_showPause;
bool& m_lockPrevX;
bool& m_legend;
bool& m_legendOutside;
bool& m_legendHorizontal;
int& m_legendLocation;
bool& m_crosshairs;
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;
2020-08-14 20:02:35 -07:00
bool apply = false;
bool& autoFit;
bool& logScale;
bool& invert;
bool& opposite;
bool& gridLines;
bool& tickMarks;
bool& tickLabels;
2020-08-14 20:02:35 -07:00
};
std::vector<PlotAxis> m_axis;
2020-08-14 20:02:35 -07:00
ImPlotRange m_xaxisRange; // read from plot, used for lockPrevX
};
class PlotView : public View {
public:
PlotView(PlotProvider* provider, Storage& storage);
void Display() override;
void Settings() override;
bool HasSettings() 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
2020-08-14 20:02:35 -07:00
PlotSeries::PlotSeries(Storage& storage)
: 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)} {}
PlotSeries::PlotSeries(Storage& storage, std::string_view id)
: PlotSeries{storage} {
m_id = id;
if (DataSource* source = DataSource::Find(id)) {
2020-08-14 20:02:35 -07:00
SetSource(source);
return;
}
CheckSource();
}
PlotSeries::PlotSeries(Storage& storage, DataSource* source)
: PlotSeries{storage} {
2020-08-14 20:02:35 -07:00
SetSource(source);
m_id = source->GetId();
2020-08-14 20:02:35 -07:00
}
PlotSeries::PlotSeries(Storage& storage, DataSource* source, int yAxis)
: PlotSeries{storage, source} {
m_yAxis = yAxis;
}
2020-08-14 20:02:35 -07:00
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) {
2020-08-14 20:02:35 -07:00
if (m_id == id) {
SetSource(source);
m_sourceCreatedConn.disconnect();
}
});
}
}
void PlotSeries::SetSource(DataSource* source) {
2020-08-14 20:02:35 -07:00
m_source = source;
if (auto s = dynamic_cast<BooleanSource*>(source)) {
m_digitalSource = true;
2020-08-14 20:02:35 -07:00
// add initial value
AppendValue(s->GetValue(), 0);
m_newValueConn = s->valueChanged.connect_connection(
[this](bool value, int64_t time) { AppendValue(value, time); });
} else if (auto s = dynamic_cast<DoubleSource*>(source)) {
m_digitalSource = false;
// add initial value
AppendValue(s->GetValue(), 0);
m_newValueConn = s->valueChanged.connect_connection(
[this](double value, int64_t time) { AppendValue(value, time); });
} else if (auto s = dynamic_cast<FloatSource*>(source)) {
m_digitalSource = false;
// add initial value
AppendValue(s->GetValue(), 0);
m_newValueConn = s->valueChanged.connect_connection(
[this](double value, int64_t time) { AppendValue(value, time); });
} else if (auto s = dynamic_cast<IntegerSource*>(source)) {
m_digitalSource = false;
// add initial value
AppendValue(s->GetValue(), 0);
m_newValueConn = s->valueChanged.connect_connection(
[this](int64_t value, int64_t time) { AppendValue(value, time); });
}
2020-08-14 20:02:35 -07:00
}
2022-10-08 10:01:31 -07:00
void PlotSeries::AppendValue(double value, int64_t timeUs) {
double time = (timeUs != 0 ? timeUs : wpi::Now()) * 1.0e-6;
2020-08-14 20:02:35 -07:00
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();
2020-08-14 20:02:35 -07:00
}
PlotSeries::Action PlotSeries::EmitPlot(PlotView& view, double now, size_t i,
size_t plotIndex) {
2020-08-14 20:02:35 -07:00
CheckSource();
char label[128];
wpi::format_to_n_c_str(label, sizeof(label), "{}###name{}_{}", GetName(),
static_cast<int>(i), static_cast<int>(plotIndex));
2020-08-14 20:02:35 -07:00
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;
2020-12-17 07:27:29 -08:00
double zeroTime;
2020-08-14 20:02:35 -07:00
ImPlotPoint* data;
int size;
int offset;
};
2020-12-17 07:27:29 -08:00
GetterData getterData = {now, GetZeroTime() * 1.0e-6, m_data, size, offset};
auto getter = [](int idx, void* data) {
2020-08-14 20:02:35 -07:00
auto d = static_cast<GetterData*>(data);
if (idx == d->size) {
2020-08-14 20:02:35 -07:00
return ImPlotPoint{
2020-12-17 07:27:29 -08:00
d->now - d->zeroTime,
d->data[d->offset == 0 ? d->size - 1 : d->offset - 1].y};
}
2020-12-17 07:27:29 -08:00
ImPlotPoint* point;
if (d->offset + idx < d->size) {
2020-12-17 07:27:29 -08:00
point = &d->data[d->offset + idx];
} else {
2020-12-17 07:27:29 -08:00
point = &d->data[d->offset + idx - d->size];
}
2020-12-17 07:27:29 -08:00
return ImPlotPoint{point->x - d->zeroTime, point->y};
2020-08-14 20:02:35 -07:00
};
if (m_color.GetColorFloat()[3] == IMPLOT_AUTO) {
SetColor(ImPlot::GetColormapColor(i));
}
ImPlot::SetNextLineStyle(m_color.GetColor(), m_weight);
2020-08-14 20:02:35 -07:00
if (IsDigital()) {
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitHeight, m_digitalBitHeight);
ImPlot::PushStyleVar(ImPlotStyleVar_DigitalBitGap, m_digitalBitGap);
ImPlot::PlotDigitalG(label, getter, &getterData, size + 1);
2020-08-14 20:02:35 -07:00
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);
2020-08-14 20:02:35 -07:00
}
// DND source for PlotSeries
if (ImPlot::BeginDragDropSourceItem(label)) {
EmitDragDropPayload(view, i, plotIndex);
ImPlot::EndDragDropSource();
2020-08-14 20:02:35 -07:00
}
// 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;
2020-08-14 20:02:35 -07:00
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")) {
2020-08-14 20:02:35 -07:00
ImGui::CloseCurrentPopup();
rv = kDelete;
2020-08-14 20:02:35 -07:00
}
EmitSettings(i);
2020-08-14 20:02:35 -07:00
ImPlot::EndLegendPopup();
}
return rv;
}
void PlotSeries::EmitDragDropPayload(PlotView& view, size_t i,
size_t plotIndex) {
PlotSeriesRef ref = {&view, plotIndex, i};
2020-08-14 20:02:35 -07:00
ImGui::SetDragDropPayload("PlotSeries", &ref, sizeof(ref));
ImGui::TextUnformatted(GetName());
2020-08-14 20:02:35 -07:00
}
void PlotSeries::EmitSettings(size_t i) {
2020-08-14 20:02:35 -07:00
// Line color
{
m_color.ColorEdit3("Color", ImGuiColorEditFlags_NoInputs);
2020-08-14 20:02:35 -07:00
ImGui::SameLine();
if (ImGui::Button("Default")) {
SetColor(ImPlot::GetColormapColor(i));
}
2020-08-14 20:02:35 -07:00
}
// Line weight
{
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
ImGui::InputFloat("Weight", &m_weight, 0.1f, 1.0f, "%.1f");
}
2020-08-14 20:02:35 -07:00
// Digital
{
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6);
m_digital.Combo("Digital");
2020-08-14 20:02:35 -07:00
}
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");
2020-08-14 20:02:35 -07:00
}
}
}
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_backgroundColor{
storage.GetFloatArray("backgroundColor", kDefaultBackgroundColor)},
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_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")));
2020-08-14 20:02:35 -07:00
}
}
void Plot::DragDropAccept(PlotView& view, size_t i, int yAxis) {
// accept any of double/float/boolean/integer sources
DataSource* source = AcceptDragDropPayload<DoubleSource>();
if (!source) {
source = AcceptDragDropPayload<FloatSource>();
}
if (!source) {
source = AcceptDragDropPayload<BooleanSource>();
}
if (!source) {
source = AcceptDragDropPayload<IntegerSource>();
}
if (source) {
2020-08-14 20:02:35 -07:00
// 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));
2020-08-14 20:02:35 -07:00
}
} 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);
2020-08-14 20:02:35 -07:00
} else if (const ImGuiPayload* payload =
ImGui::AcceptDragDropPayload("Plot")) {
auto ref = static_cast<const PlotSeriesRef*>(payload->Data);
view.MovePlot(ref->view, ref->plotIndex, i);
2020-08-14 20:02:35 -07:00
}
}
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;
}
2020-08-14 20:02:35 -07:00
bool lockX = (i != 0 && m_lockPrevX);
if (!lockX && m_showPause && ImGui::Button(m_paused ? "Resume" : "Pause")) {
2020-08-14 20:02:35 -07:00
m_paused = !m_paused;
}
2020-08-14 20:02:35 -07:00
char label[128];
wpi::format_to_n_c_str(label, sizeof(label), "{}###plot{}", m_name,
static_cast<int>(i));
ImPlotFlags plotFlags = (m_legend ? 0 : ImPlotFlags_NoLegend) |
(m_crosshairs ? ImPlotFlags_Crosshairs : 0) |
(m_mousePosition ? 0 : ImPlotFlags_NoMouseText);
if (ImPlot::BeginPlot(label, ImVec2(-1, m_height), plotFlags)) {
if (m_backgroundColor.GetColorFloat()[3] == IMPLOT_AUTO) {
SetColor(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
}
ImPlot::PushStyleColor(ImPlotCol_PlotBg, m_backgroundColor.GetColor());
// setup legend
if (m_legend) {
ImPlotLegendFlags legendFlags =
(m_legendOutside ? ImPlotLegendFlags_Outside : 0) |
(m_legendHorizontal ? ImPlotLegendFlags_Horizontal : 0);
ImPlot::SetupLegend(m_legendLocation, legendFlags);
}
2020-08-14 20:02:35 -07:00
// 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].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);
ImPlot::SetupAxisScale(ImAxis_Y1 + i, m_axis[i].logScale
? ImPlotScale_Log10
: ImPlotScale_Linear);
m_axis[i].apply = false;
}
2020-08-14 20:02:35 -07:00
ImPlot::SetupFinish();
2020-08-14 20:02:35 -07:00
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;
2020-08-14 20:02:35 -07:00
}
}
DragDropTarget(view, i, true);
2020-08-14 20:02:35 -07:00
m_xaxisRange = ImPlot::GetPlotLimits().X;
ImPlotPlot* plot = ImPlot::GetCurrentPlot();
ImPlot::PopStyleColor();
2020-08-14 20:02:35 -07:00
ImPlot::EndPlot();
// copy plot settings back to storage
m_legend = (plot->Flags & ImPlotFlags_NoLegend) == 0;
m_crosshairs = (plot->Flags & ImPlotFlags_Crosshairs) != 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].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;
m_axis[i].logScale = plot->Axes[ImAxis_Y1 + i].Scale == ImPlotScale_Log10;
}
2020-08-14 20:02:35 -07:00
}
}
void Plot::EmitSettingsLimits(int axis) {
ImGui::Indent();
ImGui::PushID(axis);
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10);
ImGui::InputText("Label", &m_axis[axis].label);
2020-08-14 20:02:35 -07:00
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
ImGui::InputDouble("Min", &m_axis[axis].min, 0, 0, "%.3f");
2020-08-14 20:02:35 -07:00
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5);
ImGui::InputDouble("Max", &m_axis[axis].max, 0, 0, "%.3f");
2020-08-14 20:02:35 -07:00
ImGui::SameLine();
if (ImGui::Button("Apply")) {
m_axis[axis].apply = true;
}
2020-08-14 20:02:35 -07:00
ImGui::TextUnformatted("Lock Axis");
ImGui::SameLine();
ImGui::Checkbox("Min##minlock", &m_axis[axis].lockMin);
2020-08-14 20:02:35 -07:00
ImGui::SameLine();
ImGui::Checkbox("Max##maxlock", &m_axis[axis].lockMax);
2020-08-14 20:02:35 -07:00
ImGui::PopID();
ImGui::Unindent();
}
void Plot::EmitSettings(size_t i) {
ImGui::Text("Edit plot name:");
ImGui::InputText("##editname", &m_name);
2020-08-14 20:02:35 -07:00
ImGui::Checkbox("Visible", &m_visible);
m_backgroundColor.ColorEdit3("Background color",
ImGuiColorEditFlags_NoInputs);
ImGui::SameLine();
if (ImGui::Button("Default")) {
SetColor(ImGui::GetStyleColorVec4(ImGuiCol_WindowBg));
}
ImGui::Checkbox("Show Pause Button", &m_showPause);
if (i != 0) {
ImGui::Checkbox("Lock X-axis to previous plot", &m_lockPrevX);
}
2020-08-14 20:02:35 -07:00
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);
}
2020-08-14 20:02:35 -07:00
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;
}
}
2020-08-14 20:02:35 -07:00
}
}
2020-08-14 20:02:35 -07:00
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 (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()));
}
2020-08-14 20:02:35 -07:00
// 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);
}
2020-08-14 20:02:35 -07:00
}
2020-12-17 07:27:29 -08:00
double now = wpi::Now() * 1.0e-6;
for (size_t i = 0; i < m_plots.size(); ++i) {
2020-08-14 20:02:35 -07:00
ImGui::PushID(i);
m_plots[i]->EmitPlot(*this, now, m_provider->IsPaused(), i);
2020-08-14 20:02:35 -07:00
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();
});
}
void PlotView::Settings() {
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()) {
wpi::format_to_n_c_str(name, sizeof(name), "{}", plot->GetName().c_str());
} else {
wpi::format_to_n_c_str(name, sizeof(name), "Plot {}",
static_cast<int>(i));
}
char label[90];
wpi::format_to_n_c_str(label, sizeof(label), "{}###header{}", 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();
}
}
bool PlotView::HasSettings() {
return true;
}
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();
}
2020-08-14 20:02:35 -07:00
}
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) {
wpi::format_to_n_c_str(id, sizeof(id), "Plot <{}>", static_cast<int>(i));
bool match = false;
for (size_t j = 0; 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);
}
2020-08-14 20:02:35 -07:00
}
}