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 "SignalPlotter.h"
6
7namespace DynExpModule
8{
10 : QModuleWidget(Owner, parent),
11 PlotContextMenu(new QMenu(this)),
12 DataSeries(nullptr), DataChart(nullptr), XAxis(nullptr), YAxis(nullptr)
13 {
14 ui.setupUi(this);
15
16 PlotAutoscaleAction = PlotContextMenu->addAction("&Autoscale axes");
17 PlotAutoscaleAction->setCheckable(true);
18 PlotAutoscaleAction->setChecked(true);
19 PlotRollingViewAction = PlotContextMenu->addAction("&Rolling view");
20 PlotRollingViewAction->setCheckable(true);
21 PlotContextMenu->addSeparator();
22 PlotClearAction = PlotContextMenu->addAction("&Clear stream");
23
24 DataChart = new QChart();
25 ui.Signal->setChart(DataChart); // Takes ownership of DataChart.
26 ui.Signal->setRenderHint(QPainter::Antialiasing);
28 DataChart->legend()->setVisible(false);
29 ui.action_Run->setChecked(true);
30 }
31
32 void SignalPlotterWidget::UpdateUI(bool IsRunning)
33 {
34 if (DataChart->axes().size() >= 2)
35 DataChart->axes()[1]->setLabelsEditable(!IsRunning);
36 }
37
38 void SignalPlotterWidget::SetAxes(QValueAxis* XAxis, QValueAxis* YAxis)
39 {
40 if (!XAxis || !YAxis)
41 return;
42
43 if (this->XAxis)
44 {
45 DataChart->removeAxis(this->XAxis);
46 delete this->XAxis;
47 }
48 if (this->YAxis)
49 {
50 DataChart->removeAxis(this->YAxis);
51 delete this->YAxis;
52 }
53
54 this->XAxis = XAxis;
55 this->YAxis = YAxis;
56
57 // Chart takes ownership of axes.
58 DataChart->addAxis(XAxis, Qt::AlignBottom);
59 DataChart->addAxis(YAxis, Qt::AlignLeft);
60 }
61
63 {
64 if (IsSavingData)
65 return;
66
67 // DataSeries->replace() does not work...
68 DataChart->removeAllSeries();
69
70 if (SampleData.Points.empty())
71 return;
72
73 if (SampleData.Points.size() > 1)
74 {
75 DataSeries = new QLineSeries(this);
76 DataSeries->append(SampleData.Points);
77 DataSeries->setPointsVisible(false);
78
79 DataChart->axes()[0]->setRange(SampleData.MinValues.x(), SampleData.MaxValues.x());
80 }
81 else
82 {
83 auto ScatterSeries = new QScatterSeries(this);
84 ScatterSeries->append(SampleData.Points);
85 ScatterSeries->setMarkerShape(QScatterSeries::MarkerShape::MarkerShapeCircle);
86 ScatterSeries->setMarkerSize(15);
87 DataSeries = ScatterSeries;
88
89 DataChart->axes()[0]->setRange(SampleData.MinValues.x() - 2, SampleData.MaxValues.x() + 2);
90 }
91
92 if (PlotAutoscaleAction->isChecked())
93 {
94 if (SampleData.MinValues.y() == SampleData.MaxValues.y())
95 {
96 auto Factor = std::abs(SampleData.MinValues.y()) * .01;
97 if (Factor == 0)
98 Factor = 2;
99
100 DataChart->axes()[1]->setRange(SampleData.MinValues.y() - Factor, SampleData.MaxValues.y() + Factor);
101 }
102 else
103 DataChart->axes()[1]->setRange(SampleData.MinValues.y(), SampleData.MaxValues.y());
104 }
105
106 DataChart->addSeries(DataSeries);
107 DataSeries->attachAxis(DataChart->axes()[0]);
108 DataSeries->attachAxis(DataChart->axes()[1]);
109
110 Multiplier = SampleData.Multiplier;
111 }
112
114 {
115 PlotContextMenu->exec(ui.Signal->mapToGlobal(Position));
116 }
117
119 {
120 // As soon as FinishedSavingDataGuard is destroyed, IsSavingData is set back to false.
122 IsSavingData = true;
123
124 auto Filename = Util::PromptSaveFilePathModule(this, "Save data", ".csv", " Comma-separated values file (*.csv)");
125 if (Filename.isEmpty())
126 return;
127
128 std::stringstream CSVData;
129 CSVData << "X;Y\n";
130 for (int i = 0; i < Util::NumToT<int>(DataSeries->count()); ++i)
131 CSVData << DataSeries->at(i).x() / std::pow(10.0, Multiplier) << ";" << DataSeries->at(i).y() << "\n";
132
133 if (!Util::SaveToFile(Filename, CSVData.str()))
134 QMessageBox::warning(this, "DynExp - Error", "Error writing data to file.");
135 }
136
138 {
139 Points.clear();
140
141 MinValues = {};
142 MaxValues = {};
143 Multiplier = 0;
144 }
145
147 {
148 switch (Multiplier)
149 {
150 case 0: return "";
151 case 3: return "m";
152 case 6: return "u";
153 case 9: return "n";
154 default: return "?";
155 }
156 }
157
159 {
160 // Update CurrentSourceIndex first since GetDataStreamInstr() depends on it.
161 CurrentSourceIndex = Index;
162
163 ValueUnit = GetDataStreamInstr()->GetValueUnit();
164 UIInitialized = false;
165 }
166
171
173 {
174 UIInitialized = false;
175 CurrentSourceIndex = 0; // non-optional DataStreamInstr parameter, so it is ensured that there is at least one source (see RunnableInstance::LockObject()).
176 IsRunning = true;
177 IsBasicSampleTimeUsed = false;
179 PlotAxesChanged = true;
180 Autoscale = true;
181 RollingView = false;
182
183 SampleData.Reset();
184 }
185
187 {
188 std::vector<DynExpInstr::BasicSample> BasicSamples;
189 bool IsBasicSampleTimeUsed{};
190
191 try
192 {
193 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance.ModuleDataGetter());
194
195 if (ModuleData->IsRunning)
196 {
197 ModuleData->GetDataStreamInstr()->ReadData();
198
199 auto InstrData = DynExp::dynamic_InstrumentData_cast<DynExpInstr::DataStreamInstrument>(ModuleData->GetDataStreamInstr()->GetInstrumentData());
200 auto SampleStream = InstrData->GetSampleStream();
201
202 if (!ModuleData->RollingView || !SampleStream->SeekEqual(std::ios_base::in))
203 SampleStream->SeekBeg(std::ios_base::in);
204 BasicSamples = SampleStream->ReadBasicSamples(SampleStream->GetStreamSizeRead());
205 IsBasicSampleTimeUsed = SampleStream->IsBasicSampleTimeUsed();
206 } // InstrData data unlocked here.
207
208 NumFailedUpdateAttempts = 0;
209 } // ModuleData unlocked here.
210 catch (const Util::TimeoutException& e)
211 {
212 if (NumFailedUpdateAttempts++ >= 3)
213 Instance.GetOwner().SetWarning(e);
214 }
215
216 if (ProcessBasicSamples(std::move(BasicSamples), IsBasicSampleTimeUsed))
217 IsBasicSampleTimeUsed = false;
218
219 {
220 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance.ModuleDataGetter());
221
222 if (ModuleData->IsBasicSampleTimeUsed != IsBasicSampleTimeUsed && ModuleData->IsRunning)
223 {
224 ModuleData->IsBasicSampleTimeUsed = IsBasicSampleTimeUsed;
225 ModuleData->PlotAxesChanged = true;
226 }
227 } // ModuleData unlocked here.
228
230 }
231
233 {
234 NumFailedUpdateAttempts = 0;
235 }
236
237 std::unique_ptr<DynExp::QModuleWidget> SignalPlotter::MakeUIWidget()
238 {
239 auto Widget = std::make_unique<SignalPlotterWidget>(*this);
240
241 Connect(Widget->ui.action_Run, &QAction::triggered, this, &SignalPlotter::OnRunClicked);
242 Connect(Widget->ui.CBSource, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SignalPlotter::OnSourceChanged);
243 Connect(Widget->GetAutoscalePlotAction(), &QAction::triggered, this, &SignalPlotter::OnPlotAutoscaleClicked);
244 Connect(Widget->GetRollingViewPlotAction(), &QAction::triggered, this, &SignalPlotter::OnPlotRollingViewClicked);
245 Connect(Widget->GetClearPlotAction(), &QAction::triggered, this, &SignalPlotter::OnClearStream);
246
247 return Widget;
248 }
249
250 void SignalPlotter::UpdateUIChild(const ModuleBase::ModuleDataGetterType& ModuleDataGetter)
251 {
252 auto Widget = GetWidget<SignalPlotterWidget>();
253 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(ModuleDataGetter());
254
255 if (!Widget->ui.CBSource->count())
256 {
257 const QSignalBlocker CBSourceBlocker(Widget->ui.CBSource);
258
259 for (const auto& InstrLabel : ModuleData->GetDataStreamInstrLabels())
260 Widget->ui.CBSource->addItem(QIcon(ModuleData->GetDataStreamInstrIconPath().data()), QString::fromStdString(InstrLabel));
261
262 Widget->ui.CBSource->setCurrentIndex(0);
263 Widget->ui.CBSource->setVisible(Widget->ui.CBSource->count() > 1);
264 }
265
266 if (!ModuleData->IsUIInitialized())
267 {
268 Widget->GetAutoscalePlotAction()->setChecked(ModuleData->Autoscale);
269 Widget->GetRollingViewPlotAction()->setChecked(ModuleData->RollingView);
270
271 auto* XAxis = new QValueAxis(Widget);
272 auto* YAxis = new QValueAxis(Widget);
273 YAxis->setTitleText(QString("Signal in ") + DynExpInstr::DataStreamInstrumentData::UnitTypeToStr(ModuleData->ValueUnit));
275 {
276 YAxis->setLabelFormat("%d");
277 YAxis->setRange(0, 1);
278 YAxis->setTickCount(2);
279 YAxis->setMinorTickCount(0);
280 YAxis->setMinorGridLineVisible(false);
281 }
282
283 Widget->SetAxes(XAxis, YAxis);
284
285 ModuleData->PlotAxesChanged = true;
286 ModuleData->SetUIInitialized();
287 }
288
289 if (ModuleData->PlotAxesChanged ||
290 (ModuleData->IsRunning && ModuleData->SampleData.Multiplier != Widget->GetMultiplier() && !ModuleData->SampleData.Points.empty()))
291 {
292 if (Widget->GetXAxis())
293 {
294 Widget->GetXAxis()->setTitleText(ModuleData->IsBasicSampleTimeUsed ?
295 "time in " + ModuleData->SampleData.GetMultiplierLabel() + "s" : "sample in #");
296 Widget->GetXAxis()->setLabelFormat(ModuleData->IsBasicSampleTimeUsed ? "%.3f" : "%d");
297 }
298
299 ModuleData->PlotAxesChanged = false;
300 }
301
302 if (ModuleData->IsRunning)
303 {
304 Widget->SetData(ModuleData->SampleData);
305 Widget->ui.LNumSamples->setText(QString::number(ModuleData->SampleData.Points.size()) + " sample"
306 + (ModuleData->SampleData.Points.size() == 1 ? "" : "s"));
307 }
308
309 Widget->UpdateUI(ModuleData->IsRunning);
310 }
311
312 bool SignalPlotter::ProcessBasicSamples(std::vector<DynExpInstr::BasicSample>&& BasicSamples, bool IsBasicSampleTimeUsed)
313 {
314 bool FallenBackToNotUseSampleTime = false;
315 unsigned int Multiplier = 0;
316
317 if (BasicSamples.empty())
318 {
319 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(GetModuleData());
320 ModuleData->SampleData.Reset();
321
322 return FallenBackToNotUseSampleTime;
323 }
324
325 // Sorting is believed to be very fast if samples are already sorted by ascending x values.
326 if (IsBasicSampleTimeUsed)
327 {
328 // Use stable_sort() to not affect the order of samples with equal time, in case the data
329 // stream supports sample timing, but the user of the stream ignores sample timing.
330 std::stable_sort(BasicSamples.begin(), BasicSamples.end(), [](const auto& a, const auto& b) {
331 return a.Time < b.Time;
332 });
333
334 // Switch back to use sample indices as x values if all Time values are equal.
335 if (BasicSamples.front().Time == BasicSamples.back().Time && BasicSamples.size() > 1)
336 {
337 IsBasicSampleTimeUsed = false;
338 FallenBackToNotUseSampleTime = true;
339 }
340
341 // Determine best order of magnitude to display the time with.
342 if (std::abs(BasicSamples.front().Time) < 1.0 && std::abs(BasicSamples.back().Time) < 1.0)
343 {
344 Multiplier = 3;
345 if (std::abs(BasicSamples.front().Time) < 1e-3 && std::abs(BasicSamples.back().Time) < 1e-3)
346 {
347 Multiplier = 6;
348 if (std::abs(BasicSamples.front().Time) < 1e-6 && std::abs(BasicSamples.back().Time) < 1e-6)
349 Multiplier = 9;
350 }
351 }
352 }
353
355 double YMin(std::numeric_limits<double>::max()), YMax(std::numeric_limits<double>::lowest());
356
357 for (size_t i = 0; i < BasicSamples.size(); ++i)
358 {
359 auto Y = BasicSamples[i].Value;
360 SampleData.Points.append({ IsBasicSampleTimeUsed ? BasicSamples[i].Time * std::pow(10.0, Multiplier) : i, Y });
361
362 YMin = std::min(YMin, Y);
363 YMax = std::max(YMax, Y);
364 }
365
366 SampleData.MinValues = { SampleData.Points.first().x(), YMin };
367 SampleData.MaxValues = { SampleData.Points.last().x(), YMax };
368 SampleData.Multiplier = Multiplier;
369
370 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(GetModuleData());
371 ModuleData->SampleData = std::move(SampleData);
372
373 return FallenBackToNotUseSampleTime;
374 }
375
377 {
378 auto ModuleParams = DynExp::dynamic_Params_cast<SignalPlotter>(Instance->ParamsGetter());
379 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
380
381 ModuleData->LockInstruments(Instance, ModuleParams->DataStreamInstr);
382
383 ModuleData->ValueUnit = ModuleData->GetDataStreamInstr()->GetValueUnit();
384 ModuleData->Autoscale = ModuleParams->Autoscale.Get();
385 ModuleData->RollingView = ModuleParams->RollingView.Get();
386 }
387
389 {
390 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
391
392 ModuleData->UnlockInstruments(Instance);
393 }
394
395 void SignalPlotter::OnRunClicked(DynExp::ModuleInstance* Instance, bool Checked) const
396 {
397 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
398
399 if (Checked)
400 ModuleData->GetDataStreamInstr()->ResetStreamSize();
401
402 ModuleData->IsRunning = Checked;
403 }
404
406 {
407 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
408
409 ModuleData->SetCurrentSourceIndex(Index);
410 }
411
413 {
414 auto ModuleParams = DynExp::dynamic_Params_cast<SignalPlotter>(GetNonConstParams());
415 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
416
417 ModuleParams->Autoscale = Checked;
418 ModuleData->Autoscale = Checked;
419 }
420
422 {
423 auto ModuleParams = DynExp::dynamic_Params_cast<SignalPlotter>(GetNonConstParams());
424 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
425
426 ModuleParams->RollingView = Checked;
427 ModuleData->RollingView = Checked;
428 }
429
431 {
432 auto ModuleData = DynExp::dynamic_ModuleData_cast<SignalPlotter>(Instance->ModuleDataGetter());
433 auto InstrData = DynExp::dynamic_InstrumentData_cast<DynExpInstr::DataStreamInstrument>(ModuleData->GetDataStreamInstr()->GetInstrumentData());
434
435 InstrData->GetSampleStream()->Clear();
436 }
437}
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...
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:189
Defines data for a thread belonging to a ModuleBase instance. Refer to RunnableInstance.
Definition Module.h:793
const ModuleBase::ModuleDataGetterType ModuleDataGetter
Getter for module's data. Refer to ModuleBase::ModuleDataGetterType.
Definition Module.h:825
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:3671
const auto & GetOwner() const noexcept
Returns Owner.
Definition Object.h:3524
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:261
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.