DynExp
Highly flexible laboratory automation for dynamically changing experiments.
Loading...
Searching...
No Matches
SignalPlotter.cpp
Go to the documentation of this file.
1// This file is part of DynExp.
2
3#include "stdafx.h"
4#include "moc_SignalPlotter.cpp"
5#include "ui_SignalPlotter.h"
6#include "SignalPlotter.h"
7
8namespace DynExpModule
9{
11 : QModuleWidget(Owner, parent),
12 ui(std::make_unique<Ui::SignalPlotter>()),
13 PlotContextMenu(new QMenu(this)),
14 DataSeries(nullptr), DataChart(nullptr), XAxis(nullptr), YAxis(nullptr)
15 {
16 ui->setupUi(this);
17
18 PlotAutoscaleAction = PlotContextMenu->addAction("&Autoscale axes");
19 PlotAutoscaleAction->setCheckable(true);
20 PlotAutoscaleAction->setChecked(true);
21 PlotRollingViewAction = PlotContextMenu->addAction("&Rolling view");
22 PlotRollingViewAction->setCheckable(true);
23 PlotContextMenu->addSeparator();
24 PlotClearAction = PlotContextMenu->addAction("&Clear stream");
25
26 DataChart = new QChart();
27 ui->Signal->setChart(DataChart); // Takes ownership of DataChart.
28 ui->Signal->setRenderHint(QPainter::Antialiasing);
30 DataChart->legend()->setVisible(false);
31 ui->action_Run->setChecked(true);
32 }
33
34 void SignalPlotterWidget::UpdateUI(bool IsRunning)
35 {
36 if (DataChart->axes().size() >= 2)
37 DataChart->axes()[1]->setLabelsEditable(!IsRunning);
38 }
39
40 void SignalPlotterWidget::SetAxes(QValueAxis* XAxis, QValueAxis* YAxis)
41 {
42 if (!XAxis || !YAxis)
43 return;
44
45 if (this->XAxis)
46 {
47 DataChart->removeAxis(this->XAxis);
48 delete this->XAxis;
49 }
50 if (this->YAxis)
51 {
52 DataChart->removeAxis(this->YAxis);
53 delete this->YAxis;
54 }
55
56 this->XAxis = XAxis;
57 this->YAxis = YAxis;
58
59 // Chart takes ownership of axes.
60 DataChart->addAxis(XAxis, Qt::AlignBottom);
61 DataChart->addAxis(YAxis, Qt::AlignLeft);
62 }
63
65 {
66 if (IsSavingData)
67 return;
68
69 // DataSeries->replace() does not work...
70 DataChart->removeAllSeries();
71
72 if (SampleData.Points.empty())
73 return;
74
75 if (SampleData.Points.size() > 1)
76 {
77 DataSeries = new QLineSeries(this);
78 DataSeries->append(SampleData.Points);
79 DataSeries->setPointsVisible(false);
80
81 DataChart->axes()[0]->setRange(SampleData.MinValues.x(), SampleData.MaxValues.x());
82 }
83 else
84 {
85 auto ScatterSeries = new QScatterSeries(this);
86 ScatterSeries->append(SampleData.Points);
87 ScatterSeries->setMarkerShape(QScatterSeries::MarkerShape::MarkerShapeCircle);
88 ScatterSeries->setMarkerSize(15);
89 DataSeries = ScatterSeries;
90
91 DataChart->axes()[0]->setRange(SampleData.MinValues.x() - 2, SampleData.MaxValues.x() + 2);
92 }
93
94 if (PlotAutoscaleAction->isChecked())
95 {
96 if (SampleData.MinValues.y() == SampleData.MaxValues.y())
97 {
98 auto Factor = std::abs(SampleData.MinValues.y()) * .01;
99 if (Factor == 0)
100 Factor = 2;
101
102 DataChart->axes()[1]->setRange(SampleData.MinValues.y() - Factor, SampleData.MaxValues.y() + Factor);
103 }
104 else
105 DataChart->axes()[1]->setRange(SampleData.MinValues.y(), SampleData.MaxValues.y());
106 }
107
108 DataChart->addSeries(DataSeries);
109 DataSeries->attachAxis(DataChart->axes()[0]);
110 DataSeries->attachAxis(DataChart->axes()[1]);
111
112 Multiplier = SampleData.Multiplier;
113 }
114
116 {
117 PlotContextMenu->exec(ui->Signal->mapToGlobal(Position));
118 }
119
121 {
122 // As soon as FinishedSavingDataGuard is destroyed, IsSavingData is set back to false.
124 IsSavingData = true;
125
126 auto Filename = Util::PromptSaveFilePathModule(this, "Save data", ".csv", " Comma-separated values file (*.csv)");
127 if (Filename.isEmpty())
128 return;
129
130 std::stringstream CSVData;
131 CSVData << "X;Y\n";
132 for (int i = 0; i < Util::NumToT<int>(DataSeries->count()); ++i)
133 CSVData << DataSeries->at(i).x() / std::pow(10.0, Multiplier) << ";" << DataSeries->at(i).y() << "\n";
134
135 if (!Util::SaveToFile(Filename, CSVData.str()))
136 QMessageBox::warning(this, "DynExp - Error", "Error writing data to file.");
137 }
138
140 {
141 Points.clear();
142
143 MinValues = {};
144 MaxValues = {};
145 Multiplier = 0;
146 }
147
149 {
150 switch (Multiplier)
151 {
152 case 0: return "";
153 case 3: return "m";
154 case 6: return "u";
155 case 9: return "n";
156 default: return "?";
157 }
158 }
159
161 {
162 // Update CurrentSourceIndex first since GetDataStreamInstr() depends on it.
163 CurrentSourceIndex = Index;
164
165 ValueUnit = GetDataStreamInstr()->GetValueUnit();
166 UIInitialized = false;
167 }
168
173
175 {
176 UIInitialized = false;
177 CurrentSourceIndex = 0; // non-optional DataStreamInstr parameter, so it is ensured that there is at least one source (see RunnableInstance::LockObject()).
178 IsRunning = true;
179 IsBasicSampleTimeUsed = false;
181 PlotAxesChanged = true;
182 Autoscale = true;
183 RollingView = false;
184
185 SampleData.Reset();
186 }
187
189 {
190 std::vector<DynExpInstr::BasicSample> BasicSamples;
191 bool IsBasicSampleTimeUsed{};
192
193 try
194 {
195 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance.ModuleDataGetter());
196
197 if (ModuleData->IsRunning)
198 {
199 ModuleData->GetDataStreamInstr()->ReadData();
200
201 auto InstrData = DynExp::dynamic_InstrumentData_cast<DynExpInstr::DataStreamInstrument>(ModuleData->GetDataStreamInstr()->GetInstrumentData());
202 auto SampleStream = InstrData->GetSampleStream();
203
204 if (!ModuleData->RollingView || !SampleStream->SeekEqual(std::ios_base::in))
205 SampleStream->SeekBeg(std::ios_base::in);
206 BasicSamples = SampleStream->ReadBasicSamples(SampleStream->GetStreamSizeRead());
207 IsBasicSampleTimeUsed = SampleStream->IsBasicSampleTimeUsed();
208 } // InstrData data unlocked here.
209
210 NumFailedUpdateAttempts = 0;
211 } // ModuleData unlocked here.
212 catch (const Util::TimeoutException& e)
213 {
214 if (NumFailedUpdateAttempts++ >= 3)
215 Instance.GetOwner().SetWarning(e);
216 }
217
218 if (ProcessBasicSamples(std::move(BasicSamples), IsBasicSampleTimeUsed))
219 IsBasicSampleTimeUsed = false;
220
221 {
222 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance.ModuleDataGetter());
223
224 if (ModuleData->IsBasicSampleTimeUsed != IsBasicSampleTimeUsed && ModuleData->IsRunning)
225 {
226 ModuleData->IsBasicSampleTimeUsed = IsBasicSampleTimeUsed;
227 ModuleData->PlotAxesChanged = true;
228 }
229 } // ModuleData unlocked here.
230
232 }
233
235 {
236 NumFailedUpdateAttempts = 0;
237 }
238
239 std::unique_ptr<DynExp::QModuleWidget> SignalPlotter::MakeUIWidget()
240 {
241 auto Widget = std::make_unique<SignalPlotterWidget>(*this);
242
243 Connect(Widget->ui->action_Run, &QAction::triggered, this, &SignalPlotter::OnRunClicked);
244 Connect(Widget->ui->CBSource, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SignalPlotter::OnSourceChanged);
245 Connect(Widget->GetAutoscalePlotAction(), &QAction::triggered, this, &SignalPlotter::OnPlotAutoscaleClicked);
246 Connect(Widget->GetRollingViewPlotAction(), &QAction::triggered, this, &SignalPlotter::OnPlotRollingViewClicked);
247 Connect(Widget->GetClearPlotAction(), &QAction::triggered, this, &SignalPlotter::OnClearStream);
248
249 return Widget;
250 }
251
252 void SignalPlotter::UpdateUIChild(const ModuleBase::ModuleDataGetterType& ModuleDataGetter)
253 {
254 auto Widget = GetWidget<SignalPlotterWidget>();
255 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(ModuleDataGetter());
256
257 if (!Widget->ui->CBSource->count())
258 {
259 const QSignalBlocker CBSourceBlocker(Widget->ui->CBSource);
260
261 for (const auto& InstrLabel : ModuleData->GetDataStreamInstrLabels())
262 Widget->ui->CBSource->addItem(QIcon(ModuleData->GetDataStreamInstrIconPath().data()), QString::fromStdString(InstrLabel));
263
264 Widget->ui->CBSource->setCurrentIndex(0);
265 Widget->ui->CBSource->setVisible(Widget->ui->CBSource->count() > 1);
266 }
267
268 if (!ModuleData->IsUIInitialized())
269 {
270 Widget->GetAutoscalePlotAction()->setChecked(ModuleData->Autoscale);
271 Widget->GetRollingViewPlotAction()->setChecked(ModuleData->RollingView);
272
273 auto* XAxis = new QValueAxis(Widget);
274 auto* YAxis = new QValueAxis(Widget);
275 YAxis->setTitleText(QString("Signal in ") + DynExpInstr::DataStreamInstrumentData::UnitTypeToStr(ModuleData->ValueUnit));
277 {
278 YAxis->setLabelFormat("%d");
279 YAxis->setRange(0, 1);
280 YAxis->setTickCount(2);
281 YAxis->setMinorTickCount(0);
282 YAxis->setMinorGridLineVisible(false);
283 }
284
285 Widget->SetAxes(XAxis, YAxis);
286
287 ModuleData->PlotAxesChanged = true;
288 ModuleData->SetUIInitialized();
289 }
290
291 if (ModuleData->PlotAxesChanged ||
292 (ModuleData->IsRunning && ModuleData->SampleData.Multiplier != Widget->GetMultiplier() && !ModuleData->SampleData.Points.empty()))
293 {
294 if (Widget->GetXAxis())
295 {
296 Widget->GetXAxis()->setTitleText(ModuleData->IsBasicSampleTimeUsed ?
297 "time in " + ModuleData->SampleData.GetMultiplierLabel() + "s" : "sample in #");
298 Widget->GetXAxis()->setLabelFormat(ModuleData->IsBasicSampleTimeUsed ? "%.3f" : "%d");
299 }
300
301 ModuleData->PlotAxesChanged = false;
302 }
303
304 if (ModuleData->IsRunning)
305 {
306 Widget->SetData(ModuleData->SampleData);
307 Widget->ui->LNumSamples->setText(QString::number(ModuleData->SampleData.Points.size()) + " sample"
308 + (ModuleData->SampleData.Points.size() == 1 ? "" : "s"));
309 }
310
311 Widget->UpdateUI(ModuleData->IsRunning);
312 }
313
314 bool SignalPlotter::ProcessBasicSamples(std::vector<DynExpInstr::BasicSample>&& BasicSamples, bool IsBasicSampleTimeUsed)
315 {
316 bool FallenBackToNotUseSampleTime = false;
317 unsigned int Multiplier = 0;
318
319 if (BasicSamples.empty())
320 {
321 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(GetModuleData());
322 ModuleData->SampleData.Reset();
323
324 return FallenBackToNotUseSampleTime;
325 }
326
327 // Sorting is believed to be very fast if samples are already sorted by ascending x values.
328 if (IsBasicSampleTimeUsed)
329 {
330 // Use stable_sort() to not affect the order of samples with equal time, in case the data
331 // stream supports sample timing, but the user of the stream ignores sample timing.
332 std::stable_sort(BasicSamples.begin(), BasicSamples.end(), [](const auto& a, const auto& b) {
333 return a.Time < b.Time;
334 });
335
336 // Switch back to use sample indices as x values if all Time values are equal.
337 if (BasicSamples.front().Time == BasicSamples.back().Time && BasicSamples.size() > 1)
338 {
339 IsBasicSampleTimeUsed = false;
340 FallenBackToNotUseSampleTime = true;
341 }
342
343 // Determine best order of magnitude to display the time with.
344 if (std::abs(BasicSamples.front().Time) < 1.0 && std::abs(BasicSamples.back().Time) < 1.0)
345 {
346 Multiplier = 3;
347 if (std::abs(BasicSamples.front().Time) < 1e-3 && std::abs(BasicSamples.back().Time) < 1e-3)
348 {
349 Multiplier = 6;
350 if (std::abs(BasicSamples.front().Time) < 1e-6 && std::abs(BasicSamples.back().Time) < 1e-6)
351 Multiplier = 9;
352 }
353 }
354 }
355
357 double YMin(std::numeric_limits<double>::max()), YMax(std::numeric_limits<double>::lowest());
358
359 for (size_t i = 0; i < BasicSamples.size(); ++i)
360 {
361 auto Y = BasicSamples[i].Value;
362 SampleData.Points.append({ IsBasicSampleTimeUsed ? BasicSamples[i].Time * std::pow(10.0, Multiplier) : i, Y });
363
364 YMin = std::min(YMin, Y);
365 YMax = std::max(YMax, Y);
366 }
367
368 SampleData.MinValues = { SampleData.Points.first().x(), YMin };
369 SampleData.MaxValues = { SampleData.Points.last().x(), YMax };
370 SampleData.Multiplier = Multiplier;
371
372 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(GetModuleData());
373 ModuleData->SampleData = std::move(SampleData);
374
375 return FallenBackToNotUseSampleTime;
376 }
377
379 {
380 auto ModuleParams = DynExp::dynamic_Params_cast<SignalPlotter>(Instance->ParamsGetter());
381 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
382
383 ModuleData->LockInstruments(Instance, ModuleParams->DataStreamInstr);
384
385 ModuleData->ValueUnit = ModuleData->GetDataStreamInstr()->GetValueUnit();
386 ModuleData->Autoscale = ModuleParams->Autoscale.Get();
387 ModuleData->RollingView = ModuleParams->RollingView.Get();
388 }
389
391 {
392 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
393
394 ModuleData->UnlockInstruments(Instance);
395 }
396
397 void SignalPlotter::OnRunClicked(DynExp::ModuleInstance* Instance, bool Checked) const
398 {
399 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
400
401 if (Checked)
402 ModuleData->GetDataStreamInstr()->ResetStreamSize();
403
404 ModuleData->IsRunning = Checked;
405 }
406
408 {
409 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
410
411 ModuleData->SetCurrentSourceIndex(Index);
412 }
413
415 {
416 auto ModuleParams = DynExp::dynamic_Params_cast<SignalPlotter>(GetNonConstParams());
417 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
418
419 ModuleParams->Autoscale = Checked;
420 ModuleData->Autoscale = Checked;
421 }
422
424 {
425 auto ModuleParams = DynExp::dynamic_Params_cast<SignalPlotter>(GetNonConstParams());
426 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
427
428 ModuleParams->RollingView = Checked;
429 ModuleData->RollingView = Checked;
430 }
431
433 {
434 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
435 auto InstrData = DynExp::dynamic_InstrumentData_cast<DynExpInstr::DataStreamInstrument>(ModuleData->GetDataStreamInstr()->GetInstrumentData());
436
437 InstrData->GetSampleStream()->Clear();
438 }
439}
Implementation of a module to plot the samples stored in data stream instruments.
@ LogicLevel
Logic level (TTL) units (1 or 0)
static const char * UnitTypeToStr(const UnitType &Unit)
Returns a descriptive string of a respective unit to be e.g. used in plots.
void ResetImpl(dispatch_tag< QModuleDataBase >) override final
void OnPlotContextMenuRequested(const QPoint &Position)
SignalPlotterWidget(SignalPlotter &Owner, QModuleWidget *parent=nullptr)
decltype(SampleDataType::Multiplier) Multiplier
Describes the factor 10^Multiplier the displayed data has been multiplied with for better x-label rea...
std::unique_ptr< Ui::SignalPlotter > ui
void SetAxes(QValueAxis *XAxis, QValueAxis *YAxis)
void SetData(const SampleDataType &SampleData)
Util::DynExpErrorCodes::DynExpErrorCodes ModuleMainLoop(DynExp::ModuleInstance &Instance) override final
Module main loop. The function is executed periodically by the module thread. Also refer to GetMainLo...
void OnExit(DynExp::ModuleInstance *Instance) const override final
This event is triggered right before the module thread terminates (not due to an exception,...
void OnPlotRollingViewClicked(DynExp::ModuleInstance *Instance, bool Checked) const
void OnRunClicked(DynExp::ModuleInstance *Instance, bool Checked) const
void ResetImpl(dispatch_tag< QModuleBase >) override final
void OnSourceChanged(DynExp::ModuleInstance *Instance, int Index) const
bool ProcessBasicSamples(std::vector< DynExpInstr::BasicSample > &&BasicSamples, bool IsBasicSampleTimeUsed)
Converts BasicSamples to displayable format.
std::unique_ptr< DynExp::QModuleWidget > MakeUIWidget() override final
Used by InitUI() as a factory function for the module's user interface widget. Create the widget here...
void OnInit(DynExp::ModuleInstance *Instance) const override final
This event is triggered right before the module thread starts. Override it to lock instruments this m...
void OnPlotAutoscaleClicked(DynExp::ModuleInstance *Instance, bool Checked) const
void OnClearStream(DynExp::ModuleInstance *Instance, bool) const
void UpdateUIChild(const ModuleBase::ModuleDataGetterType &ModuleDataGetter) override final
Refer to ParamsBase::dispatch_tag.
Definition Module.h:191
Defines data for a thread belonging to a ModuleBase instance. Refer to RunnableInstance.
Definition Module.h:840
const ModuleBase::ModuleDataGetterType ModuleDataGetter
Getter for module's data. Refer to ModuleBase::ModuleDataGetterType.
Definition Module.h:872
Refer to ParamsBase::dispatch_tag.
Definition Object.h:2018
const Object::ParamsGetterType ParamsGetter
Invoke to obtain the parameters (derived from ParamsBase) of Owner.
Definition Object.h:3710
const auto & GetOwner() const noexcept
Returns Owner.
Definition Object.h:3556
Holds a CallableMemberWrapper and invokes its callable when being destroyed.
Definition Util.h:494
Thrown when an operation timed out before it could be completed, especially used for locking shared d...
Definition Exception.h:262
DynExp's module namespace contains the implementation of DynExp modules which extend DynExp's core fu...
constexpr auto DefaultQChartTheme
DynExpErrorCodes
DynExp's error codes
Definition Exception.h:22
bool SaveToFile(const QString &Filename, std::string_view Text)
Saves a std::string_view to a file (using QFile). Creates a new file or truncates an existing file's ...
Definition QtUtil.cpp:236
QString PromptSaveFilePathModule(DynExp::QModuleWidget *Parent, const QString &Title, const QString &DefaultSuffix, const QString &NameFilter)
Works as PromptOpenFilePath() but asks the user to select a single file which does not need to exist....
Definition QtUtil.cpp:143
Accumulates include statements to provide a precompiled header.