// 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 #include #include #include #include #include #include #include #include #include #define IMGUI_DEFINE_MATH_OPERATORS #include #include #include #include #include #include #include #include #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; 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 = 2000; static constexpr double kTimeGap = 0.05; std::atomic m_size = 0; std::atomic 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> m_series; std::vector>& 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); bool m_paused = false; std::string& m_name; bool& m_visible; bool& m_showPause; bool& m_lockPrevX; bool& m_legend; bool& m_yAxis2; bool& m_yAxis3; float& m_viewTime; bool& m_autoHeight; int& m_height; struct PlotRange { explicit PlotRange(Storage& storage); std::string& label; double& min; double& max; bool& lockMin; bool& lockMax; bool apply = false; }; std::vector 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>& m_plotsStorage; std::vector> m_plots; }; } // namespace PlotSeries::PlotSeries(Storage& storage, int yAxis) : m_id{storage.GetString("id")}, m_name{storage.GetString("name")}, m_yAxis{storage.GetInt("yAxis", yAxis)}, 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, 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 m_data[m_size++] = ImPlotPoint{wpi::Now() * 1.0e-6, source->GetValue()}; 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", GetName()); 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(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 { ImPlot::SetPlotYAxis(m_yAxis); 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::PlotRange::PlotRange(Storage& storage) : label{storage.GetString("label")}, min{storage.GetDouble("min", 0)}, max{storage.GetDouble("max", 1)}, lockMin{storage.GetBool("lockMin", false)}, lockMax{storage.GetBool("lockMax", false)} {} 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_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(3); for (auto&& axisStorage : axesStorage) { if (!axisStorage) { axisStorage = std::make_unique(); } m_axis.emplace_back(*axisStorage); } // loop over series for (auto&& v : m_seriesStorage) { m_series.emplace_back( std::make_unique(*v, v->ReadString("id"))); } } void Plot::DragDropTarget(PlotView& view, size_t i, bool inPlot) { if (!ImGui::BeginDragDropTarget()) { return; } // handle dragging onto a specific Y axis int yAxis = -1; if (inPlot) { for (int y = 0; y < 3; ++y) { if (ImPlot::IsPlotYAxisHovered(y)) { yAxis = y; break; } } } if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DataSource")) { auto source = *static_cast(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()); m_series.emplace_back(std::make_unique( *m_seriesStorage.back(), source, yAxis == -1 ? 0 : yAxis)); } } else if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("PlotSeries")) { auto ref = static_cast(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(payload->Data); view.MovePlot(ref->view, ref->plotIndex, i); } } 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", m_name.c_str()); if (lockX) { ImPlot::SetNextPlotLimitsX(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::SetNextPlotLimitsX( now - zeroTime - m_viewTime, now - zeroTime, (paused || m_paused) ? ImGuiCond_Once : ImGuiCond_Always); } ImPlotAxisFlags yFlags[3] = {ImPlotAxisFlags_None, ImPlotAxisFlags_NoGridLines, ImPlotAxisFlags_NoGridLines}; for (int i = 0; i < 3; ++i) { ImPlot::SetNextPlotLimitsY( m_axis[i].min, m_axis[i].max, m_axis[i].apply ? ImGuiCond_Always : ImGuiCond_Once, i); m_axis[i].apply = false; if (m_axis[i].lockMin) { yFlags[i] |= ImPlotAxisFlags_LockMin; } if (m_axis[i].lockMax) { yFlags[i] |= ImPlotAxisFlags_LockMax; } } ImPlotFlags plotFlags = (m_legend ? 0 : ImPlotFlags_NoLegend) | (m_yAxis2 ? ImPlotFlags_YAxis2 : 0) | (m_yAxis3 ? ImPlotFlags_YAxis3 : 0); if (ImPlot::BeginPlot( label, nullptr, m_axis[0].label.empty() ? nullptr : m_axis[0].label.c_str(), ImVec2(-1, m_height), plotFlags, ImPlotAxisFlags_None, yFlags[0], yFlags[1], yFlags[2], m_axis[1].label.empty() ? nullptr : m_axis[1].label.c_str(), m_axis[2].label.empty() ? nullptr : m_axis[2].label.c_str())) { for (size_t j = 0; j < m_series.size(); ++j) { ImGui::PushID(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; } ImGui::PopID(); } DragDropTarget(view, i, true); m_xaxisRange = ImPlot::GetPlotLimits().X; ImPlot::EndPlot(); } } 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); ImGui::Checkbox("Show Legend", &m_legend); 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(*v)); } } void PlotView::Display() { if (ImGui::BeginPopupContextItem()) { if (ImGui::Button("Add plot")) { m_plotsStorage.emplace_back(std::make_unique()); m_plots.emplace_back(std::make_unique(*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(i)); } char label[90]; std::snprintf(label, sizeof(label), "%s###header%d", name, static_cast(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()); m_plots.emplace_back(std::make_unique(*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(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(win->GetView()); if (!view) { win->SetView(std::make_unique(this, windowkv.value())); view = static_cast(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(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(this, m_storage.GetChild(id)))) { win->SetDefaultSize(700, 400); } } }