VisualizationGraphHelper.cpp
523 lines
| 17.5 KiB
| text/x-c
|
CppLexer
r243 | #include "Visualization/VisualizationGraphHelper.h" | |||
Alexandre Leroux
|
r181 | #include "Visualization/qcustomplot.h" | ||
r1420 | #include <Data/ScalarTimeSerie.h> | |||
#include <Data/SpectrogramTimeSerie.h> | ||||
#include <Data/VectorTimeSerie.h> | ||||
Alexandre Leroux
|
r182 | |||
r1420 | #include <Variable/Variable2.h> | |||
Alexandre Leroux
|
r181 | |||
r243 | Q_LOGGING_CATEGORY(LOG_VisualizationGraphHelper, "VisualizationGraphHelper") | |||
Alexandre Leroux
|
r181 | |||
r1420 | namespace | |||
{ | ||||
Alexandre Leroux
|
r182 | |||
r1420 | class SqpDataContainer : public QCPGraphDataContainer | |||
{ | ||||
r364 | public: | |||
r1420 | void appendGraphData(const QCPGraphData& data) { mData.append(data); } | |||
r364 | }; | |||
Alexandre Leroux
|
r583 | /** | ||
r591 | * Struct used to create plottables, depending on the type of the data series from which to create | |||
* them | ||||
Alexandre Leroux
|
r583 | * @tparam T the data series' type | ||
* @remarks Default implementation can't create plottables | ||||
*/ | ||||
template <typename T, typename Enabled = void> | ||||
r1420 | struct PlottablesCreator | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot&, const std::shared_ptr<T>& dataSeries) | |||
{ | ||||
return {}; | ||||
} | ||||
Alexandre Leroux
|
r583 | }; | ||
r235 | ||||
r1420 | PlottablesMap createGraphs(QCustomPlot& plot, int nbGraphs) | |||
Alexandre Leroux
|
r1280 | { | ||
r1420 | PlottablesMap result {}; | |||
Alexandre Leroux
|
r1280 | |||
// Creates {nbGraphs} QCPGraph to add to the plot | ||||
r1420 | for (auto i = 0; i < nbGraphs; ++i) | |||
{ | ||||
Alexandre Leroux
|
r1280 | auto graph = plot.addGraph(); | ||
r1420 | result.insert({ i, graph }); | |||
Alexandre Leroux
|
r1280 | } | ||
plot.replot(); | ||||
return result; | ||||
} | ||||
Alexandre Leroux
|
r583 | /** | ||
Alexandre Leroux
|
r1280 | * Specialization of PlottablesCreator for scalars | ||
Alexandre Leroux
|
r583 | * @sa ScalarSeries | ||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesCreator<T, typename std::enable_if_t<std::is_base_of<ScalarTimeSerie, T>::value>> | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | |||
{ | ||||
return createGraphs(plot, 1); | ||||
} | ||||
Alexandre Leroux
|
r1280 | }; | ||
Alexandre Leroux
|
r583 | |||
Alexandre Leroux
|
r1280 | /** | ||
* Specialization of PlottablesCreator for vectors | ||||
* @sa VectorSeries | ||||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesCreator<T, typename std::enable_if_t<std::is_base_of<VectorTimeSerie, T>::value>> | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | |||
{ | ||||
return createGraphs(plot, 3); | ||||
} | ||||
}; | ||||
/** | ||||
* Specialization of PlottablesCreator for MultiComponentTimeSeries | ||||
* @sa VectorSeries | ||||
*/ | ||||
template <typename T> | ||||
struct PlottablesCreator<T, | ||||
typename std::enable_if_t<std::is_base_of<MultiComponentTimeSerie, T>::value>> | ||||
{ | ||||
static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | ||||
{ | ||||
return createGraphs(plot, dataSeries->size(1)); | ||||
} | ||||
Alexandre Leroux
|
r583 | }; | ||
r548 | ||||
Alexandre Leroux
|
r902 | /** | ||
* Specialization of PlottablesCreator for spectrograms | ||||
* @sa SpectrogramSeries | ||||
*/ | ||||
template <typename T> | ||||
struct PlottablesCreator<T, | ||||
r1420 | typename std::enable_if_t<std::is_base_of<SpectrogramTimeSerie, T>::value>> | |||
{ | ||||
r1431 | static PlottablesMap createPlottables(QCustomPlot& plot, const std::shared_ptr<T>& dataSeries) | |||
Alexandre Leroux
|
r902 | { | ||
r1420 | PlottablesMap result {}; | |||
result.insert({ 0, new QCPColorMap { plot.xAxis, plot.yAxis } }); | ||||
Alexandre Leroux
|
r902 | |||
plot.replot(); | ||||
return result; | ||||
} | ||||
}; | ||||
Alexandre Leroux
|
r583 | /** | ||
r591 | * Struct used to update plottables, depending on the type of the data series from which to update | |||
* them | ||||
Alexandre Leroux
|
r583 | * @tparam T the data series' type | ||
* @remarks Default implementation can't update plottables | ||||
*/ | ||||
template <typename T, typename Enabled = void> | ||||
r1420 | struct PlottablesUpdater | |||
{ | ||||
static void setPlotYAxisRange(T&, const DateTimeRange&, QCustomPlot&) | ||||
Alexandre Leroux
|
r900 | { | ||
Alexandre Leroux
|
r905 | qCCritical(LOG_VisualizationGraphHelper()) | ||
Alexandre Leroux
|
r900 | << QObject::tr("Can't set plot y-axis range: unmanaged data series type"); | ||
} | ||||
r1420 | static void updatePlottables(T&, PlottablesMap&, const DateTimeRange&, bool) | |||
Alexandre Leroux
|
r583 | { | ||
Alexandre Leroux
|
r905 | qCCritical(LOG_VisualizationGraphHelper()) | ||
Alexandre Leroux
|
r583 | << QObject::tr("Can't update plottables: unmanaged data series type"); | ||
} | ||||
}; | ||||
Alexandre Leroux
|
r182 | |||
Alexandre Leroux
|
r583 | /** | ||
* Specialization of PlottablesUpdater for scalars and vectors | ||||
* @sa ScalarSeries | ||||
* @sa VectorSeries | ||||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesUpdater<T, typename std::enable_if_t<std::is_base_of<ScalarTimeSerie, T>::value>> | |||
{ | ||||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
Alexandre Leroux
|
r900 | { | ||
auto minValue = 0., maxValue = 0.; | ||||
r1420 | if (auto serie = dynamic_cast<ScalarTimeSerie*>(&dataSeries)) | |||
{ | ||||
r1423 | if (serie->size()) | |||
{ | ||||
maxValue = (*std::max_element(std::begin(*serie), std::end(*serie))).v(); | ||||
minValue = (*std::min_element(std::begin(*serie), std::end(*serie))).v(); | ||||
} | ||||
Alexandre Leroux
|
r900 | } | ||
r1420 | plot.yAxis->setRange(QCPRange { minValue, maxValue }); | |||
Alexandre Leroux
|
r900 | } | ||
r1420 | static void updatePlottables( | |||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
Alexandre Leroux
|
r583 | { | ||
// For each plottable to update, resets its data | ||||
r1420 | for (const auto& plottable : plottables) | |||
{ | ||||
if (auto graph = dynamic_cast<QCPGraph*>(plottable.second)) | ||||
{ | ||||
Alexandre Leroux
|
r583 | auto dataContainer = QSharedPointer<SqpDataContainer>::create(); | ||
r1420 | if (auto serie = dynamic_cast<ScalarTimeSerie*>(&dataSeries)) | |||
{ | ||||
std::for_each( | ||||
std::begin(*serie), std::end(*serie), [&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData(QCPGraphData(value.t(), value.v())); | ||||
}); | ||||
} | ||||
Alexandre Leroux
|
r583 | graph->setData(dataContainer); | ||
} | ||||
} | ||||
r235 | ||||
r1420 | if (!plottables.empty()) | |||
{ | ||||
Alexandre Leroux
|
r583 | auto plot = plottables.begin()->second->parentPlot(); | ||
Alexandre Leroux
|
r183 | |||
r1420 | if (rescaleAxes) | |||
{ | ||||
Alexandre Leroux
|
r583 | plot->rescaleAxes(); | ||
} | ||||
} | ||||
Alexandre Leroux
|
r182 | } | ||
Alexandre Leroux
|
r583 | }; | ||
r1420 | ||||
Alexandre Leroux
|
r903 | template <typename T> | ||
r1420 | struct PlottablesUpdater<T, typename std::enable_if_t<std::is_base_of<VectorTimeSerie, T>::value>> | |||
{ | ||||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
Alexandre Leroux
|
r904 | { | ||
r1420 | double minValue = 0., maxValue = 0.; | |||
if (auto serie = dynamic_cast<VectorTimeSerie*>(&dataSeries)) | ||||
{ | ||||
std::for_each( | ||||
std::begin(*serie), std::end(*serie), [&minValue, &maxValue](const auto& v) { | ||||
minValue = std::min({ minValue, v.v().x, v.v().y, v.v().z }); | ||||
maxValue = std::max({ maxValue, v.v().x, v.v().y, v.v().z }); | ||||
}); | ||||
Alexandre Leroux
|
r904 | } | ||
r1420 | ||||
plot.yAxis->setRange(QCPRange { minValue, maxValue }); | ||||
Alexandre Leroux
|
r904 | } | ||
r1420 | static void updatePlottables( | |||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
Alexandre Leroux
|
r903 | { | ||
r1420 | // For each plottable to update, resets its data | |||
for (const auto& plottable : plottables) | ||||
{ | ||||
if (auto graph = dynamic_cast<QCPGraph*>(plottable.second)) | ||||
{ | ||||
auto dataContainer = QSharedPointer<SqpDataContainer>::create(); | ||||
if (auto serie = dynamic_cast<VectorTimeSerie*>(&dataSeries)) | ||||
{ | ||||
switch (plottable.first) | ||||
{ | ||||
case 0: | ||||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value.v().x)); | ||||
}); | ||||
break; | ||||
case 1: | ||||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value.v().y)); | ||||
}); | ||||
break; | ||||
case 2: | ||||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value.v().z)); | ||||
}); | ||||
break; | ||||
default: | ||||
break; | ||||
} | ||||
Alexandre Leroux
|
r922 | } | ||
r1420 | graph->setData(dataContainer); | |||
Alexandre Leroux
|
r903 | } | ||
} | ||||
r1420 | if (!plottables.empty()) | |||
{ | ||||
auto plot = plottables.begin()->second->parentPlot(); | ||||
Alexandre Leroux
|
r903 | |||
r1420 | if (rescaleAxes) | |||
{ | ||||
plot->rescaleAxes(); | ||||
} | ||||
Alexandre Leroux
|
r903 | } | ||
} | ||||
}; | ||||
r1431 | ||||
template <typename T> | ||||
r1432 | struct PlottablesUpdater<T, | |||
typename std::enable_if_t<std::is_base_of<MultiComponentTimeSerie, T>::value>> | ||||
r1431 | { | |||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
{ | ||||
double minValue = 0., maxValue = 0.; | ||||
if (auto serie = dynamic_cast<MultiComponentTimeSerie*>(&dataSeries)) | ||||
{ | ||||
r1436 | std::for_each( | |||
std::begin(*serie), std::end(*serie), [&minValue, &maxValue](const auto& v) { | ||||
minValue = std::min( minValue, std::min_element(v.begin(), v.end())->v() ); | ||||
maxValue = std::max( maxValue, std::max_element(v.begin(), v.end())->v() ); | ||||
}); | ||||
r1431 | } | |||
plot.yAxis->setRange(QCPRange { minValue, maxValue }); | ||||
} | ||||
static void updatePlottables( | ||||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
{ | ||||
for (const auto& plottable : plottables) | ||||
{ | ||||
if (auto graph = dynamic_cast<QCPGraph*>(plottable.second)) | ||||
{ | ||||
auto dataContainer = QSharedPointer<SqpDataContainer>::create(); | ||||
if (auto serie = dynamic_cast<MultiComponentTimeSerie*>(&dataSeries)) | ||||
{ | ||||
r1432 | // TODO | |||
std::for_each(std::begin(*serie), std::end(*serie), | ||||
[&dataContainer, component = plottable.first](const auto& value) { | ||||
dataContainer->appendGraphData( | ||||
QCPGraphData(value.t(), value[component])); | ||||
}); | ||||
r1431 | } | |||
graph->setData(dataContainer); | ||||
} | ||||
} | ||||
if (!plottables.empty()) | ||||
{ | ||||
auto plot = plottables.begin()->second->parentPlot(); | ||||
if (rescaleAxes) | ||||
{ | ||||
plot->rescaleAxes(); | ||||
} | ||||
} | ||||
} | ||||
}; | ||||
r1420 | /** | |||
* Specialization of PlottablesUpdater for spectrograms | ||||
* @sa SpectrogramSeries | ||||
*/ | ||||
template <typename T> | ||||
struct PlottablesUpdater<T, | ||||
typename std::enable_if_t<std::is_base_of<SpectrogramTimeSerie, T>::value>> | ||||
{ | ||||
static void setPlotYAxisRange(T& dataSeries, const DateTimeRange& xAxisRange, QCustomPlot& plot) | ||||
{ | ||||
// TODO | ||||
// double min, max; | ||||
// std::tie(min, max) = dataSeries.yBounds(); | ||||
// if (!std::isnan(min) && !std::isnan(max)) | ||||
// { | ||||
// plot.yAxis->setRange(QCPRange { min, max }); | ||||
// } | ||||
} | ||||
static void updatePlottables( | ||||
T& dataSeries, PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) | ||||
{ | ||||
// TODO | ||||
// if (plottables.empty()) | ||||
// { | ||||
// qCDebug(LOG_VisualizationGraphHelper()) | ||||
// << QObject::tr("Can't update spectrogram: no colormap has been | ||||
// associated"); | ||||
// return; | ||||
// } | ||||
// // Gets the colormap to update (normally there is only one colormap) | ||||
// Q_ASSERT(plottables.size() == 1); | ||||
// auto colormap = dynamic_cast<QCPColorMap*>(plottables.at(0)); | ||||
// Q_ASSERT(colormap != nullptr); | ||||
// dataSeries.lockRead(); | ||||
// // Processing spectrogram data for display in QCustomPlot | ||||
// auto its = dataSeries.xAxisRange(range.m_TStart, range.m_TEnd); | ||||
// // Computes logarithmic y-axis resolution for the spectrogram | ||||
// auto yData = its.first->y(); | ||||
// auto yResolution = DataSeriesUtils::resolution(yData.begin(), yData.end(), true); | ||||
// // Generates mesh for colormap | ||||
// auto mesh = DataSeriesUtils::regularMesh(its.first, its.second, | ||||
// DataSeriesUtils::Resolution { dataSeries.xResolution() }, yResolution); | ||||
// dataSeries.unlock(); | ||||
// colormap->data()->setSize(mesh.m_NbX, mesh.m_NbY); | ||||
// if (!mesh.isEmpty()) | ||||
// { | ||||
// colormap->data()->setRange(QCPRange { mesh.m_XMin, mesh.xMax() }, | ||||
// // y-axis range is converted to linear values | ||||
// QCPRange { std::pow(10, mesh.m_YMin), std::pow(10, mesh.yMax()) }); | ||||
// // Sets values | ||||
// auto index = 0; | ||||
// for (auto it = mesh.m_Data.begin(), end = mesh.m_Data.end(); it != end; ++it, | ||||
// ++index) | ||||
// { | ||||
// auto xIndex = index % mesh.m_NbX; | ||||
// auto yIndex = index / mesh.m_NbX; | ||||
// colormap->data()->setCell(xIndex, yIndex, *it); | ||||
// // Makes the NaN values to be transparent in the colormap | ||||
// if (std::isnan(*it)) | ||||
// { | ||||
// colormap->data()->setAlpha(xIndex, yIndex, 0); | ||||
// } | ||||
// } | ||||
// } | ||||
// // Rescales axes | ||||
// auto plot = colormap->parentPlot(); | ||||
// if (rescaleAxes) | ||||
// { | ||||
// plot->rescaleAxes(); | ||||
// } | ||||
} | ||||
}; | ||||
Alexandre Leroux
|
r583 | /** | ||
* Helper used to create/update plottables | ||||
*/ | ||||
r1420 | struct IPlottablesHelper | |||
{ | ||||
Alexandre Leroux
|
r583 | virtual ~IPlottablesHelper() noexcept = default; | ||
r1420 | virtual PlottablesMap create(QCustomPlot& plot) const = 0; | |||
virtual void setYAxisRange(const DateTimeRange& xAxisRange, QCustomPlot& plot) const = 0; | ||||
virtual void update( | ||||
PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes = false) const = 0; | ||||
Alexandre Leroux
|
r583 | }; | ||
/** | ||||
* Default implementation of IPlottablesHelper, which takes data series to create/update plottables | ||||
* @tparam T the data series' type | ||||
*/ | ||||
template <typename T> | ||||
r1420 | struct PlottablesHelper : public IPlottablesHelper | |||
{ | ||||
r1421 | explicit PlottablesHelper(std::shared_ptr<T> dataSeries) : m_DataSeries { dataSeries } {} | |||
Alexandre Leroux
|
r583 | |||
r1420 | PlottablesMap create(QCustomPlot& plot) const override | |||
Alexandre Leroux
|
r583 | { | ||
r1431 | return PlottablesCreator<T>::createPlottables(plot, m_DataSeries); | |||
Alexandre Leroux
|
r182 | } | ||
r1420 | void update( | |||
PlottablesMap& plottables, const DateTimeRange& range, bool rescaleAxes) const override | ||||
Alexandre Leroux
|
r583 | { | ||
r1420 | if (m_DataSeries) | |||
{ | ||||
Alexandre Leroux
|
r1280 | PlottablesUpdater<T>::updatePlottables(*m_DataSeries, plottables, range, rescaleAxes); | ||
} | ||||
r1420 | else | |||
{ | ||||
Alexandre Leroux
|
r1280 | qCCritical(LOG_VisualizationGraphHelper()) << "Can't update plottables: inconsistency " | ||
"between the type of data series and the " | ||||
"type supposed"; | ||||
} | ||||
Alexandre Leroux
|
r583 | } | ||
Alexandre Leroux
|
r182 | |||
r1420 | void setYAxisRange(const DateTimeRange& xAxisRange, QCustomPlot& plot) const override | |||
Alexandre Leroux
|
r900 | { | ||
r1420 | if (m_DataSeries) | |||
{ | ||||
Alexandre Leroux
|
r1280 | PlottablesUpdater<T>::setPlotYAxisRange(*m_DataSeries, xAxisRange, plot); | ||
} | ||||
r1420 | else | |||
{ | ||||
Alexandre Leroux
|
r1280 | qCCritical(LOG_VisualizationGraphHelper()) << "Can't update plottables: inconsistency " | ||
"between the type of data series and the " | ||||
"type supposed"; | ||||
} | ||||
Alexandre Leroux
|
r900 | } | ||
r1421 | std::shared_ptr<T> m_DataSeries; | |||
Alexandre Leroux
|
r583 | }; | ||
Alexandre Leroux
|
r182 | |||
Alexandre Leroux
|
r1280 | /// Creates IPlottablesHelper according to the type of data series a variable holds | ||
r1420 | std::unique_ptr<IPlottablesHelper> createHelper(std::shared_ptr<Variable2> variable) noexcept | |||
r548 | { | |||
r1420 | switch (variable->type()) | |||
{ | ||||
Alexandre Leroux
|
r1280 | case DataSeriesType::SCALAR: | ||
r1420 | return std::make_unique<PlottablesHelper<ScalarTimeSerie>>( | |||
r1421 | std::dynamic_pointer_cast<ScalarTimeSerie>(variable->data())); | |||
Alexandre Leroux
|
r1280 | case DataSeriesType::SPECTROGRAM: | ||
r1420 | return std::make_unique<PlottablesHelper<SpectrogramTimeSerie>>( | |||
r1421 | std::dynamic_pointer_cast<SpectrogramTimeSerie>(variable->data())); | |||
Alexandre Leroux
|
r1280 | case DataSeriesType::VECTOR: | ||
r1420 | return std::make_unique<PlottablesHelper<VectorTimeSerie>>( | |||
r1421 | std::dynamic_pointer_cast<VectorTimeSerie>(variable->data())); | |||
r1431 | case DataSeriesType::MULTICOMPONENT: | |||
return std::make_unique<PlottablesHelper<MultiComponentTimeSerie>>( | ||||
std::dynamic_pointer_cast<MultiComponentTimeSerie>(variable->data())); | ||||
Alexandre Leroux
|
r1280 | default: | ||
// Creates default helper | ||||
break; | ||||
r548 | } | |||
Alexandre Leroux
|
r1280 | |||
r1420 | return std::make_unique<PlottablesHelper<TimeSeries::ITimeSerie>>(nullptr); | |||
r548 | } | |||
Alexandre Leroux
|
r583 | } // namespace | ||
Alexandre Leroux
|
r181 | |||
r1420 | PlottablesMap VisualizationGraphHelper::create( | |||
std::shared_ptr<Variable2> variable, QCustomPlot& plot) noexcept | ||||
Alexandre Leroux
|
r583 | { | ||
r1420 | if (variable) | |||
{ | ||||
Alexandre Leroux
|
r1280 | auto helper = createHelper(variable); | ||
Alexandre Leroux
|
r583 | auto plottables = helper->create(plot); | ||
return plottables; | ||||
Alexandre Leroux
|
r181 | } | ||
r1420 | else | |||
{ | ||||
r243 | qCDebug(LOG_VisualizationGraphHelper()) | |||
Alexandre Leroux
|
r181 | << QObject::tr("Can't create graph plottables : the variable is null"); | ||
r1420 | return PlottablesMap {}; | |||
Alexandre Leroux
|
r181 | } | ||
} | ||||
r235 | ||||
r1420 | void VisualizationGraphHelper::setYAxisRange( | |||
std::shared_ptr<Variable2> variable, QCustomPlot& plot) noexcept | ||||
Alexandre Leroux
|
r900 | { | ||
r1420 | if (variable) | |||
{ | ||||
Alexandre Leroux
|
r1280 | auto helper = createHelper(variable); | ||
Alexandre Leroux
|
r900 | helper->setYAxisRange(variable->range(), plot); | ||
} | ||||
r1420 | else | |||
{ | ||||
Alexandre Leroux
|
r900 | qCDebug(LOG_VisualizationGraphHelper()) | ||
<< QObject::tr("Can't set y-axis range of plot: the variable is null"); | ||||
} | ||||
} | ||||
r1420 | void VisualizationGraphHelper::updateData( | |||
PlottablesMap& plottables, std::shared_ptr<Variable2> variable, const DateTimeRange& dateTime) | ||||
r235 | { | |||
Alexandre Leroux
|
r1280 | auto helper = createHelper(variable); | ||
Alexandre Leroux
|
r583 | helper->update(plottables, dateTime); | ||
r235 | } | |||