Add initial support for XY plotting (#218 and #157)

This commit is contained in:
Alex Spataru 2024-11-29 10:20:18 -05:00
parent cabe4e8c77
commit e333783784
16 changed files with 790 additions and 255 deletions

View File

@ -66,9 +66,10 @@ Widgets.Pane {
Rectangle { Rectangle {
z: 2 z: 2
id: header id: header
height: actionsLayout.implicitHeight + 12
visible: Cpp_UI_Dashboard.actionCount > 0
color: Cpp_ThemeManager.colors["groupbox_background"] color: Cpp_ThemeManager.colors["groupbox_background"]
height: visible ? actionsLayout.implicitHeight + 12 : 0
visible: Cpp_UI_Dashboard.actionCount > 0 && !Cpp_CSV_Player.isOpen
anchors { anchors {
top: parent.top top: parent.top
left: parent.left left: parent.left
@ -122,8 +123,8 @@ Widgets.Pane {
contentWidth: width contentWidth: width
anchors.leftMargin: 8 anchors.leftMargin: 8
anchors.bottomMargin: 8 anchors.bottomMargin: 8
anchors.topMargin: header.height + 8
contentHeight: grid.height contentHeight: grid.height
anchors.topMargin: header.height + 8
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
id: scroll id: scroll

View File

@ -56,9 +56,9 @@ Item {
xMax: root.model.maxX xMax: root.model.maxX
yMin: root.model.minY yMin: root.model.minY
yMax: root.model.maxY yMax: root.model.maxY
xLabel: qsTr("Samples")
curveColors: [root.color] curveColors: [root.color]
yLabel: root.model.yLabel yLabel: root.model.yLabel
xLabel: root.model.xLabel
xAxis.tickInterval: root.model.xTickInterval xAxis.tickInterval: root.model.xTickInterval
yAxis.tickInterval: root.model.yTickInterval yAxis.tickInterval: root.model.yTickInterval
Component.onCompleted: graph.addSeries(lineSeries) Component.onCompleted: graph.addSeries(lineSeries)

View File

@ -22,6 +22,30 @@
#include "JSON/Action.h" #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. * @brief Constructs an Action object with a specified action ID.
* *
@ -124,11 +148,10 @@ bool JSON::Action::read(const QJsonObject &object)
{ {
if (!object.isEmpty()) if (!object.isEmpty())
{ {
m_icon = object.value(QStringLiteral("icon")).toString(); m_txData = SAFE_READ(object, "txData", "").toString();
m_txData = object.value(QStringLiteral("txData")).toString(); m_eolSequence = SAFE_READ(object, "eol", "").toString();
m_eolSequence = object.value(QStringLiteral("eol")).toString(); m_icon = SAFE_READ(object, "icon", "").toString().simplified();
m_title = object.value(QStringLiteral("title")).toString().simplified(); m_title = SAFE_READ(object, "title", "").toString().simplified();
return true; return true;
} }

View File

@ -22,6 +22,33 @@
#include "JSON/Dataset.h" #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) JSON::Dataset::Dataset(const int groupId, const int datasetId)
: m_fft(false) : m_fft(false)
, m_led(false) , m_led(false)
@ -36,6 +63,7 @@ JSON::Dataset::Dataset(const int groupId, const int datasetId)
, m_min(0) , m_min(0)
, m_alarm(0) , m_alarm(0)
, m_ledHigh(1) , m_ledHigh(1)
, m_xAxisId(-1)
, m_fftSamples(256) , m_fftSamples(256)
, m_fftSamplingRate(100) , m_fftSamplingRate(100)
, m_groupId(groupId) , m_groupId(groupId)
@ -147,6 +175,15 @@ const QString &JSON::Dataset::widget() const
return m_widget; 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 * 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("index"), m_index);
object.insert(QStringLiteral("alarm"), m_alarm); object.insert(QStringLiteral("alarm"), m_alarm);
object.insert(QStringLiteral("graph"), m_graph); object.insert(QStringLiteral("graph"), m_graph);
object.insert(QStringLiteral("xAxis"), m_xAxisId);
object.insert(QStringLiteral("ledHigh"), m_ledHigh); object.insert(QStringLiteral("ledHigh"), m_ledHigh);
object.insert(QStringLiteral("fftSamples"), m_fftSamples); object.insert(QStringLiteral("fftSamples"), m_fftSamples);
object.insert(QStringLiteral("value"), m_value.simplified()); object.insert(QStringLiteral("value"), m_value.simplified());
@ -229,21 +267,22 @@ bool JSON::Dataset::read(const QJsonObject &object)
{ {
if (!object.isEmpty()) if (!object.isEmpty())
{ {
m_fft = object.value(QStringLiteral("fft")).toBool(); m_min = SAFE_READ(object, "min", 0).toDouble();
m_led = object.value(QStringLiteral("led")).toBool(); m_max = SAFE_READ(object, "max", 0).toDouble();
m_log = object.value(QStringLiteral("log")).toBool(); m_index = SAFE_READ(object, "index", 0).toInt();
m_min = object.value(QStringLiteral("min")).toDouble(); m_fft = SAFE_READ(object, "fft", false).toBool();
m_max = object.value(QStringLiteral("max")).toDouble(); m_led = SAFE_READ(object, "led", false).toBool();
m_index = object.value(QStringLiteral("index")).toInt(); m_log = SAFE_READ(object, "log", false).toBool();
m_alarm = object.value(QStringLiteral("alarm")).toDouble(); m_xAxisId = SAFE_READ(object, "xAxis", 0).toInt();
m_graph = object.value(QStringLiteral("graph")).toBool(); m_alarm = SAFE_READ(object, "alarm", 0).toDouble();
m_ledHigh = object.value(QStringLiteral("ledHigh")).toDouble(); m_graph = SAFE_READ(object, "graph", false).toBool();
m_fftSamples = object.value(QStringLiteral("fftSamples")).toInt(); m_ledHigh = SAFE_READ(object, "ledHigh", 0).toDouble();
m_title = object.value(QStringLiteral("title")).toString().simplified(); m_fftSamples = SAFE_READ(object, "fftSamples", 256).toInt();
m_value = object.value(QStringLiteral("value")).toString().simplified(); m_title = SAFE_READ(object, "title", "").toString().simplified();
m_units = object.value(QStringLiteral("units")).toString().simplified(); m_value = SAFE_READ(object, "value", "").toString().simplified();
m_widget = object.value(QStringLiteral("widget")).toString().simplified(); m_units = SAFE_READ(object, "units", "").toString().simplified();
m_fftSamplingRate = object.value(QStringLiteral("fftSamplingRate")).toInt(); m_widget = SAFE_READ(object, "widget", "").toString().simplified();
m_fftSamplingRate = SAFE_READ(object, "fftSamplingRate", 100).toInt();
if (m_value.isEmpty()) if (m_value.isEmpty())
m_value = QStringLiteral("--.--"); m_value = QStringLiteral("--.--");

View File

@ -83,6 +83,8 @@ public:
[[nodiscard]] double max() const; [[nodiscard]] double max() const;
[[nodiscard]] double alarm() const; [[nodiscard]] double alarm() const;
[[nodiscard]] double ledHigh() const; [[nodiscard]] double ledHigh() const;
[[nodiscard]] int xAxisId() const;
[[nodiscard]] int fftSamples() const; [[nodiscard]] int fftSamples() const;
[[nodiscard]] int fftSamplingRate() const; [[nodiscard]] int fftSamplingRate() const;
@ -121,6 +123,7 @@ private:
int m_fftSamplingRate; int m_fftSamplingRate;
int m_groupId; int m_groupId;
int m_xAxisId;
int m_datasetId; int m_datasetId;
friend class JSON::ProjectModel; friend class JSON::ProjectModel;

View File

@ -22,6 +22,30 @@
#include "JSON/Frame.h" #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 * Destructor function, free memory used by the @c Group objects before
* destroying an instance of this class. * destroying an instance of this class.
@ -96,16 +120,15 @@ bool JSON::Frame::read(const QJsonObject &object)
// Get title & groups array // Get title & groups array
const auto groups = object.value(QStringLiteral("groups")).toArray(); const auto groups = object.value(QStringLiteral("groups")).toArray();
const auto actions = object.value(QStringLiteral("actions")).toArray(); const auto actions = object.value(QStringLiteral("actions")).toArray();
const auto title const auto title = SAFE_READ(object, "title", "").toString().simplified();
= object.value(QStringLiteral("title")).toString().simplified();
// We need to have a project title and at least one group // We need to have a project title and at least one group
if (!title.isEmpty() && !groups.isEmpty()) if (!title.isEmpty() && !groups.isEmpty())
{ {
// Update title // Update title
m_title = title; m_title = title;
m_frameEnd = object.value(QStringLiteral("frameEnd")).toString(); m_frameEnd = SAFE_READ(object, "frameEnd", "").toString();
m_frameStart = object.value(QStringLiteral("frameStart")).toString(); m_frameStart = SAFE_READ(object, "frameStart", "").toString();
// Generate groups & datasets from data frame // Generate groups & datasets from data frame
for (auto i = 0; i < groups.count(); ++i) for (auto i = 0; i < groups.count(); ++i)

View File

@ -23,6 +23,30 @@
#include <QJsonArray> #include <QJsonArray>
#include "JSON/Group.h" #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 * @brief Constructor function
*/ */
@ -78,8 +102,8 @@ bool JSON::Group::read(const QJsonObject &object)
{ {
// clang-format off // clang-format off
const auto array = object.value(QStringLiteral("datasets")).toArray(); const auto array = object.value(QStringLiteral("datasets")).toArray();
const auto title = object.value(QStringLiteral("title")).toString().simplified(); const auto title = SAFE_READ(object, "title", "").toString().simplified();
const auto widget = object.value(QStringLiteral("widget")).toString().simplified(); const auto widget = SAFE_READ(object, "widget", "").toString().simplified();
// clang-format on // clang-format on
if (!title.isEmpty() && !array.isEmpty()) if (!title.isEmpty() && !array.isEmpty())

View File

@ -86,6 +86,7 @@ typedef enum
kDatasetView_Alarm, /**< Represents the dataset alarm value item. */ kDatasetView_Alarm, /**< Represents the dataset alarm value item. */
kDatasetView_FFT_Samples, /**< Represents the FFT window size item. */ kDatasetView_FFT_Samples, /**< Represents the FFT window size item. */
kDatasetView_FFT_SamplingRate, /**< Represents the FFT sampling rate item. */ kDatasetView_FFT_SamplingRate, /**< Represents the FFT sampling rate item. */
kDatasetView_xAxis /**< Represents the plot X axis item. */
} DatasetItem; } DatasetItem;
// clang-format on // clang-format on
@ -347,6 +348,39 @@ QString JSON::ProjectModel::selectedIcon() const
return data.toString(); 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<int, QString> 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. * @brief Retrieves the project title.
* *
@ -2315,6 +2349,7 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset)
const bool showWidget = currentDatasetIsEditable(); const bool showWidget = currentDatasetIsEditable();
const bool showFFTOptions = dataset.fft(); const bool showFFTOptions = dataset.fft();
const bool showLedOptions = dataset.led(); const bool showLedOptions = dataset.led();
const bool showPlotOptions = dataset.graph();
const bool showMinMax = dataset.graph() || dataset.widget() == "gauge" const bool showMinMax = dataset.graph() || dataset.widget() == "gauge"
|| dataset.widget() == "bar" || dataset.widget() == "bar"
|| m_selectedGroup.widget() == "multiplot"; || m_selectedGroup.widget() == "multiplot";
@ -2388,6 +2423,90 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset)
m_datasetModel->appendRow(widget); 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 // Add minimum/maximum values
if (showMinMax) if (showMinMax)
{ {
@ -2431,46 +2550,6 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset)
m_datasetModel->appendRow(alarm); 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 // FFT-specific options
if (showFFTOptions) if (showFFTOptions)
{ {
@ -2504,17 +2583,6 @@ void JSON::ProjectModel::buildDatasetModel(const JSON::Dataset &dataset)
m_datasetModel->appendRow(fftSamplingRate); 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 // Add LED High value
if (showLedOptions) if (showLedOptions)
{ {
@ -2982,6 +3050,9 @@ void JSON::ProjectModel::onDatasetItemChanged(QStandardItem *item)
m_selectedDataset.m_log = plotOptions.at(value.toInt()).second; m_selectedDataset.m_log = plotOptions.at(value.toInt()).second;
buildDatasetModel(m_selectedDataset); buildDatasetModel(m_selectedDataset);
break; break;
case kDatasetView_xAxis:
m_selectedDataset.m_xAxisId = value.toInt();
break;
case kDatasetView_Min: case kDatasetView_Min:
m_selectedDataset.m_min = value.toFloat(); m_selectedDataset.m_min = value.toFloat();
break; break;

View File

@ -224,6 +224,8 @@ public:
[[nodiscard]] QString selectedText() const; [[nodiscard]] QString selectedText() const;
[[nodiscard]] QString selectedIcon() const; [[nodiscard]] QString selectedIcon() const;
[[nodiscard]] QStringList xDataSources() const;
[[nodiscard]] const QString actionIcon() const; [[nodiscard]] const QString actionIcon() const;
[[nodiscard]] const QStringList &availableActionIcons() const; [[nodiscard]] const QStringList &availableActionIcons() const;

View File

@ -29,16 +29,60 @@
#include "JSON/Dataset.h" #include "JSON/Dataset.h"
/** /**
* @typedef Curve * @typedef PlotDataX
* @brief Defines a plot series or curve as a vector of real (qreal) values. * @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<qreal> Curve; typedef QVector<qreal> PlotDataX;
/** /**
* @typedef MultipleCurves * @typedef PlotDataY
* @brief Defines a collection of curves, used for representing multiple plots. * @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<Curve> MultipleCurves; typedef QVector<qreal> 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<PlotDataY> 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<PlotDataY> y;
} MultiLineSeries;
/** /**
* @class SerialStudio * @class SerialStudio

View File

@ -448,6 +448,22 @@ QStringList UI::Dashboard::actionTitles() const
return titles; 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<int, JSON::Dataset> &UI::Dashboard::datasets() const
{
return m_datasets;
}
/** /**
* @brief Provides access to a specific group widget based on widget type and * @brief Provides access to a specific group widget based on widget type and
* relative index. * relative index.
@ -493,48 +509,52 @@ const JSON::Frame &UI::Dashboard::currentFrame()
/** /**
* @brief Provides the FFT plot values currently displayed on the dashboard. * @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<Curve> &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. * @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<Curve> &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. * @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<MultipleCurves> &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. * @brief Sets the number of data points for the dashboard plots.
* Clears existing multiplot and linear plot values and emits the
* @c pointsChanged signal.
* *
* @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) void UI::Dashboard::setPoints(const int points)
{ {
if (m_points != points) if (m_points != points)
{ {
// Update number of points
m_points = 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(); Q_EMIT pointsChanged();
} }
} }
@ -592,12 +612,18 @@ void UI::Dashboard::setShowLegends(const bool enabled)
void UI::Dashboard::resetData(const bool notify) void UI::Dashboard::resetData(const bool notify)
{ {
// Clear plotting data // Clear plotting data
m_fftPlotValues.clear(); m_fftValues.clear();
m_multiplotValues.clear(); m_pltValues.clear();
m_linearPlotValues.clear(); m_multipltValues.clear();
m_fftPlotValues.squeeze();
m_multiplotValues.squeeze(); // Free memory associated with the containers of the plotting data
m_linearPlotValues.squeeze(); m_fftValues.squeeze();
m_pltValues.squeeze();
m_multipltValues.squeeze();
// Clear X/Y axis arrays
m_xAxisData.clear();
m_yAxisData.clear();
// Clear widget & action structures // Clear widget & action structures
m_widgetCount = 0; 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 * @brief Updates the plot data for all dashboard widgets.
* dashboard.
* *
* This function checks and initializes the data structures for each plot type * This function ensures that the data structures for FFT plots, linear plots,
* (linear plots, FFT plots, and multiplots) if needed. * 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 * @note This function is typically called in real-time to keep plots
* shifting older data back and adding new data to the end. * synchronized with incoming data.
*/ */
void UI::Dashboard::updatePlots() 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<qreal>(m_linearPlotValues.last().data(), points() + 1, 0);
}
}
// Check if we need to re-initialize FFT plots data // Check if we need to re-initialize FFT plots data
if (m_fftPlotValues.count() != widgetCount(SerialStudio::DashboardFFT)) if (m_fftValues.count() != widgetCount(SerialStudio::DashboardFFT))
{ configureFftSeries();
m_fftPlotValues.clear();
m_fftPlotValues.squeeze(); // Check if we need to re-initialize linear plots data
for (int i = 0; i < widgetCount(SerialStudio::DashboardFFT); ++i) if (m_pltValues.count() != widgetCount(SerialStudio::DashboardPlot))
{ configureLineSeries();
const auto &dataset = getDatasetWidget(SerialStudio::DashboardFFT, i);
m_fftPlotValues.append(Curve());
m_fftPlotValues.last().resize(dataset.fftSamples());
SIMD::fill<qreal>(m_fftPlotValues.last().data(), dataset.fftSamples(), 0);
}
}
// Check if we need to re-initialize multiplot data // Check if we need to re-initialize multiplot data
if (m_multiplotValues.count() if (m_multipltValues.count() != widgetCount(SerialStudio::DashboardMultiPlot))
!= widgetCount(SerialStudio::DashboardMultiPlot)) configureMultiLineSeries();
{
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<qreal>(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<qreal>(data, count, dataset.value().toFloat());
}
// Append latest values to FFT plots data // Append latest values to FFT plots data
for (int i = 0; i < widgetCount(SerialStudio::DashboardFFT); ++i) for (int i = 0; i < widgetCount(SerialStudio::DashboardFFT); ++i)
{ {
const auto &dataset = getDatasetWidget(SerialStudio::DashboardFFT, i); const auto &dataset = getDatasetWidget(SerialStudio::DashboardFFT, i);
auto *data = m_fftPlotValues[i].data(); auto *data = m_fftValues[i].data();
auto count = m_fftPlotValues[i].count(); auto count = m_fftValues[i].count();
SIMD::shift<qreal>(data, count, dataset.value().toFloat()); SIMD::shift<qreal>(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<qreal>(xData, xCount, xDataset.value().toFloat());
SIMD::shift<qreal>(yData, yCount, yDataset.value().toFloat());
}
else
{
auto *data = m_yAxisData[yDataset.index()].data();
auto count = m_yAxisData[yDataset.index()].count();
SIMD::shift<qreal>(data, count, yDataset.value().toFloat());
}
}
// Append latest values to multiplots data // Append latest values to multiplots data
for (int i = 0; i < widgetCount(SerialStudio::DashboardMultiPlot); ++i) 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) for (int j = 0; j < group.datasetCount(); ++j)
{ {
const auto &dataset = group.datasets()[j]; const auto &dataset = group.datasets()[j];
auto *data = m_multiplotValues[i][j].data(); auto *data = m_multipltValues[i].y[j].data();
auto count = m_multiplotValues[i][j].count(); auto count = m_multipltValues[i].y[j].count();
SIMD::shift<qreal>(data, count, dataset.value().toFloat()); SIMD::shift<qreal>(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<qreal>(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<qreal>(m_xAxisData[xDataset.index()].data(), points() + 1, 0);
SIMD::fill<qreal>(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<qreal>(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<qreal>(series.y.last().data(), points() + 1, 0);
}
m_multipltValues.append(series);
}
}
/** /**
* @brief Processes and updates the dashboard data based on a new frame. * @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(); Q_EMIT actionCountChanged();
// Update widget data structures // Update widget data structures
m_datasets.clear();
JSON::Group ledPanel; JSON::Group ledPanel;
for (const auto &group : frame.groups()) for (const auto &group : frame.groups())
{ {
@ -816,6 +975,7 @@ void UI::Dashboard::processFrame(const JSON::Frame &frame)
for (const auto &dataset : group.datasets()) for (const auto &dataset : group.datasets())
{ {
m_datasets.insert(dataset.index(), dataset);
auto keys = SerialStudio::getDashboardWidgets(dataset); auto keys = SerialStudio::getDashboardWidgets(dataset);
for (const auto &key : keys) 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 // Update user interface
Q_EMIT widgetCountChanged(); Q_EMIT widgetCountChanged();
Q_EMIT widgetVisibilityChanged(); Q_EMIT widgetVisibilityChanged();

View File

@ -134,14 +134,16 @@ public:
[[nodiscard]] QStringList actionTitles() const; [[nodiscard]] QStringList actionTitles() const;
// clang-format off // clang-format off
[[nodiscard]] const QMap<int, JSON::Dataset> &datasets() const;
[[nodiscard]] const JSON::Group &getGroupWidget(const SerialStudio::DashboardWidget widget, const int index) 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; [[nodiscard]] const JSON::Dataset &getDatasetWidget(const SerialStudio::DashboardWidget widget, const int index) const;
// clang-format on // clang-format on
[[nodiscard]] const JSON::Frame &currentFrame(); [[nodiscard]] const JSON::Frame &currentFrame();
[[nodiscard]] const QVector<Curve> &fftPlotValues();
[[nodiscard]] const QVector<Curve> &linearPlotValues(); [[nodiscard]] const PlotDataY &fftData(const int index) const;
[[nodiscard]] const QVector<MultipleCurves> &multiplotValues(); [[nodiscard]] const LineSeries &plotData(const int index) const;
[[nodiscard]] const MultiLineSeries &multiplotData(const int index) const;
public slots: public slots:
void setPoints(const int points); void setPoints(const int points);
@ -155,6 +157,9 @@ public slots:
private slots: private slots:
void updatePlots(); void updatePlots();
void configureFftSeries();
void configureLineSeries();
void configureMultiLineSeries();
void processFrame(const JSON::Frame &frame); void processFrame(const JSON::Frame &frame);
private: private:
@ -165,11 +170,16 @@ private:
bool m_updateRequired; bool m_updateRequired;
SerialStudio::AxisVisibility m_axisVisibility; SerialStudio::AxisVisibility m_axisVisibility;
QVector<Curve> m_fftPlotValues; PlotDataX m_defaultXAxis;
QVector<Curve> m_linearPlotValues; QMap<int, PlotDataX> m_xAxisData;
QVector<MultipleCurves> m_multiplotValues; QMap<int, PlotDataY> m_yAxisData;
QVector<PlotDataY> m_fftValues;
QVector<LineSeries> m_pltValues;
QVector<MultiLineSeries> m_multipltValues;
QVector<JSON::Action> m_actions; QVector<JSON::Action> m_actions;
QMap<int, JSON::Dataset> m_datasets;
QList<SerialStudio::DashboardWidget> m_availableWidgets; QList<SerialStudio::DashboardWidget> m_availableWidgets;
QMap<int, QPair<SerialStudio::DashboardWidget, int>> m_widgetMap; QMap<int, QPair<SerialStudio::DashboardWidget, int>> m_widgetMap;
QMap<SerialStudio::DashboardWidget, QVector<bool>> m_widgetVisibility; QMap<SerialStudio::DashboardWidget, QVector<bool>> m_widgetVisibility;

View File

@ -146,15 +146,12 @@ void Widgets::FFTPlot::updateData()
if (!isEnabled()) if (!isEnabled())
return; return;
// Get the plot data if (VALIDATE_WIDGET(SerialStudio::DashboardFFT, m_index))
auto dash = &UI::Dashboard::instance();
auto plotData = dash->fftPlotValues();
// If the plot data is valid, update the data
if (plotData.count() > m_index)
{ {
// Get the plot data
const auto &data = UI::Dashboard::instance().fftData(m_index);
// Obtain samples from data // Obtain samples from data
const auto &data = plotData.at(m_index);
for (int i = 0; i < m_size; ++i) for (int i = 0; i < m_size; ++i)
m_samples[i] = static_cast<float>(data[i]); m_samples[i] = static_cast<float>(data[i]);

View File

@ -194,19 +194,15 @@ void Widgets::MultiPlot::updateData()
if (VALIDATE_WIDGET(SerialStudio::DashboardMultiPlot, m_index)) if (VALIDATE_WIDGET(SerialStudio::DashboardMultiPlot, m_index))
{ {
const auto &plotData = UI::Dashboard::instance().multiplotValues(); const auto &data = UI::Dashboard::instance().multiplotData(m_index);
if (m_index >= 0 && plotData.count() > m_index) for (int i = 0; i < data.y.count(); ++i)
{ {
const auto &curves = plotData[m_index]; const auto &series = data.y[i];
for (int i = 0; i < curves.count(); ++i) if (m_data[i].count() != series.count())
{ m_data[i].resize(series.count());
const auto &values = curves[i];
if (m_data[i].count() != values.count())
m_data[i].resize(values.count());
for (int j = 0; j < values.count(); ++j) for (int j = 0; j < series.count(); ++j)
m_data[i][j] = QPointF(j, values[j]); m_data[i][j] = QPointF(data.x->at(j), series[j]);
}
} }
} }
} }

View File

@ -39,14 +39,26 @@ Widgets::Plot::Plot(const int index, QQuickItem *parent)
{ {
if (VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) 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(yDataset.min(), yDataset.max());
m_minY = qMin(dataset.min(), dataset.max()); m_maxY = qMax(yDataset.min(), yDataset.max());
m_maxY = qMax(dataset.min(), dataset.max());
if (!dataset.units().isEmpty()) const auto xAxisId = yDataset.xAxisId();
m_yLabel += " (" + dataset.units() + ")"; 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, connect(&UI::Dashboard::instance(), &UI::Dashboard::updated, this,
&Plot::updateData); &Plot::updateData);
@ -121,6 +133,15 @@ const QString &Widgets::Plot::yLabel() const
return m_yLabel; 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. * @brief Draws the data on the given QLineSeries.
* @param series The QLineSeries to draw the data on. * @param series The QLineSeries to draw the data on.
@ -145,16 +166,27 @@ void Widgets::Plot::updateData()
if (VALIDATE_WIDGET(SerialStudio::DashboardPlot, m_index)) 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 (Y->count() > i)
if (m_data.count() != values.count()) {
m_data.resize(UI::Dashboard::instance().points()); m_data[i] = QPointF(*x, Y->at(i));
++i;
}
for (int i = 0; i < values.count(); ++i) else
m_data[i] = QPointF(i, values[i]); break;
} }
} }
} }
@ -164,45 +196,137 @@ void Widgets::Plot::updateData()
*/ */
void Widgets::Plot::updateRange() void Widgets::Plot::updateRange()
{ {
// Reserve the number of points in the dashboard // Clear memory
m_data.clear(); m_data.clear();
m_data.squeeze(); m_data.squeeze();
m_data.resize(UI::Dashboard::instance().points() + 1); m_data.resize(UI::Dashboard::instance().points() + 1);
// Update x-axis // 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_minX = 0;
m_maxX = UI::Dashboard::instance().points(); m_maxX = UI::Dashboard::instance().points();
}
}
// Update the plot // Update the plot
Q_EMIT rangeChanged(); 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() 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<typename Extractor>
bool Widgets::Plot::computeMinMaxValues(qreal &min, qreal &max,
const JSON::Dataset &dataset,
const bool addPadding,
Extractor extractor)
{ {
// Store previous values // Store previous values
bool ok = true; bool ok = true;
const auto prevMinY = m_minY; const auto prevMinY = min;
const auto prevMaxY = m_maxY; const auto prevMaxY = max;
// If the data is empty, set the range to 0-1 // If the data is empty, set the range to 0-1
if (m_data.isEmpty()) if (m_data.isEmpty())
{ {
m_minY = 0; min = 0;
m_maxY = 1; max = 1;
} }
// Obtain min/max values from datasets // 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()); ok &= !qFuzzyCompare(dataset.min(), dataset.max());
if (ok) if (ok)
{ {
m_minY = qMin(dataset.min(), dataset.max()); min = qMin(dataset.min(), dataset.max());
m_maxY = qMax(dataset.min(), dataset.max()); max = qMax(dataset.min(), dataset.max());
} }
} }
@ -210,49 +334,52 @@ void Widgets::Plot::calculateAutoScaleRange()
if (!ok) if (!ok)
{ {
// Initialize values to ensure that min/max are set // Initialize values to ensure that min/max are set
m_minY = std::numeric_limits<qreal>::max(); min = std::numeric_limits<qreal>::max();
m_maxY = std::numeric_limits<qreal>::lowest(); max = std::numeric_limits<qreal>::lowest();
// Loop through the plot data and update the min and max // Loop through the plot data and update the min and max
m_minY = SIMD::findMin(m_data, [](const QPointF &p) { return p.y(); }); min = SIMD::findMin(m_data, extractor);
m_maxY = SIMD::findMax(m_data, [](const QPointF &p) { return p.y(); }); max = SIMD::findMax(m_data, extractor);
// If min and max are the same, adjust the range // 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; min = -1;
m_maxY = 1; max = 1;
} }
else else
{ {
double absValue = qAbs(m_minY); double absValue = qAbs(min);
m_minY = m_minY - absValue * 0.1; min = min - absValue * 0.1;
m_maxY = m_maxY + absValue * 0.1; min = max + absValue * 0.1;
} }
} }
// If the min and max are not the same, set the range to 10% more // 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; double range = max - min;
m_minY -= range * 0.1; min -= range * 0.1;
m_maxY += range * 0.1; max += range * 0.1;
} }
// Round to integer numbers // Round to integer numbers
m_maxY = std::ceil(m_maxY); max = std::ceil(max);
m_minY = std::floor(m_minY); min = std::floor(min);
if (qFuzzyCompare(m_maxY, m_minY)) if (qFuzzyCompare(max, min) && addPadding)
{ {
m_minY -= 1; min -= 1;
m_maxY += 1; max += 1;
} }
} }
// Update user interface if required // Update user interface if required
if (qFuzzyCompare(prevMinY, m_minY) || qFuzzyCompare(prevMaxY, m_maxY)) if (qFuzzyCompare(prevMinY, min) || qFuzzyCompare(prevMaxY, max))
Q_EMIT rangeChanged(); return true;
// Data not changed
return false;
} }

View File

@ -26,6 +26,8 @@
#include <QVector> #include <QVector>
#include <QLineSeries> #include <QLineSeries>
#include "JSON/Dataset.h"
namespace Widgets namespace Widgets
{ {
/** /**
@ -35,6 +37,7 @@ class Plot : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString yLabel READ yLabel CONSTANT) 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 minX READ minX NOTIFY rangeChanged)
Q_PROPERTY(qreal maxX READ maxX NOTIFY rangeChanged) Q_PROPERTY(qreal maxX READ maxX NOTIFY rangeChanged)
Q_PROPERTY(qreal minY READ minY NOTIFY rangeChanged) Q_PROPERTY(qreal minY READ minY NOTIFY rangeChanged)
@ -60,6 +63,7 @@ public:
[[nodiscard]] qreal xTickInterval() const; [[nodiscard]] qreal xTickInterval() const;
[[nodiscard]] qreal yTickInterval() const; [[nodiscard]] qreal yTickInterval() const;
[[nodiscard]] const QString &yLabel() const; [[nodiscard]] const QString &yLabel() const;
[[nodiscard]] const QString &xLabel() const;
public slots: public slots:
void draw(QLineSeries *series); void draw(QLineSeries *series);
@ -69,6 +73,11 @@ private slots:
void updateRange(); void updateRange();
void calculateAutoScaleRange(); void calculateAutoScaleRange();
private:
template<typename Extractor>
bool computeMinMaxValues(qreal &min, qreal &max, const JSON::Dataset &dataset,
const bool addPadding, Extractor extractor);
private: private:
int m_index; int m_index;
qreal m_minX; qreal m_minX;
@ -76,6 +85,7 @@ private:
qreal m_minY; qreal m_minY;
qreal m_maxY; qreal m_maxY;
QString m_yLabel; QString m_yLabel;
QString m_xLabel;
QVector<QPointF> m_data; QVector<QPointF> m_data;
}; };
} // namespace Widgets } // namespace Widgets