DynExp
Highly flexible laboratory automation for dynamically changing experiments.
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 
7 namespace 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 
168  {
169  Init();
170  }
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...
Definition: SignalPlotter.h:75
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
@ Y
Y component of the signal in cartesian coordinates.
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.