diff --git a/app/qml/MainWindow/Dashboard/WidgetGrid.qml b/app/qml/MainWindow/Dashboard/WidgetGrid.qml index 1dad8fb0..bb3f08e0 100644 --- a/app/qml/MainWindow/Dashboard/WidgetGrid.qml +++ b/app/qml/MainWindow/Dashboard/WidgetGrid.qml @@ -66,9 +66,10 @@ Widgets.Pane { Rectangle { z: 2 id: header - height: actionsLayout.implicitHeight + 12 - visible: Cpp_UI_Dashboard.actionCount > 0 color: Cpp_ThemeManager.colors["groupbox_background"] + height: visible ? actionsLayout.implicitHeight + 12 : 0 + visible: Cpp_UI_Dashboard.actionCount > 0 && !Cpp_CSV_Player.isOpen + anchors { top: parent.top left: parent.left @@ -122,8 +123,8 @@ Widgets.Pane { contentWidth: width anchors.leftMargin: 8 anchors.bottomMargin: 8 - anchors.topMargin: header.height + 8 contentHeight: grid.height + anchors.topMargin: header.height + 8 ScrollBar.vertical: ScrollBar { id: scroll diff --git a/app/qml/Widgets/Dashboard/Plot.qml b/app/qml/Widgets/Dashboard/Plot.qml index fd8eb126..1ca86027 100644 --- a/app/qml/Widgets/Dashboard/Plot.qml +++ b/app/qml/Widgets/Dashboard/Plot.qml @@ -56,9 +56,9 @@ Item { xMax: root.model.maxX yMin: root.model.minY yMax: root.model.maxY - xLabel: qsTr("Samples") curveColors: [root.color] yLabel: root.model.yLabel + xLabel: root.model.xLabel xAxis.tickInterval: root.model.xTickInterval yAxis.tickInterval: root.model.yTickInterval Component.onCompleted: graph.addSeries(lineSeries) diff --git a/app/src/JSON/Action.cpp b/app/src/JSON/Action.cpp index 8a615340..303d06a6 100644 --- a/app/src/JSON/Action.cpp +++ b/app/src/JSON/Action.cpp @@ -22,6 +22,30 @@ #include "JSON/Action.h" +/** + * @brief Reads a value from a QJsonObject based on a key, returning a default + * value if the key does not exist. + * + * This function checks if the given key exists in the provided QJsonObject. + * If the key is found, it returns the associated value. Otherwise, it returns + * the specified default value. + * + * @param object The QJsonObject to read the data from. + * @param key The key to look for in the QJsonObject. + * @param defaultValue The value to return if the key is not found in the + * QJsonObject. + * @return The value associated with the key, or the defaultValue if the key is + * not present. + */ +static QVariant SAFE_READ(const QJsonObject &object, const QString &key, + const QVariant &defaultValue) +{ + if (object.contains(key)) + return object.value(key); + + return defaultValue; +} + /** * @brief Constructs an Action object with a specified action ID. * @@ -124,11 +148,10 @@ bool JSON::Action::read(const QJsonObject &object) { if (!object.isEmpty()) { - m_icon = object.value(QStringLiteral("icon")).toString(); - m_txData = object.value(QStringLiteral("txData")).toString(); - m_eolSequence = object.value(QStringLiteral("eol")).toString(); - m_title = object.value(QStringLiteral("title")).toString().simplified(); - + m_txData = SAFE_READ(object, "txData", "").toString(); + m_eolSequence = SAFE_READ(object, "eol", "").toString(); + m_icon = SAFE_READ(object, "icon", "").toString().simplified(); + m_title = SAFE_READ(object, "title", "").toString().simplified(); return true; } diff --git a/app/src/JSON/Dataset.cpp b/app/src/JSON/Dataset.cpp index db26edf5..9029221b 100644 --- a/app/src/JSON/Dataset.cpp +++ b/app/src/JSON/Dataset.cpp @@ -22,6 +22,33 @@ #include "JSON/Dataset.h" +/** + * @brief Reads a value from a QJsonObject based on a key, returning a default + * value if the key does not exist. + * + * This function checks if the given key exists in the provided QJsonObject. + * If the key is found, it returns the associated value. Otherwise, it returns + * the specified default value. + * + * @param object The QJsonObject to read the data from. + * @param key The key to look for in the QJsonObject. + * @param defaultValue The value to return if the key is not found in the + * QJsonObject. + * @return The value associated with the key, or the defaultValue if the key is + * not present. + */ +static QVariant SAFE_READ(const QJsonObject &object, const QString &key, + const QVariant &defaultValue) +{ + if (object.contains(key)) + return object.value(key); + + return defaultValue; +} + +/** + * @brief Constructor function, initializes default values + */ JSON::Dataset::Dataset(const int groupId, const int datasetId) : m_fft(false) , m_led(false) @@ -36,6 +63,7 @@ JSON::Dataset::Dataset(const int groupId, const int datasetId) , m_min(0) , m_alarm(0) , m_ledHigh(1) + , m_xAxisId(-1) , m_fftSamples(256) , m_fftSamplingRate(100) , m_groupId(groupId) @@ -147,6 +175,15 @@ const QString &JSON::Dataset::widget() const return m_widget; } +/** + * @return The frame index for the data source for the x-axis, -1 when the + * x axis data should be automatically generated by Serial Studio. + */ +int JSON::Dataset::xAxisId() const +{ + return m_xAxisId; +} + /** * Returns the maximum freq. for the FFT transform */ @@ -210,6 +247,7 @@ QJsonObject JSON::Dataset::serialize() const object.insert(QStringLiteral("index"), m_index); object.insert(QStringLiteral("alarm"), m_alarm); object.insert(QStringLiteral("graph"), m_graph); + object.insert(QStringLiteral("xAxis"), m_xAxisId); object.insert(QStringLiteral("ledHigh"), m_ledHigh); object.insert(QStringLiteral("fftSamples"), m_fftSamples); object.insert(QStringLiteral("value"), m_value.simplified()); @@ -229,21 +267,22 @@ bool JSON::Dataset::read(const QJsonObject &object) { if (!object.isEmpty()) { - m_fft = object.value(QStringLiteral("fft")).toBool(); - m_led = object.value(QStringLiteral("led")).toBool(); - m_log = object.value(QStringLiteral("log")).toBool(); - m_min = object.value(QStringLiteral("min")).toDouble(); - m_max = object.value(QStringLiteral("max")).toDouble(); - m_index = object.value(QStringLiteral("index")).toInt(); - m_alarm = object.value(QStringLiteral("alarm")).toDouble(); - m_graph = object.value(QStringLiteral("graph")).toBool(); - m_ledHigh = object.value(QStringLiteral("ledHigh")).toDouble(); - m_fftSamples = object.value(QStringLiteral("fftSamples")).toInt(); - m_title = object.value(QStringLiteral("title")).toString().simplified(); - m_value = object.value(QStringLiteral("value")).toString().simplified(); - m_units = object.value(QStringLiteral("units")).toString().simplified(); - m_widget = object.value(QStringLiteral("widget")).toString().simplified(); - m_fftSamplingRate = object.value(QStringLiteral("fftSamplingRate")).toInt(); + m_min = SAFE_READ(object, "min", 0).toDouble(); + m_max = SAFE_READ(object, "max", 0).toDouble(); + m_index = SAFE_READ(object, "index", 0).toInt(); + m_fft = SAFE_READ(object, "fft", false).toBool(); + m_led = SAFE_READ(object, "led", false).toBool(); + m_log = SAFE_READ(object, "log", false).toBool(); + m_xAxisId = SAFE_READ(object, "xAxis", 0).toInt(); + m_alarm = SAFE_READ(object, "alarm", 0).toDouble(); + m_graph = SAFE_READ(object, "graph", false).toBool(); + m_ledHigh = SAFE_READ(object, "ledHigh", 0).toDouble(); + m_fftSamples = SAFE_READ(object, "fftSamples", 256).toInt(); + m_title = SAFE_READ(object, "title", "").toString().simplified(); + m_value = SAFE_READ(object, "value", "").toString().simplified(); + m_units = SAFE_READ(object, "units", "").toString().simplified(); + m_widget = SAFE_READ(object, "widget", "").toString().simplified(); + m_fftSamplingRate = SAFE_READ(object, "fftSamplingRate", 100).toInt(); if (m_value.isEmpty()) m_value = QStringLiteral("--.--"); diff --git a/app/src/JSON/Dataset.h b/app/src/JSON/Dataset.h index 8e183b07..ad996f27 100644 --- a/app/src/JSON/Dataset.h +++ b/app/src/JSON/Dataset.h @@ -83,6 +83,8 @@ public: [[nodiscard]] double max() const; [[nodiscard]] double alarm() const; [[nodiscard]] double ledHigh() const; + + [[nodiscard]] int xAxisId() const; [[nodiscard]] int fftSamples() const; [[nodiscard]] int fftSamplingRate() const; @@ -121,6 +123,7 @@ private: int m_fftSamplingRate; int m_groupId; + int m_xAxisId; int m_datasetId; friend class JSON::ProjectModel; diff --git a/app/src/JSON/Frame.cpp b/app/src/JSON/Frame.cpp index a84e07ff..9d64d085 100644 --- a/app/src/JSON/Frame.cpp +++ b/app/src/JSON/Frame.cpp @@ -22,6 +22,30 @@ #include "JSON/Frame.h" +/** + * @brief Reads a value from a QJsonObject based on a key, returning a default + * value if the key does not exist. + * + * This function checks if the given key exists in the provided QJsonObject. + * If the key is found, it returns the associated value. Otherwise, it returns + * the specified default value. + * + * @param object The QJsonObject to read the data from. + * @param key The key to look for in the QJsonObject. + * @param defaultValue The value to return if the key is not found in the + * QJsonObject. + * @return The value associated with the key, or the defaultValue if the key is + * not present. + */ +static QVariant SAFE_READ(const QJsonObject &object, const QString &key, + const QVariant &defaultValue) +{ + if (object.contains(key)) + return object.value(key); + + return defaultValue; +} + /** * Destructor function, free memory used by the @c Group objects before * destroying an instance of this class. @@ -96,16 +120,15 @@ bool JSON::Frame::read(const QJsonObject &object) // Get title & groups array const auto groups = object.value(QStringLiteral("groups")).toArray(); const auto actions = object.value(QStringLiteral("actions")).toArray(); - const auto title - = object.value(QStringLiteral("title")).toString().simplified(); + const auto title = SAFE_READ(object, "title", "").toString().simplified(); // We need to have a project title and at least one group if (!title.isEmpty() && !groups.isEmpty()) { // Update title m_title = title; - m_frameEnd = object.value(QStringLiteral("frameEnd")).toString(); - m_frameStart = object.value(QStringLiteral("frameStart")).toString(); + m_frameEnd = SAFE_READ(object, "frameEnd", "").toString(); + m_frameStart = SAFE_READ(object, "frameStart", "").toString(); // Generate groups & datasets from data frame for (auto i = 0; i < groups.count(); ++i) diff --git a/app/src/JSON/Group.cpp b/app/src/JSON/Group.cpp index 13a36b82..53b8df66 100644 --- a/app/src/JSON/Group.cpp +++ b/app/src/JSON/Group.cpp @@ -23,6 +23,30 @@ #include #include "JSON/Group.h" +/** + * @brief Reads a value from a QJsonObject based on a key, returning a default + * value if the key does not exist. + * + * This function checks if the given key exists in the provided QJsonObject. + * If the key is found, it returns the associated value. Otherwise, it returns + * the specified default value. + * + * @param object The QJsonObject to read the data from. + * @param key The key to look for in the QJsonObject. + * @param defaultValue The value to return if the key is not found in the + * QJsonObject. + * @return The value associated with the key, or the defaultValue if the key is + * not present. + */ +static QVariant SAFE_READ(const QJsonObject &object, const QString &key, + const QVariant &defaultValue) +{ + if (object.contains(key)) + return object.value(key); + + return defaultValue; +} + /** * @brief Constructor function */ @@ -78,8 +102,8 @@ bool JSON::Group::read(const QJsonObject &object) { // clang-format off const auto array = object.value(QStringLiteral("datasets")).toArray(); - const auto title = object.value(QStringLiteral("title")).toString().simplified(); - const auto widget = object.value(QStringLiteral("widget")).toString().simplified(); + const auto title = SAFE_READ(object, "title", "").toString().simplified(); + const auto widget = SAFE_READ(object, "widget", "").toString().simplified(); // clang-format on if (!title.isEmpty() && !array.isEmpty()) diff --git a/app/src/JSON/ProjectModel.cpp b/app/src/JSON/ProjectModel.cpp index 11c107d1..ecff1de5 100644 --- a/app/src/JSON/ProjectModel.cpp +++ b/app/src/JSON/ProjectModel.cpp @@ -73,19 +73,20 @@ typedef enum // clang-format off typedef enum { - kDatasetView_Title, /**< Represents the dataset title item. */ - kDatasetView_Index, /**< Represents the dataset frame index item. */ - kDatasetView_Units, /**< Represents the dataset units item. */ - kDatasetView_Widget, /**< Represents the dataset widget item. */ - kDatasetView_FFT, /**< Represents the FFT plot checkbox item. */ - kDatasetView_LED, /**< Represents the LED panel checkbox item. */ - kDatasetView_LED_High, /**< Represents the LED high (on) value item. */ - kDatasetView_Plot, /**< Represents the dataset plot mode item. */ - kDatasetView_Min, /**< Represents the dataset minimum value item. */ - kDatasetView_Max, /**< Represents the dataset maximum value item. */ - kDatasetView_Alarm, /**< Represents the dataset alarm value item. */ - kDatasetView_FFT_Samples, /**< Represents the FFT window size item. */ + kDatasetView_Title, /**< Represents the dataset title item. */ + kDatasetView_Index, /**< Represents the dataset frame index item. */ + kDatasetView_Units, /**< Represents the dataset units item. */ + kDatasetView_Widget, /**< Represents the dataset widget item. */ + kDatasetView_FFT, /**< Represents the FFT plot checkbox item. */ + kDatasetView_LED, /**< Represents the LED panel checkbox item. */ + kDatasetView_LED_High, /**< Represents the LED high (on) value item. */ + kDatasetView_Plot, /**< Represents the dataset plot mode item. */ + kDatasetView_Min, /**< Represents the dataset minimum value item. */ + kDatasetView_Max, /**< Represents the dataset maximum value item. */ + kDatasetView_Alarm, /**< Represents the dataset alarm value item. */ + kDatasetView_FFT_Samples, /**< Represents the FFT window size item. */ kDatasetView_FFT_SamplingRate, /**< Represents the FFT sampling rate item. */ + kDatasetView_xAxis /**< Represents the plot X axis item. */ } DatasetItem; // clang-format on @@ -347,6 +348,39 @@ QString JSON::ProjectModel::selectedIcon() const return data.toString(); } +/** + * @brief Retrieves a list of available X-axis data sources. + * + * This function returns a list of X-axis data source names. It includes a + * default entry ("Samples") and all registered datasets from the project model. + * Each dataset is identified by its title and the title of the group it belongs + * to. + * + * @return A `QStringList` containing the names of X-axis data sources. + */ +QStringList JSON::ProjectModel::xDataSources() const +{ + QStringList list; + list.append(tr("Samples")); + + QMap registeredDatasets; + for (const auto &group : m_groups) + { + for (const auto &dataset : group.datasets()) + { + const auto index = dataset.index(); + if (!registeredDatasets.contains(index)) + { + const auto t = QString("%1 (%2)").arg(dataset.title(), group.title()); + registeredDatasets.insert(index, t); + list.append(t); + } + } + } + + return list; +} + /** * @brief Retrieves the project title. * @@ -2315,6 +2349,7 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset) const bool showWidget = currentDatasetIsEditable(); const bool showFFTOptions = dataset.fft(); const bool showLedOptions = dataset.led(); + const bool showPlotOptions = dataset.graph(); const bool showMinMax = dataset.graph() || dataset.widget() == "gauge" || dataset.widget() == "bar" || m_selectedGroup.widget() == "multiplot"; @@ -2388,6 +2423,90 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset) m_datasetModel->appendRow(widget); } + // Get appropiate plotting mode index for current dataset + int plotIndex = 0; + bool found = false; + const auto currentPair = qMakePair(dataset.graph(), dataset.log()); + for (auto it = m_plotOptions.begin(); it != m_plotOptions.end(); + ++it, ++plotIndex) + { + if (it.key() == currentPair) + { + found = true; + break; + } + } + + // If not found, reset the index to 0 + if (!found) + plotIndex = 0; + + // Add plotting mode + auto plot = new QStandardItem(); + plot->setEditable(true); + plot->setData(ComboBox, WidgetType); + plot->setData(m_plotOptions.values(), ComboBoxData); + plot->setData(plotIndex, EditableValue); + plot->setData(tr("Oscilloscope Plot"), ParameterName); + plot->setData(kDatasetView_Plot, ParameterType); + plot->setData(tr("Plot data in real-time"), ParameterDescription); + m_datasetModel->appendRow(plot); + + // Add FFT checkbox + auto fft = new QStandardItem(); + fft->setEditable(true); + fft->setData(CheckBox, WidgetType); + fft->setData(dataset.fft(), EditableValue); + fft->setData(tr("FFT Plot"), ParameterName); + fft->setData(kDatasetView_FFT, ParameterType); + fft->setData(0, PlaceholderValue); + fft->setData(tr("Plot frequency-domain data"), ParameterDescription); + m_datasetModel->appendRow(fft); + + // Add LED panel checkbox + auto led = new QStandardItem(); + led->setEditable(true); + led->setData(CheckBox, WidgetType); + led->setData(dataset.led(), EditableValue); + led->setData(tr("Show in LED Panel"), ParameterName); + led->setData(kDatasetView_LED, ParameterType); + led->setData(0, PlaceholderValue); + led->setData(tr("Quick status monitoring"), ParameterDescription); + m_datasetModel->appendRow(led); + + // Add X-axis selector + if (showPlotOptions) + { + // Ensure X-axis ID is reset to "Samples" when an invalid index is set + int xAxisIdx = 0; + for (const auto &group : m_groups) + { + for (const auto &dataset : group.datasets()) + { + const auto index = dataset.index(); + if (index == m_selectedDataset.xAxisId()) + { + xAxisIdx = index; + break; + } + } + + if (xAxisIdx != 0) + break; + } + + // Construct item + auto xAxis = new QStandardItem(); + xAxis->setEditable(true); + xAxis->setData(ComboBox, WidgetType); + xAxis->setData(xAxisIdx, EditableValue); + xAxis->setData(xDataSources(), ComboBoxData); + xAxis->setData(kDatasetView_xAxis, ParameterType); + xAxis->setData(tr("X-Axis Source"), ParameterName); + xAxis->setData(tr("Data series for the X-Axis"), ParameterDescription); + m_datasetModel->appendRow(xAxis); + } + // Add minimum/maximum values if (showMinMax) { @@ -2431,46 +2550,6 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset) m_datasetModel->appendRow(alarm); } - // Get appropiate plotting mode index for current dataset - int plotIndex = 0; - bool found = false; - const auto currentPair = qMakePair(dataset.graph(), dataset.log()); - for (auto it = m_plotOptions.begin(); it != m_plotOptions.end(); - ++it, ++plotIndex) - { - if (it.key() == currentPair) - { - found = true; - break; - } - } - - // If not found, reset the index to 0 - if (!found) - plotIndex = 0; - - // Add plotting mode - auto plot = new QStandardItem(); - plot->setEditable(true); - plot->setData(ComboBox, WidgetType); - plot->setData(m_plotOptions.values(), ComboBoxData); - plot->setData(plotIndex, EditableValue); - plot->setData(tr("Oscilloscope Plot"), ParameterName); - plot->setData(kDatasetView_Plot, ParameterType); - plot->setData(tr("Plot data in real-time"), ParameterDescription); - m_datasetModel->appendRow(plot); - - // Add FFT checkbox - auto fft = new QStandardItem(); - fft->setEditable(true); - fft->setData(CheckBox, WidgetType); - fft->setData(dataset.fft(), EditableValue); - fft->setData(tr("FFT Plot"), ParameterName); - fft->setData(kDatasetView_FFT, ParameterType); - fft->setData(0, PlaceholderValue); - fft->setData(tr("Plot frequency-domain data"), ParameterDescription); - m_datasetModel->appendRow(fft); - // FFT-specific options if (showFFTOptions) { @@ -2504,17 +2583,6 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset) m_datasetModel->appendRow(fftSamplingRate); } - // Add LED panel checkbox - auto led = new QStandardItem(); - led->setEditable(true); - led->setData(CheckBox, WidgetType); - led->setData(dataset.led(), EditableValue); - led->setData(tr("Show in LED Panel"), ParameterName); - led->setData(kDatasetView_LED, ParameterType); - led->setData(0, PlaceholderValue); - led->setData(tr("Quick status monitoring"), ParameterDescription); - m_datasetModel->appendRow(led); - // Add LED High value if (showLedOptions) { @@ -2982,6 +3050,9 @@ void JSON::ProjectModel::onDatasetItemChanged(QStandardItem *item) m_selectedDataset.m_log = plotOptions.at(value.toInt()).second; buildDatasetModel(m_selectedDataset); break; + case kDatasetView_xAxis: + m_selectedDataset.m_xAxisId = value.toInt(); + break; case kDatasetView_Min: m_selectedDataset.m_min = value.toFloat(); break; diff --git a/app/src/JSON/ProjectModel.h b/app/src/JSON/ProjectModel.h index 4ed99559..6767de26 100644 --- a/app/src/JSON/ProjectModel.h +++ b/app/src/JSON/ProjectModel.h @@ -224,6 +224,8 @@ public: [[nodiscard]] QString selectedText() const; [[nodiscard]] QString selectedIcon() const; + [[nodiscard]] QStringList xDataSources() const; + [[nodiscard]] const QString actionIcon() const; [[nodiscard]] const QStringList &availableActionIcons() const; diff --git a/app/src/SerialStudio.h b/app/src/SerialStudio.h index c6b808c2..6ba713f7 100644 --- a/app/src/SerialStudio.h +++ b/app/src/SerialStudio.h @@ -29,16 +29,60 @@ #include "JSON/Dataset.h" /** - * @typedef Curve - * @brief Defines a plot series or curve as a vector of real (qreal) values. + * @typedef PlotDataX + * @brief Represents the unique X-axis data points for a plot. + * + * The X-axis data points are stored as a set of unique `qreal` values. + * This ensures that each X value is distinct, which is essential for correct + * rendering of the plot. The set is inherently ordered in ascending order. */ -typedef QVector Curve; +typedef QVector PlotDataX; /** - * @typedef MultipleCurves - * @brief Defines a collection of curves, used for representing multiple plots. + * @typedef PlotDataY + * @brief Represents the Y-axis data points for a single curve. + * + * The Y-axis data points are stored as a vector of `qreal` values. + * Unlike X-axis data, Y values can have duplicates and are directly + * mapped to the corresponding X values during plotting. */ -typedef QVector MultipleCurves; +typedef QVector PlotDataY; + +/** + * @typedef MultiPlotDataY + * @brief Represents Y-axis data for multiple curves in a multiplot. + * + * A vector of `PlotDataY` structures, where each element represents + * the Y-axis data for one curve in a multiplot widget. This allows + * managing multiple curves independently. + */ +typedef QVector MultiPlotDataY; + +/** + * @typedef LineSeries + * @brief Represents a paired series of X-axis and Y-axis data for a plot. + * + * The `LineSeries` type is defined as a `QPair` containing: + * - A pointer to `PlotDataX`, which holds the unique X-axis values. + * - A pointer to `PlotDataY`, which holds the Y-axis values. + * + * This type simplifies data processing by tightly coupling the related X and Y + * data for a plot, ensuring that they are always accessed and managed together. + */ +typedef struct +{ + PlotDataX *x; + PlotDataY *y; +} LineSeries; + +/** + * @typedef MultiLineSeries + */ +typedef struct +{ + PlotDataX *x; + QList y; +} MultiLineSeries; /** * @class SerialStudio diff --git a/app/src/UI/Dashboard.cpp b/app/src/UI/Dashboard.cpp index b1947491..addc7e17 100644 --- a/app/src/UI/Dashboard.cpp +++ b/app/src/UI/Dashboard.cpp @@ -448,6 +448,22 @@ QStringList UI::Dashboard::actionTitles() const return titles; } +/** + * @brief Provides access to the map of dataset objects. + * + * This function returns a constant reference to the map that associates dataset + * indexes with their corresponding `JSON::Dataset` objects. + * + * @return A constant reference to the `QMap` mapping dataset indexes (`int`) + * to their respective `JSON::Dataset` objects. + * + * @note The map can be used to retrieve datasets by their index. + */ +const QMap &UI::Dashboard::datasets() const +{ + return m_datasets; +} + /** * @brief Provides access to a specific group widget based on widget type and * relative index. @@ -493,48 +509,52 @@ const JSON::Frame &UI::Dashboard::currentFrame() /** * @brief Provides the FFT plot values currently displayed on the dashboard. - * @return A reference to a QVector containing the FFT Curve data. + * @return A reference to a QVector containing the FFT PlotDataY data. */ -const QVector &UI::Dashboard::fftPlotValues() +const PlotDataY &UI::Dashboard::fftData(const int index) const { - return m_fftPlotValues; + return m_fftValues[index]; } /** * @brief Provides the linear plot values currently displayed on the dashboard. - * @return A reference to a QVector containing the linear Curve data. + * @return A reference to a QVector containing the linear PlotDataY data. */ -const QVector &UI::Dashboard::linearPlotValues() +const LineSeries &UI::Dashboard::plotData(const int index) const { - return m_linearPlotValues; + return m_pltValues[index]; } /** * @brief Provides the values for multiplot visuals on the dashboard. - * @return A reference to a QVector containing MultipleCurves data. + * @return A reference to a QVector containing MultiPlotDataY data. */ -const QVector &UI::Dashboard::multiplotValues() +const MultiLineSeries &UI::Dashboard::multiplotData(const int index) const { - return m_multiplotValues; + return m_multipltValues[index]; } /** - * @brief Sets the number of data points displayed in the dashboard plots. - * Clears existing multiplot and linear plot values and emits the - * @c pointsChanged signal. + * @brief Sets the number of data points for the dashboard plots. * - * @param points The new point/sample count. + * This function updates the total number of points (samples) used in the plots + * and reconfigures the data structures for linear and multi-line series to + * reflect the new point count. + * + * @param points The new number of data points (samples). */ void UI::Dashboard::setPoints(const int points) { if (m_points != points) { + // Update number of points m_points = points; - m_multiplotValues.clear(); - m_linearPlotValues.clear(); - m_multiplotValues.squeeze(); - m_linearPlotValues.squeeze(); + // Update plot data structures + configureLineSeries(); + configureMultiLineSeries(); + + // Update the UI Q_EMIT pointsChanged(); } } @@ -592,12 +612,18 @@ void UI::Dashboard::setShowLegends(const bool enabled) void UI::Dashboard::resetData(const bool notify) { // Clear plotting data - m_fftPlotValues.clear(); - m_multiplotValues.clear(); - m_linearPlotValues.clear(); - m_fftPlotValues.squeeze(); - m_multiplotValues.squeeze(); - m_linearPlotValues.squeeze(); + m_fftValues.clear(); + m_pltValues.clear(); + m_multipltValues.clear(); + + // Free memory associated with the containers of the plotting data + m_fftValues.squeeze(); + m_pltValues.squeeze(); + m_multipltValues.squeeze(); + + // Clear X/Y axis arrays + m_xAxisData.clear(); + m_yAxisData.clear(); // Clear widget & action structures m_widgetCount = 0; @@ -662,81 +688,62 @@ void UI::Dashboard::setWidgetVisible(const SerialStudio::DashboardWidget widget, } /** - * @brief Updates plot data for linear, FFT, and multiplot widgets on the - * dashboard. + * @brief Updates the plot data for all dashboard widgets. * - * This function checks and initializes the data structures for each plot type - * (linear plots, FFT plots, and multiplots) if needed. + * This function ensures that the data structures for FFT plots, linear plots, + * and multiplots are correctly initialized and updated with the latest values + * from the datasets. It handles reinitialization if the widget count changes + * and shifts data to accommodate new samples. * - * It then appends the latest values from the data sources to these plots by - * shifting older data back and adding new data to the end. + * @note This function is typically called in real-time to keep plots + * synchronized with incoming data. */ void UI::Dashboard::updatePlots() { - // Check if we need to re-initialize linear plots data - if (m_linearPlotValues.count() != widgetCount(SerialStudio::DashboardPlot)) - { - m_linearPlotValues.clear(); - m_linearPlotValues.squeeze(); - for (int i = 0; i < widgetCount(SerialStudio::DashboardPlot); ++i) - { - m_linearPlotValues.append(Curve()); - m_linearPlotValues.last().resize(points() + 1); - SIMD::fill(m_linearPlotValues.last().data(), points() + 1, 0); - } - } - // Check if we need to re-initialize FFT plots data - if (m_fftPlotValues.count() != widgetCount(SerialStudio::DashboardFFT)) - { - m_fftPlotValues.clear(); - m_fftPlotValues.squeeze(); - for (int i = 0; i < widgetCount(SerialStudio::DashboardFFT); ++i) - { - const auto &dataset = getDatasetWidget(SerialStudio::DashboardFFT, i); - m_fftPlotValues.append(Curve()); - m_fftPlotValues.last().resize(dataset.fftSamples()); - SIMD::fill(m_fftPlotValues.last().data(), dataset.fftSamples(), 0); - } - } + if (m_fftValues.count() != widgetCount(SerialStudio::DashboardFFT)) + configureFftSeries(); + + // Check if we need to re-initialize linear plots data + if (m_pltValues.count() != widgetCount(SerialStudio::DashboardPlot)) + configureLineSeries(); // Check if we need to re-initialize multiplot data - if (m_multiplotValues.count() - != widgetCount(SerialStudio::DashboardMultiPlot)) - { - m_multiplotValues.clear(); - m_multiplotValues.squeeze(); - for (int i = 0; i < widgetCount(SerialStudio::DashboardMultiPlot); ++i) - { - const auto &group = getGroupWidget(SerialStudio::DashboardMultiPlot, i); - m_multiplotValues.append(MultipleCurves()); - m_multiplotValues.last().resize(group.datasetCount()); - for (int j = 0; j < group.datasetCount(); ++j) - { - m_multiplotValues[i][j].resize(points() + 1); - SIMD::fill(m_multiplotValues[i][j].data(), points() + 1, 0); - } - } - } - - // Append latest values to linear plots data - for (int i = 0; i < widgetCount(SerialStudio::DashboardPlot); ++i) - { - const auto &dataset = getDatasetWidget(SerialStudio::DashboardPlot, i); - auto *data = m_linearPlotValues[i].data(); - auto count = m_linearPlotValues[i].count(); - SIMD::shift(data, count, dataset.value().toFloat()); - } + if (m_multipltValues.count() != widgetCount(SerialStudio::DashboardMultiPlot)) + configureMultiLineSeries(); // Append latest values to FFT plots data for (int i = 0; i < widgetCount(SerialStudio::DashboardFFT); ++i) { const auto &dataset = getDatasetWidget(SerialStudio::DashboardFFT, i); - auto *data = m_fftPlotValues[i].data(); - auto count = m_fftPlotValues[i].count(); + auto *data = m_fftValues[i].data(); + auto count = m_fftValues[i].count(); SIMD::shift(data, count, dataset.value().toFloat()); } + // Append latest values to linear plots data + for (int i = 0; i < widgetCount(SerialStudio::DashboardPlot); ++i) + { + const auto &yDataset = getDatasetWidget(SerialStudio::DashboardPlot, i); + if (m_datasets.contains(yDataset.xAxisId())) + { + const auto &xDataset = m_datasets[yDataset.xAxisId()]; + auto *xData = m_xAxisData[xDataset.index()].data(); + auto *yData = m_yAxisData[yDataset.index()].data(); + auto xCount = m_xAxisData[xDataset.index()].count(); + auto yCount = m_yAxisData[yDataset.index()].count(); + SIMD::shift(xData, xCount, xDataset.value().toFloat()); + SIMD::shift(yData, yCount, yDataset.value().toFloat()); + } + + else + { + auto *data = m_yAxisData[yDataset.index()].data(); + auto count = m_yAxisData[yDataset.index()].count(); + SIMD::shift(data, count, yDataset.value().toFloat()); + } + } + // Append latest values to multiplots data for (int i = 0; i < widgetCount(SerialStudio::DashboardMultiPlot); ++i) { @@ -744,13 +751,164 @@ void UI::Dashboard::updatePlots() for (int j = 0; j < group.datasetCount(); ++j) { const auto &dataset = group.datasets()[j]; - auto *data = m_multiplotValues[i][j].data(); - auto count = m_multiplotValues[i][j].count(); + auto *data = m_multipltValues[i].y[j].data(); + auto count = m_multipltValues[i].y[j].count(); SIMD::shift(data, count, dataset.value().toFloat()); } } } +/** + * @brief Configures the FFT series data structure for the dashboard. + * + * This function clears existing FFT values and initializes the data structure + * for each FFT plot widget with a predefined number of samples, filling it with + * zeros. + * + * @note Typically called during dashboard setup or reset to prepare FFT plot + * widgets for rendering. + */ +void UI::Dashboard::configureFftSeries() +{ + // Clear memory + m_fftValues.clear(); + m_fftValues.squeeze(); + + // Construct FFT plot data structure + for (int i = 0; i < widgetCount(SerialStudio::DashboardFFT); ++i) + { + const auto &dataset = getDatasetWidget(SerialStudio::DashboardFFT, i); + m_fftValues.append(PlotDataY()); + m_fftValues.last().resize(dataset.fftSamples()); + SIMD::fill(m_fftValues.last().data(), dataset.fftSamples(), 0); + } +} + +/** + * @brief Configures the line series data structure for the dashboard. + * + * This function clears and reinitializes the X-axis and Y-axis data arrays, + * as well as the plot values structure (`m_pltValues`). It associates each + * dataset with its respective X and Y data, creating `LineSeries` objects + * for plotting. + * + * - If a dataset specifies an X-axis source, the corresponding data is used. + * - Otherwise, the default X-axis (based on sample points) is used. + * + * @note Typically called during dashboard setup or reset to prepare plot + * widgets for rendering. + */ +void UI::Dashboard::configureLineSeries() +{ + // Clear memory + m_xAxisData.clear(); + m_yAxisData.clear(); + m_pltValues.clear(); + m_pltValues.squeeze(); + + // Reset default X-axis data + m_defaultXAxis.clear(); + m_defaultXAxis.squeeze(); + m_defaultXAxis.reserve(points() + 1); + for (int i = 0; i < points() + 1; ++i) + m_defaultXAxis.append(i); + + // Construct X/Y axis data arrays + for (auto i = m_widgetDatasets.begin(); i != m_widgetDatasets.end(); ++i) + { + // Obtain list of datasets for a widget type + const auto &datasets = i.value(); + + // Iterate over all the datasets + for (auto d = datasets.begin(); d != datasets.end(); ++d) + { + if (d->graph()) + { + // Register X-axis + PlotDataY yAxis; + m_yAxisData.insert(d->index(), yAxis); + + // Register X-axis + int xSource = d->xAxisId(); + if (!m_xAxisData.contains(xSource)) + { + PlotDataX xAxis; + if (m_datasets.contains(xSource)) + m_xAxisData.insert(xSource, xAxis); + } + } + } + } + + // Construct plot values structure + for (int i = 0; i < widgetCount(SerialStudio::DashboardPlot); ++i) + { + // Obtain Y-axis data + const auto &yDataset = getDatasetWidget(SerialStudio::DashboardPlot, i); + + // Add X-axis data & generate a line series with X/Y data + if (m_datasets.contains(yDataset.xAxisId())) + { + const auto &xDataset = m_datasets[yDataset.xAxisId()]; + m_xAxisData[xDataset.index()].resize(points() + 1); + m_yAxisData[yDataset.index()].resize(points() + 1); + SIMD::fill(m_xAxisData[xDataset.index()].data(), points() + 1, 0); + SIMD::fill(m_yAxisData[yDataset.index()].data(), points() + 1, 0); + + LineSeries series; + series.x = &m_xAxisData[xDataset.index()]; + series.y = &m_yAxisData[yDataset.index()]; + m_pltValues.append(series); + } + + // Only use Y-axis data, use samples/points as X-axis + else + { + m_yAxisData[yDataset.index()].resize(points() + 1); + SIMD::fill(m_yAxisData[yDataset.index()].data(), points() + 1, 0); + + LineSeries series; + series.x = &m_defaultXAxis; + series.y = &m_yAxisData[yDataset.index()]; + m_pltValues.append(series); + } + } +} + +/** + * @brief Configures the multi-line series data structure for the dashboard. + * + * This function initializes the data structure used for multi-plot widgets. + * It assigns the default X-axis to all multi-line series and creates a + * `PlotDataY` vector for each dataset in the group, initializing it with zeros. + * + * @note Typically called during dashboard setup or reset to prepare multi-plot + * widgets for rendering. + */ +void UI::Dashboard::configureMultiLineSeries() +{ + // Clear data + m_multipltValues.clear(); + m_multipltValues.squeeze(); + + // Construct multi-plot values structure + for (int i = 0; i < widgetCount(SerialStudio::DashboardMultiPlot); ++i) + { + const auto &group = getGroupWidget(SerialStudio::DashboardMultiPlot, i); + + MultiLineSeries series; + series.x = &m_defaultXAxis; + for (int j = 0; j < group.datasetCount(); ++j) + { + series.y.append(PlotDataY()); + series.y.last().resize(points() + 1); + SIMD::fill(series.y.last().data(), points() + 1, 0); + } + + m_multipltValues.append(series); + } +} + /** * @brief Processes and updates the dashboard data based on a new frame. * @@ -803,6 +961,7 @@ void UI::Dashboard::processFrame(const JSON::Frame &frame) Q_EMIT actionCountChanged(); // Update widget data structures + m_datasets.clear(); JSON::Group ledPanel; for (const auto &group : frame.groups()) { @@ -816,6 +975,7 @@ void UI::Dashboard::processFrame(const JSON::Frame &frame) for (const auto &dataset : group.datasets()) { + m_datasets.insert(dataset.index(), dataset); auto keys = SerialStudio::getDashboardWidgets(dataset); for (const auto &key : keys) { @@ -904,6 +1064,11 @@ void UI::Dashboard::processFrame(const JSON::Frame &frame) } } + // Clear plot data setup + m_fftValues.clear(); + m_pltValues.clear(); + m_multipltValues.clear(); + // Update user interface Q_EMIT widgetCountChanged(); Q_EMIT widgetVisibilityChanged(); diff --git a/app/src/UI/Dashboard.h b/app/src/UI/Dashboard.h index 5dc653d4..3e782203 100644 --- a/app/src/UI/Dashboard.h +++ b/app/src/UI/Dashboard.h @@ -134,14 +134,16 @@ public: [[nodiscard]] QStringList actionTitles() const; // clang-format off + [[nodiscard]] const QMap &datasets() const; [[nodiscard]] const JSON::Group &getGroupWidget(const SerialStudio::DashboardWidget widget, const int index) const; [[nodiscard]] const JSON::Dataset &getDatasetWidget(const SerialStudio::DashboardWidget widget, const int index) const; // clang-format on [[nodiscard]] const JSON::Frame ¤tFrame(); - [[nodiscard]] const QVector &fftPlotValues(); - [[nodiscard]] const QVector &linearPlotValues(); - [[nodiscard]] const QVector &multiplotValues(); + + [[nodiscard]] const PlotDataY &fftData(const int index) const; + [[nodiscard]] const LineSeries &plotData(const int index) const; + [[nodiscard]] const MultiLineSeries &multiplotData(const int index) const; public slots: void setPoints(const int points); @@ -155,6 +157,9 @@ public slots: private slots: void updatePlots(); + void configureFftSeries(); + void configureLineSeries(); + void configureMultiLineSeries(); void processFrame(const JSON::Frame &frame); private: @@ -165,11 +170,16 @@ private: bool m_updateRequired; SerialStudio::AxisVisibility m_axisVisibility; - QVector m_fftPlotValues; - QVector m_linearPlotValues; - QVector m_multiplotValues; + PlotDataX m_defaultXAxis; + QMap m_xAxisData; + QMap m_yAxisData; + + QVector m_fftValues; + QVector m_pltValues; + QVector m_multipltValues; QVector m_actions; + QMap m_datasets; QList m_availableWidgets; QMap> m_widgetMap; QMap> m_widgetVisibility; diff --git a/app/src/UI/Widgets/FFTPlot.cpp b/app/src/UI/Widgets/FFTPlot.cpp index 193d2d55..d30deb1b 100644 --- a/app/src/UI/Widgets/FFTPlot.cpp +++ b/app/src/UI/Widgets/FFTPlot.cpp @@ -146,15 +146,12 @@ void Widgets::FFTPlot::updateData() if (!isEnabled()) return; - // Get the plot data - auto dash = &UI::Dashboard::instance(); - auto plotData = dash->fftPlotValues(); - - // If the plot data is valid, update the data - if (plotData.count() > m_index) + if (VALIDATE_WIDGET(SerialStudio::DashboardFFT, m_index)) { + // Get the plot data + const auto &data = UI::Dashboard::instance().fftData(m_index); + // Obtain samples from data - const auto &data = plotData.at(m_index); for (int i = 0; i < m_size; ++i) m_samples[i] = static_cast(data[i]); diff --git a/app/src/UI/Widgets/MultiPlot.cpp b/app/src/UI/Widgets/MultiPlot.cpp index aff56db3..af814d0c 100644 --- a/app/src/UI/Widgets/MultiPlot.cpp +++ b/app/src/UI/Widgets/MultiPlot.cpp @@ -194,19 +194,15 @@ void Widgets::MultiPlot::updateData() if (VALIDATE_WIDGET(SerialStudio::DashboardMultiPlot, m_index)) { - const auto &plotData = UI::Dashboard::instance().multiplotValues(); - if (m_index >= 0 && plotData.count() > m_index) + const auto &data = UI::Dashboard::instance().multiplotData(m_index); + for (int i = 0; i < data.y.count(); ++i) { - const auto &curves = plotData[m_index]; - for (int i = 0; i < curves.count(); ++i) - { - const auto &values = curves[i]; - if (m_data[i].count() != values.count()) - m_data[i].resize(values.count()); + const auto &series = data.y[i]; + if (m_data[i].count() != series.count()) + m_data[i].resize(series.count()); - for (int j = 0; j < values.count(); ++j) - m_data[i][j] = QPointF(j, values[j]); - } + for (int j = 0; j < series.count(); ++j) + m_data[i][j] = QPointF(data.x->at(j), series[j]); } } } diff --git a/app/src/UI/Widgets/Plot.cpp b/app/src/UI/Widgets/Plot.cpp index 8789d87b..1a66eabb 100644 --- a/app/src/UI/Widgets/Plot.cpp +++ b/app/src/UI/Widgets/Plot.cpp @@ -39,14 +39,26 @@ Widgets::Plot::Plot(const int index, QQuickItem *parent) { if (VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) { - const auto &dataset = GET_DATASET(SerialStudio::DashboardPlot, m_index); + const auto &yDataset = GET_DATASET(SerialStudio::DashboardPlot, m_index); - m_yLabel = dataset.title(); - m_minY = qMin(dataset.min(), dataset.max()); - m_maxY = qMax(dataset.min(), dataset.max()); + m_minY = qMin(yDataset.min(), yDataset.max()); + m_maxY = qMax(yDataset.min(), yDataset.max()); - if (!dataset.units().isEmpty()) - m_yLabel += " (" + dataset.units() + ")"; + const auto xAxisId = yDataset.xAxisId(); + if (UI::Dashboard::instance().datasets().contains(xAxisId)) + { + const auto &xDataset = UI::Dashboard::instance().datasets()[xAxisId]; + m_xLabel = xDataset.title(); + if (!xDataset.units().isEmpty()) + m_xLabel += " (" + xDataset.units() + ")"; + } + + else + m_xLabel = tr("Samples"); + + m_yLabel = yDataset.title(); + if (!yDataset.units().isEmpty()) + m_yLabel += " (" + yDataset.units() + ")"; connect(&UI::Dashboard::instance(), &UI::Dashboard::updated, this, &Plot::updateData); @@ -121,6 +133,15 @@ const QString &Widgets::Plot::yLabel() const return m_yLabel; } +/** + * @brief Returns the X-axis label. + * @return The X-axis label. + */ +const QString &Widgets::Plot::xLabel() const +{ + return m_xLabel; +} + /** * @brief Draws the data on the given QLineSeries. * @param series The QLineSeries to draw the data on. @@ -145,16 +166,27 @@ void Widgets::Plot::updateData() if (VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) { - const auto &plotData = UI::Dashboard::instance().linearPlotValues(); + // Get plotting data + const auto &plotData = UI::Dashboard::instance().plotData(m_index); + const auto X = plotData.x; + const auto Y = plotData.y; - if (m_index >= 0 && plotData.count() > m_index) + // Resize series array if required + if (m_data.count() != X->count()) + m_data.resize(X->count()); + + // Convert data to a list of points + int i = 0; + for (auto x = X->begin(); x != X->end(); ++x) { - const auto &values = plotData[m_index]; - if (m_data.count() != values.count()) - m_data.resize(UI::Dashboard::instance().points()); + if (Y->count() > i) + { + m_data[i] = QPointF(*x, Y->at(i)); + ++i; + } - for (int i = 0; i < values.count(); ++i) - m_data[i] = QPointF(i, values[i]); + else + break; } } } @@ -164,45 +196,137 @@ void Widgets::Plot::updateData() */ void Widgets::Plot::updateRange() { - // Reserve the number of points in the dashboard + // Clear memory m_data.clear(); m_data.squeeze(); m_data.resize(UI::Dashboard::instance().points() + 1); - // Update x-axis - m_minX = 0; - m_maxX = UI::Dashboard::instance().points(); + // Obtain dataset information + if (VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) + { + const auto &yD = GET_DATASET(SerialStudio::DashboardPlot, m_index); + if (yD.xAxisId() > 0) + { + const auto &xD = UI::Dashboard::instance().datasets()[yD.xAxisId()]; + m_minX = xD.min(); + m_maxX = xD.max(); + } + + else + { + m_minX = 0; + m_maxX = UI::Dashboard::instance().points(); + } + } // Update the plot Q_EMIT rangeChanged(); } /** - * @brief Calculates the auto-scale range for the Y-axis. + * @brief Calculates the auto-scale range for both X and Y axes of the plot. + * + * This function determines the minimum and maximum values for the X and Y axes + * of the plot based on the associated dataset. If the X-axis data source is set + * to a specific dataset, its range is computed; otherwise, the range defaults + * to `[0, points]`. For the Y-axis, the range is always determined from the + * dataset values. + * + * @note The function emits the `rangeChanged()` signal if either the X or Y + * range is updated. */ void Widgets::Plot::calculateAutoScaleRange() +{ + // Validate that the dataset exists + if (!VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) + return; + + // Initialize parameters + bool xChanged = false; + bool yChanged = false; + + // Obtain scale range for Y-axis + // clang-format off + const auto &yDataset = GET_DATASET(SerialStudio::DashboardPlot, m_index); + yChanged = computeMinMaxValues(m_minY, m_maxY, yDataset, true, [](const QPointF &p) { return p.y(); }); + // clang-format on + + // Obtain range scale for X-axis + // clang-format off + if (UI::Dashboard::instance().datasets().contains(yDataset.xAxisId())) + { + const auto &xDataset = UI::Dashboard::instance().datasets()[yDataset.xAxisId()]; + xChanged = computeMinMaxValues(m_minX, m_maxX, xDataset, false, [](const QPointF &p) { return p.x(); }); + } + // clang-format on + + // X-axis data source set to samples, use [0, points] as range + else + { + const auto points = UI::Dashboard::instance().points(); + + if (m_minX != 0 || m_maxX != points) + { + m_minX = 0; + m_maxX = points; + xChanged = true; + } + } + + // Update user interface + if (xChanged || yChanged) + Q_EMIT rangeChanged(); +} + +/** + * @brief Computes the minimum and maximum values for a given axis of the plot. + * + * This templated function calculates the minimum and maximum values for a plot + * axis (either X or Y) using the provided dataset and an extractor function. If + * the dataset has no valid range or is empty, a fallback range `[0, 1]` or an + * adjusted range is applied. + * + * @tparam Extractor A callable object (e.g., lambda) used to extract values + * from data points. + * + * @param min Reference to the variable storing the minimum value. + * @param max Reference to the variable storing the maximum value. + * @param dataset The dataset to compute the range from. + * @param extractor A function used to extract axis-specific values (e.g., + * `p.y()` or `p.x()`). + * + * @return `true` if the computed range differs from the previous range, `false` + * otherwise. + * + * @note If the dataset has the same minimum and maximum values, the range is + * adjusted to provide a better display. + */ +template +bool Widgets::Plot::computeMinMaxValues(qreal &min, qreal &max, + const JSON::Dataset &dataset, + const bool addPadding, + Extractor extractor) { // Store previous values bool ok = true; - const auto prevMinY = m_minY; - const auto prevMaxY = m_maxY; + const auto prevMinY = min; + const auto prevMaxY = max; // If the data is empty, set the range to 0-1 if (m_data.isEmpty()) { - m_minY = 0; - m_maxY = 1; + min = 0; + max = 1; } // Obtain min/max values from datasets - else if (VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) + else { - const auto &dataset = GET_DATASET(SerialStudio::DashboardPlot, m_index); ok &= !qFuzzyCompare(dataset.min(), dataset.max()); if (ok) { - m_minY = qMin(dataset.min(), dataset.max()); - m_maxY = qMax(dataset.min(), dataset.max()); + min = qMin(dataset.min(), dataset.max()); + max = qMax(dataset.min(), dataset.max()); } } @@ -210,49 +334,52 @@ void Widgets::Plot::calculateAutoScaleRange() if (!ok) { // Initialize values to ensure that min/max are set - m_minY = std::numeric_limits::max(); - m_maxY = std::numeric_limits::lowest(); + min = std::numeric_limits::max(); + max = std::numeric_limits::lowest(); // Loop through the plot data and update the min and max - m_minY = SIMD::findMin(m_data, [](const QPointF &p) { return p.y(); }); - m_maxY = SIMD::findMax(m_data, [](const QPointF &p) { return p.y(); }); + min = SIMD::findMin(m_data, extractor); + max = SIMD::findMax(m_data, extractor); // If min and max are the same, adjust the range - if (qFuzzyCompare(m_minY, m_maxY)) + if (qFuzzyCompare(min, max)) { - if (qFuzzyIsNull(m_minY)) + if (qFuzzyIsNull(min)) { - m_minY = -1; - m_maxY = 1; + min = -1; + max = 1; } else { - double absValue = qAbs(m_minY); - m_minY = m_minY - absValue * 0.1; - m_maxY = m_maxY + absValue * 0.1; + double absValue = qAbs(min); + min = min - absValue * 0.1; + min = max + absValue * 0.1; } } // If the min and max are not the same, set the range to 10% more - else + else if (addPadding) { - double range = m_maxY - m_minY; - m_minY -= range * 0.1; - m_maxY += range * 0.1; + double range = max - min; + min -= range * 0.1; + max += range * 0.1; } // Round to integer numbers - m_maxY = std::ceil(m_maxY); - m_minY = std::floor(m_minY); - if (qFuzzyCompare(m_maxY, m_minY)) + max = std::ceil(max); + min = std::floor(min); + if (qFuzzyCompare(max, min) && addPadding) { - m_minY -= 1; - m_maxY += 1; + min -= 1; + max += 1; } } // Update user interface if required - if (qFuzzyCompare(prevMinY, m_minY) || qFuzzyCompare(prevMaxY, m_maxY)) - Q_EMIT rangeChanged(); + if (qFuzzyCompare(prevMinY, min) || qFuzzyCompare(prevMaxY, max)) + return true; + + // Data not changed + return false; } diff --git a/app/src/UI/Widgets/Plot.h b/app/src/UI/Widgets/Plot.h index 47807c55..2d3d9b4d 100644 --- a/app/src/UI/Widgets/Plot.h +++ b/app/src/UI/Widgets/Plot.h @@ -26,6 +26,8 @@ #include #include +#include "JSON/Dataset.h" + namespace Widgets { /** @@ -35,6 +37,7 @@ class Plot : public QQuickItem { Q_OBJECT Q_PROPERTY(QString yLabel READ yLabel CONSTANT) + Q_PROPERTY(QString xLabel READ xLabel CONSTANT) Q_PROPERTY(qreal minX READ minX NOTIFY rangeChanged) Q_PROPERTY(qreal maxX READ maxX NOTIFY rangeChanged) Q_PROPERTY(qreal minY READ minY NOTIFY rangeChanged) @@ -60,6 +63,7 @@ public: [[nodiscard]] qreal xTickInterval() const; [[nodiscard]] qreal yTickInterval() const; [[nodiscard]] const QString &yLabel() const; + [[nodiscard]] const QString &xLabel() const; public slots: void draw(QLineSeries *series); @@ -69,6 +73,11 @@ private slots: void updateRange(); void calculateAutoScaleRange(); +private: + template + bool computeMinMaxValues(qreal &min, qreal &max, const JSON::Dataset &dataset, + const bool addPadding, Extractor extractor); + private: int m_index; qreal m_minX; @@ -76,6 +85,7 @@ private: qreal m_minY; qreal m_maxY; QString m_yLabel; + QString m_xLabel; QVector m_data; }; } // namespace Widgets