• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 17468365695

04 Sep 2025 03:09PM UTC coverage: 70.849% (+0.02%) from 70.828%
17468365695

push

github

paulmthompson
faster loading for multi channel analog

0 of 3 new or added lines in 1 file covered. (0.0%)

770 existing lines in 6 files now uncovered.

34341 of 48471 relevant lines covered (70.85%)

1293.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

37.68
/src/WhiskerToolbox/DataViewer_Widget/DataViewer_Widget.cpp
1
#include "DataViewer_Widget.hpp"
2
#include "ui_DataViewer_Widget.h"
3

4
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
5
#include "AnalogTimeSeries/utils/statistics.hpp"
6
#include "DataManager.hpp"
7
#include "DigitalTimeSeries/Digital_Event_Series.hpp"
8
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
9

10
#include "DataViewer/AnalogTimeSeries/AnalogTimeSeriesDisplayOptions.hpp"
11
#include "DataViewer/AnalogTimeSeries/MVP_AnalogTimeSeries.hpp"
12
#include "DataViewer/DigitalEvent/DigitalEventSeriesDisplayOptions.hpp"
13
#include "DataViewer/DigitalEvent/MVP_DigitalEvent.hpp"
14
#include "DataViewer/DigitalInterval/DigitalIntervalSeriesDisplayOptions.hpp"
15
#include "DataViewer/DigitalInterval/MVP_DigitalInterval.hpp"
16
#include "Feature_Tree_Model.hpp"
17
#include "Feature_Tree_Widget/Feature_Tree_Widget.hpp"
18
#include "OpenGLWidget.hpp"
19
#include "TimeFrame/TimeFrame.hpp"
20
#include "TimeScrollBar/TimeScrollBar.hpp"
21

22
#include "AnalogTimeSeries/AnalogViewer_Widget.hpp"
23
#include "DigitalEvent/EventViewer_Widget.hpp"
24
#include "DigitalInterval/IntervalViewer_Widget.hpp"
25

26
#include <QMetaObject>
27
#include <QPointer>
28
#include <QTableWidget>
29
#include <QWheelEvent>
30
#include <QMenu>
31
#include <QFileDialog>
32
#include <QFile>
33

34
#include <algorithm>
35
#include <cmath>
36
#include <iostream>
37
#include <sstream>
38

39
DataViewer_Widget::DataViewer_Widget(std::shared_ptr<DataManager> data_manager,
15✔
40
                                     TimeScrollBar * time_scrollbar,
41
                                     MainWindow * main_window,
42
                                     QWidget * parent)
15✔
43
    : QWidget(parent),
44
      _data_manager{std::move(data_manager)},
15✔
45
      _time_scrollbar{time_scrollbar},
15✔
46
      _main_window{main_window},
15✔
47
      ui(new Ui::DataViewer_Widget) {
30✔
48

49
    ui->setupUi(this);
15✔
50

51
    // Initialize plotting manager with default viewport
52
    _plotting_manager = std::make_unique<PlottingManager>();
15✔
53

54
    // Provide PlottingManager reference to OpenGL widget
55
    ui->openGLWidget->setPlottingManager(_plotting_manager.get());
15✔
56

57
    // Initialize feature tree model
58
    _feature_tree_model = std::make_unique<Feature_Tree_Model>(this);
15✔
59
    _feature_tree_model->setDataManager(_data_manager);
15✔
60

61
    // Set up observer to automatically clean up data when it's deleted from DataManager
62
    // Queue the cleanup to the Qt event loop to avoid running during mid-update mutations
63
    _data_manager->addObserver([this]() {
15✔
64
        QPointer<DataViewer_Widget> self = this;
15✔
65
        QMetaObject::invokeMethod(self, [self]() {
15✔
66
            if (!self) return;
15✔
67
            self->cleanupDeletedData(); }, Qt::QueuedConnection);
15✔
68
    });
30✔
69

70
    // Configure Feature_Tree_Widget
71
    ui->feature_tree_widget->setTypeFilters({DM_DataType::Analog, DM_DataType::DigitalEvent, DM_DataType::DigitalInterval});
45✔
72
    ui->feature_tree_widget->setDataManager(_data_manager);
15✔
73

74
    // Connect Feature_Tree_Widget signals using the new interface
75
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::featureSelected, this, [this](std::string const & feature) {
15✔
76
        _handleFeatureSelected(QString::fromStdString(feature));
×
77
    });
×
78

79
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeature, this, [this](std::string const & feature) {
15✔
UNCOV
80
        std::cout << "Adding single feature: " << feature << std::endl;
×
81
        _addFeatureToModel(QString::fromStdString(feature), true);
×
82
    });
×
83

84
    // Install context menu handling on the embedded tree widget
85
    ui->feature_tree_widget->treeWidget()->setContextMenuPolicy(Qt::CustomContextMenu);
15✔
86
    connect(ui->feature_tree_widget->treeWidget(), &QTreeWidget::customContextMenuRequested, this, [this](QPoint const & pos) {
15✔
UNCOV
87
        auto * tw = ui->feature_tree_widget->treeWidget();
×
UNCOV
88
        QTreeWidgetItem * item = tw->itemAt(pos);
×
89
        if (!item) return;
×
90
        std::string const key = item->text(0).toStdString();
×
91
        // Group items have type column text "Group" and are under Analog data type parent
UNCOV
92
        QString const type_text = item->text(1);
×
UNCOV
93
        if (type_text == "Group") {
×
94
            // Determine if parent is Analog data type or children are analog keys
95
            bool isAnalogGroup = false;
×
96
            if (auto * parent = item->parent()) {
×
UNCOV
97
                if (parent->text(0) == QString::fromStdString(convert_data_type_to_string(DM_DataType::Analog))) {
×
UNCOV
98
                    isAnalogGroup = true;
×
99
                }
100
            }
101
            if (isAnalogGroup) {
×
102
                QPoint const global_pos = tw->viewport()->mapToGlobal(pos);
×
UNCOV
103
                _showGroupContextMenu(key, global_pos);
×
104
            }
105
        }
UNCOV
106
    });
×
107

108
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::removeFeature, this, [this](std::string const & feature) {
15✔
UNCOV
109
        std::cout << "Removing single feature: " << feature << std::endl;
×
110
        _addFeatureToModel(QString::fromStdString(feature), false);
×
111
    });
×
112

113
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeatures, this, [this](std::vector<std::string> const & features) {
15✔
UNCOV
114
        std::cout << "Adding " << features.size() << " features as group" << std::endl;
×
115

116
        // Process all features in the group without triggering individual canvas updates
117
        for (auto const & key: features) {
×
UNCOV
118
            _plotSelectedFeatureWithoutUpdate(key);
×
119
        }
120

121
        // Auto-arrange and auto-fill when toggling a group to optimize space usage
122
        if (!features.empty()) {
×
123
            std::cout << "Auto-arranging and filling canvas for group toggle" << std::endl;
×
UNCOV
124
            autoArrangeVerticalSpacing();// This now includes auto-fill functionality
×
125
        }
126

127
        // Trigger a single canvas update at the end
UNCOV
128
        if (!features.empty()) {
×
UNCOV
129
            std::cout << "Triggering single canvas update for group toggle" << std::endl;
×
UNCOV
130
            ui->openGLWidget->updateCanvas();
×
131
        }
132
    });
×
133

134
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::removeFeatures, this, [this](std::vector<std::string> const & features) {
15✔
135
        std::cout << "Removing " << features.size() << " features as group" << std::endl;
×
136

137
        // Process all features in the group without triggering individual canvas updates
UNCOV
138
        for (auto const & key: features) {
×
UNCOV
139
            _removeSelectedFeatureWithoutUpdate(key);
×
140
        }
141

142
        // Auto-arrange and auto-fill when toggling a group to optimize space usage
UNCOV
143
        if (!features.empty()) {
×
UNCOV
144
            std::cout << "Auto-arranging and filling canvas for group toggle" << std::endl;
×
UNCOV
145
            autoArrangeVerticalSpacing();// This now includes auto-fill functionality
×
146
        }
147

148
        // Trigger a single canvas update at the end
UNCOV
149
        if (!features.empty()) {
×
UNCOV
150
            std::cout << "Triggering single canvas update for group toggle" << std::endl;
×
UNCOV
151
            ui->openGLWidget->updateCanvas();
×
152
        }
153
    });
×
154

155
    // Connect color change signals from the model
156
    connect(_feature_tree_model.get(), &Feature_Tree_Model::featureColorChanged, this, &DataViewer_Widget::_handleColorChanged);
15✔
157

158
    // Connect color change signals from the tree widget to the model
159
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::colorChangeFeatures, this, [this](std::vector<std::string> const & features, std::string const & hex_color) {
15✔
UNCOV
160
        for (auto const & feature: features) {
×
UNCOV
161
            _feature_tree_model->setFeatureColor(feature, hex_color);
×
162
        }
UNCOV
163
    });
×
164

165
    connect(ui->x_axis_samples, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleXAxisSamplesChanged);
15✔
166

167
    connect(ui->global_zoom, &QDoubleSpinBox::valueChanged, this, &DataViewer_Widget::_updateGlobalScale);
15✔
168

169
    connect(ui->theme_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DataViewer_Widget::_handleThemeChanged);
15✔
170

171
    connect(time_scrollbar, &TimeScrollBar::timeChanged, this, &DataViewer_Widget::_updatePlot);
15✔
172

173
    //We should alwasy get the master clock because we plot
174
    // Check for master clock
175
    auto time_keys = _data_manager->getTimeFrameKeys();
15✔
176
    // if timekeys doesn't have master, we should throw an error
177
    if (std::find(time_keys.begin(), time_keys.end(), TimeKey("master")) == time_keys.end()) {
15✔
178
        std::cout << "No master clock found in DataManager" << std::endl;
15✔
179
        _time_frame = _data_manager->getTime(TimeKey("time"));
15✔
180
    } else {
UNCOV
181
        _time_frame = _data_manager->getTime(TimeKey("master"));
×
182
    }
183

184
    std::cout << "Setting GL limit to " << _time_frame->getTotalFrameCount() << std::endl;
15✔
185
    ui->openGLWidget->setXLimit(_time_frame->getTotalFrameCount());
15✔
186

187
    // Set the master time frame for proper coordinate conversion
188
    ui->openGLWidget->setMasterTimeFrame(_time_frame);
15✔
189

190
    // Set spinbox maximum to the actual data range (not the hardcoded UI limit)
191
    int const data_range = static_cast<int>(_time_frame->getTotalFrameCount());
15✔
192
    std::cout << "Setting x_axis_samples maximum to " << data_range << std::endl;
15✔
193
    ui->x_axis_samples->setMaximum(data_range);
15✔
194

195
    // Setup stacked widget with data-type specific viewers
196
    auto analog_widget = new AnalogViewer_Widget(_data_manager, ui->openGLWidget);
15✔
197
    auto interval_widget = new IntervalViewer_Widget(_data_manager, ui->openGLWidget);
15✔
198
    auto event_widget = new EventViewer_Widget(_data_manager, ui->openGLWidget);
15✔
199

200
    ui->stackedWidget->addWidget(analog_widget);
15✔
201
    ui->stackedWidget->addWidget(interval_widget);
15✔
202
    ui->stackedWidget->addWidget(event_widget);
15✔
203

204
    // Connect color change signals from sub-widgets
205
    connect(analog_widget, &AnalogViewer_Widget::colorChanged,
45✔
206
            this, &DataViewer_Widget::_handleColorChanged);
30✔
207
    connect(interval_widget, &IntervalViewer_Widget::colorChanged,
45✔
208
            this, &DataViewer_Widget::_handleColorChanged);
30✔
209
    connect(event_widget, &EventViewer_Widget::colorChanged,
45✔
210
            this, &DataViewer_Widget::_handleColorChanged);
30✔
211

212
    // Connect mouse hover signal from OpenGL widget
213
    connect(ui->openGLWidget, &OpenGLWidget::mouseHover,
45✔
214
            this, &DataViewer_Widget::_updateCoordinateDisplay);
30✔
215

216
    // Grid line connections
217
    connect(ui->grid_lines_enabled, &QCheckBox::toggled, this, &DataViewer_Widget::_handleGridLinesToggled);
15✔
218
    connect(ui->grid_spacing, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleGridSpacingChanged);
15✔
219

220
    // Vertical spacing connection
221
    connect(ui->vertical_spacing, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &DataViewer_Widget::_handleVerticalSpacingChanged);
15✔
222

223
    // Auto-arrange button connection
224
    connect(ui->auto_arrange_button, &QPushButton::clicked, this, &DataViewer_Widget::autoArrangeVerticalSpacing);
15✔
225

226
    // Initialize grid line UI to match OpenGLWidget defaults
227
    ui->grid_lines_enabled->setChecked(ui->openGLWidget->getGridLinesEnabled());
15✔
228
    ui->grid_spacing->setValue(ui->openGLWidget->getGridSpacing());
15✔
229

230
    // Initialize vertical spacing UI to match OpenGLWidget defaults
231
    ui->vertical_spacing->setValue(static_cast<double>(ui->openGLWidget->getVerticalSpacing()));
15✔
232
}
30✔
233

234
DataViewer_Widget::~DataViewer_Widget() {
29✔
235
    delete ui;
15✔
236
}
29✔
237

238
void DataViewer_Widget::openWidget() {
13✔
239
    std::cout << "DataViewer Widget Opened" << std::endl;
13✔
240

241
    // Tree is already populated by observer pattern in setDataManager()
242
    // Trigger refresh in case of manual opening
243
    ui->feature_tree_widget->refreshTree();
13✔
244

245
    this->show();
13✔
246
    _updateLabels();
13✔
247
}
13✔
248

UNCOV
249
void DataViewer_Widget::closeEvent(QCloseEvent * event) {
×
250
    static_cast<void>(event);
251

UNCOV
252
    std::cout << "Close event detected" << std::endl;
×
UNCOV
253
}
×
254

255
void DataViewer_Widget::resizeEvent(QResizeEvent * event) {
13✔
256
    QWidget::resizeEvent(event);
13✔
257

258
    // Update plotting manager dimensions when widget is resized
259
    _updatePlottingManagerDimensions();
13✔
260

261
    // The OpenGL widget will automatically get its resizeGL called by Qt
262
    // but we can trigger an additional update if needed
263
    if (ui->openGLWidget) {
13✔
264
        ui->openGLWidget->update();
13✔
265
    }
266
}
13✔
267

268
void DataViewer_Widget::_updatePlot(int time) {
1✔
269
    //std::cout << "Time is " << time;
270
    time = _data_manager->getTime(TimeKey("time"))->getTimeAtIndex(TimeFrameIndex(time));
1✔
271
    //std::cout << ""
272
    ui->openGLWidget->updateCanvas(time);
1✔
273

274
    _updateLabels();
1✔
275
}
1✔
276

277

278
void DataViewer_Widget::_addFeatureToModel(QString const & feature, bool enabled) {
13✔
279
    std::cout << "Feature toggle signal received: " << feature.toStdString() << " enabled: " << enabled << std::endl;
13✔
280

281
    if (enabled) {
13✔
282
        _plotSelectedFeature(feature.toStdString());
13✔
283
    } else {
UNCOV
284
        _removeSelectedFeature(feature.toStdString());
×
285
    }
286
}
13✔
287

288
void DataViewer_Widget::_plotSelectedFeature(std::string const & key) {
13✔
289
    std::cout << "Attempting to plot feature: " << key << std::endl;
13✔
290

291
    if (key.empty()) {
13✔
UNCOV
292
        std::cerr << "Error: empty key in _plotSelectedFeature" << std::endl;
×
UNCOV
293
        return;
×
294
    }
295

296
    if (!_data_manager) {
13✔
UNCOV
297
        std::cerr << "Error: null data manager in _plotSelectedFeature" << std::endl;
×
UNCOV
298
        return;
×
299
    }
300

301
    // Get color from model
302
    std::string color = _feature_tree_model->getFeatureColor(key);
13✔
303
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
13✔
304

305
    auto data_type = _data_manager->getType(key);
13✔
306
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
13✔
307

308
    // Register with plotting manager for coordinated positioning
309
    if (_plotting_manager) {
13✔
310
        std::cout << "Registering series with plotting manager: " << key << std::endl;
13✔
311
    }
312

313
    if (data_type == DM_DataType::Analog) {
13✔
314

315
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
13✔
316
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
13✔
317
        if (!series) {
13✔
UNCOV
318
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
UNCOV
319
            return;
×
320
        }
321

322

323
        auto time_key = _data_manager->getTimeKey(key);
13✔
324
        std::cout << "Time frame key: " << time_key << std::endl;
13✔
325
        auto time_frame = _data_manager->getTime(time_key);
13✔
326
        if (!time_frame) {
13✔
UNCOV
327
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
328
            return;
×
329
        }
330

331
        std::cout << "Time frame has " << time_frame->getTotalFrameCount() << " frames" << std::endl;
13✔
332

333
        // Add to plotting manager first
334
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
13✔
335

336
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
13✔
337
        std::cout << "Successfully added analog series to PlottingManager and OpenGL widget" << std::endl;
13✔
338

339
    } else if (data_type == DM_DataType::DigitalEvent) {
13✔
340

341
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
×
342
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
343
        if (!series) {
×
344
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
345
            return;
×
346
        }
347

UNCOV
348
        auto time_key = _data_manager->getTimeKey(key);
×
349
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
350
        if (!time_frame) {
×
351
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
352
            return;
×
353
        }
354

355
        // Add to plotting manager first
UNCOV
356
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
357

UNCOV
358
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
×
359

UNCOV
360
    } else if (data_type == DM_DataType::DigitalInterval) {
×
361

UNCOV
362
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
×
UNCOV
363
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
UNCOV
364
        if (!series) {
×
UNCOV
365
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
UNCOV
366
            return;
×
367
        }
368

UNCOV
369
        auto time_key = _data_manager->getTimeKey(key);
×
UNCOV
370
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
371
        if (!time_frame) {
×
UNCOV
372
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
373
            return;
×
374
        }
375

376
        // Add to plotting manager first
377
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
378

379
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
380

UNCOV
381
    } else {
×
382
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
383
        return;
×
384
    }
385

386
    // Apply coordinated plotting manager allocation after adding to OpenGL widget
387
    if (_plotting_manager) {
13✔
388
        _applyPlottingManagerAllocation(key);
13✔
389
    }
390

391
    // Auto-arrange and auto-fill canvas to make optimal use of space
392
    std::cout << "Auto-arranging and filling canvas after adding series" << std::endl;
13✔
393
    autoArrangeVerticalSpacing();// This now includes auto-fill functionality
13✔
394

395
    std::cout << "Series addition and auto-arrangement completed" << std::endl;
13✔
396
    // Trigger canvas update to show the new series
397
    std::cout << "Triggering canvas update" << std::endl;
13✔
398
    ui->openGLWidget->updateCanvas();
13✔
399
    std::cout << "Canvas update completed" << std::endl;
13✔
400
}
13✔
401

UNCOV
402
void DataViewer_Widget::_removeSelectedFeature(std::string const & key) {
×
UNCOV
403
    std::cout << "Attempting to remove feature: " << key << std::endl;
×
404

405
    if (key.empty()) {
×
406
        std::cerr << "Error: empty key in _removeSelectedFeature" << std::endl;
×
407
        return;
×
408
    }
409

UNCOV
410
    if (!_data_manager) {
×
411
        std::cerr << "Error: null data manager in _removeSelectedFeature" << std::endl;
×
412
        return;
×
413
    }
414

UNCOV
415
    auto data_type = _data_manager->getType(key);
×
416

417
    // Remove from plotting manager first
UNCOV
418
    if (_plotting_manager) {
×
419
        bool removed = false;
×
UNCOV
420
        if (data_type == DM_DataType::Analog) {
×
421
            removed = _plotting_manager->removeAnalogSeries(key);
×
422
        } else if (data_type == DM_DataType::DigitalEvent) {
×
UNCOV
423
            removed = _plotting_manager->removeDigitalEventSeries(key);
×
UNCOV
424
        } else if (data_type == DM_DataType::DigitalInterval) {
×
425
            removed = _plotting_manager->removeDigitalIntervalSeries(key);
×
426
        }
UNCOV
427
        if (removed) {
×
428
            std::cout << "Unregistered '" << key << "' from plotting manager" << std::endl;
×
429
        }
430
    }
431

UNCOV
432
    if (data_type == DM_DataType::Analog) {
×
433
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
434
    } else if (data_type == DM_DataType::DigitalEvent) {
×
435
        ui->openGLWidget->removeDigitalEventSeries(key);
×
UNCOV
436
    } else if (data_type == DM_DataType::DigitalInterval) {
×
UNCOV
437
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
438
    } else {
UNCOV
439
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
440
        return;
×
441
    }
442

443
    // Auto-arrange and auto-fill canvas to rescale remaining elements
444
    std::cout << "Auto-arranging and filling canvas after removing series" << std::endl;
×
UNCOV
445
    autoArrangeVerticalSpacing();// This now includes auto-fill functionality
×
446

447
    std::cout << "Series removal and auto-arrangement completed" << std::endl;
×
448
    // Trigger canvas update to reflect the removal
449
    std::cout << "Triggering canvas update after removal" << std::endl;
×
450
    ui->openGLWidget->updateCanvas();
×
451
}
452

UNCOV
453
void DataViewer_Widget::_handleFeatureSelected(QString const & feature) {
×
454
    std::cout << "Feature selected signal received: " << feature.toStdString() << std::endl;
×
455

UNCOV
456
    if (feature.isEmpty()) {
×
457
        std::cerr << "Error: empty feature name in _handleFeatureSelected" << std::endl;
×
458
        return;
×
459
    }
460

461
    if (!_data_manager) {
×
462
        std::cerr << "Error: null data manager in _handleFeatureSelected" << std::endl;
×
463
        return;
×
464
    }
465

UNCOV
466
    _highlighted_available_feature = feature;
×
467

468
    // Switch stacked widget based on data type
469
    auto const type = _data_manager->getType(feature.toStdString());
×
470
    auto key = feature.toStdString();
×
471

472
    std::cout << "Feature type for selection: " << convert_data_type_to_string(type) << std::endl;
×
473

474
    if (type == DM_DataType::Analog) {
×
UNCOV
475
        int const stacked_widget_index = 1;// Analog widget is at index 1 (after empty page)
×
476
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
477
        auto analog_widget = dynamic_cast<AnalogViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
478
        if (analog_widget) {
×
UNCOV
479
            analog_widget->setActiveKey(key);
×
UNCOV
480
            std::cout << "Selected Analog Time Series: " << key << std::endl;
×
481
        } else {
UNCOV
482
            std::cerr << "Error: failed to cast to AnalogViewer_Widget" << std::endl;
×
483
        }
484

UNCOV
485
    } else if (type == DM_DataType::DigitalInterval) {
×
UNCOV
486
        int const stacked_widget_index = 2;// Interval widget is at index 2
×
UNCOV
487
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
488
        auto interval_widget = dynamic_cast<IntervalViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
489
        if (interval_widget) {
×
UNCOV
490
            interval_widget->setActiveKey(key);
×
UNCOV
491
            std::cout << "Selected Digital Interval Series: " << key << std::endl;
×
492
        } else {
UNCOV
493
            std::cerr << "Error: failed to cast to IntervalViewer_Widget" << std::endl;
×
494
        }
495

UNCOV
496
    } else if (type == DM_DataType::DigitalEvent) {
×
UNCOV
497
        int const stacked_widget_index = 3;// Event widget is at index 3
×
UNCOV
498
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
499
        auto event_widget = dynamic_cast<EventViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
500
        if (event_widget) {
×
UNCOV
501
            event_widget->setActiveKey(key);
×
UNCOV
502
            std::cout << "Selected Digital Event Series: " << key << std::endl;
×
503
        } else {
UNCOV
504
            std::cerr << "Error: failed to cast to EventViewer_Widget" << std::endl;
×
505
        }
506

507
    } else {
508
        // No specific widget for this type, don't change the current index
UNCOV
509
        std::cout << "Unsupported feature type for detailed view: " << convert_data_type_to_string(type) << std::endl;
×
510
    }
UNCOV
511
}
×
512

513
void DataViewer_Widget::_handleXAxisSamplesChanged(int value) {
6✔
514
    // Use setRangeWidth for spinbox changes (absolute value)
515
    std::cout << "Spinbox requested range width: " << value << std::endl;
6✔
516
    int64_t const actual_range = ui->openGLWidget->setRangeWidth(static_cast<int64_t>(value));
6✔
517
    std::cout << "Actual range width achieved: " << actual_range << std::endl;
6✔
518

519
    // Update the spinbox with the actual range width achieved (in case it was clamped)
520
    if (actual_range != value) {
6✔
521
        std::cout << "Range was clamped, updating spinbox to: " << actual_range << std::endl;
6✔
522
        updateXAxisSamples(static_cast<int>(actual_range));
6✔
523
    }
524
}
6✔
525

526
void DataViewer_Widget::updateXAxisSamples(int value) {
6✔
527
    ui->x_axis_samples->blockSignals(true);
6✔
528
    ui->x_axis_samples->setValue(value);
6✔
529
    ui->x_axis_samples->blockSignals(false);
6✔
530
}
6✔
531

532
void DataViewer_Widget::_updateGlobalScale(double scale) {
14✔
533
    ui->openGLWidget->setGlobalScale(static_cast<float>(scale));
14✔
534

535
    // Also update PlottingManager zoom factor
536
    if (_plotting_manager) {
14✔
537
        _plotting_manager->setGlobalZoom(static_cast<float>(scale));
14✔
538

539
        // Apply updated positions to all registered series
540
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
14✔
541
        for (auto const & key: analog_keys) {
47✔
542
            _applyPlottingManagerAllocation(key);
33✔
543
        }
544
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
14✔
545
        for (auto const & key: event_keys) {
14✔
UNCOV
546
            _applyPlottingManagerAllocation(key);
×
547
        }
548
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
14✔
549
        for (auto const & key: interval_keys) {
14✔
UNCOV
550
            _applyPlottingManagerAllocation(key);
×
551
        }
552

553
        // Trigger canvas update
554
        ui->openGLWidget->updateCanvas();
14✔
555
    }
14✔
556
}
14✔
557

UNCOV
558
void DataViewer_Widget::wheelEvent(QWheelEvent * event) {
×
559
    // Disable zooming while dragging intervals
560
    if (ui->openGLWidget->isDraggingInterval()) {
×
UNCOV
561
        return;
×
562
    }
563

564
    auto const numDegrees = static_cast<float>(event->angleDelta().y()) / 8.0f;
×
UNCOV
565
    auto const numSteps = numDegrees / 15.0f;
×
566

567
    auto const current_range = ui->x_axis_samples->value();
×
568

UNCOV
569
    float rangeFactor;
×
UNCOV
570
    if (_zoom_scaling_mode == ZoomScalingMode::Adaptive) {
×
571
        // Adaptive scaling: range factor is proportional to current range width
572
        // This makes adjustments more sensitive when zoomed in (small range), less sensitive when zoomed out (large range)
UNCOV
573
        rangeFactor = static_cast<float>(current_range) * 0.1f;// 10% of current range width
×
574

575
        // Clamp range factor to reasonable bounds
UNCOV
576
        rangeFactor = std::max(1.0f, std::min(rangeFactor, static_cast<float>(_time_frame->getTotalFrameCount()) / 100.0f));
×
577
    } else {
578
        // Fixed scaling (original behavior)
UNCOV
579
        rangeFactor = static_cast<float>(_time_frame->getTotalFrameCount()) / 10000.0f;
×
580
    }
581

582
    // Calculate range delta
583
    // Wheel up (positive numSteps) should zoom IN (decrease range width)
584
    // Wheel down (negative numSteps) should zoom OUT (increase range width)
585
    auto const range_delta = static_cast<int64_t>(-numSteps * rangeFactor);
×
586

587
    // Apply range delta and get the actual achieved range
588
    ui->openGLWidget->changeRangeWidth(range_delta);
×
589

590
    // Get the actual range that was achieved (may be different due to clamping)
591
    auto x_axis = ui->openGLWidget->getXAxis();
×
UNCOV
592
    auto const actual_range = static_cast<int>(x_axis.getEnd() - x_axis.getStart());
×
593

594
    // Update spinbox with the actual achieved range (not the requested range)
595
    updateXAxisSamples(actual_range);
×
596
    _updateLabels();
×
597
}
598

599
void DataViewer_Widget::_updateLabels() {
14✔
600
    auto x_axis = ui->openGLWidget->getXAxis();
14✔
601
    ui->neg_x_label->setText(QString::number(x_axis.getStart()));
14✔
602
    ui->pos_x_label->setText(QString::number(x_axis.getEnd()));
14✔
603
}
14✔
604

605
void DataViewer_Widget::_handleColorChanged(std::string const & feature_key, std::string const & hex_color) {
×
606
    // Update the color in the OpenGL widget display options (tree widget color management will be added later)
607

UNCOV
608
    auto const type = _data_manager->getType(feature_key);
×
609

610
    if (type == DM_DataType::Analog) {
×
UNCOV
611
        auto config = ui->openGLWidget->getAnalogConfig(feature_key);
×
UNCOV
612
        if (config.has_value()) {
×
613
            config.value()->hex_color = hex_color;
×
614
        }
615

616
    } else if (type == DM_DataType::DigitalEvent) {
×
617
        auto config = ui->openGLWidget->getDigitalEventConfig(feature_key);
×
618
        if (config.has_value()) {
×
619
            config.value()->hex_color = hex_color;
×
620
        }
621

622
    } else if (type == DM_DataType::DigitalInterval) {
×
UNCOV
623
        auto config = ui->openGLWidget->getDigitalIntervalConfig(feature_key);
×
624
        if (config.has_value()) {
×
625
            config.value()->hex_color = hex_color;
×
626
        }
627
    }
628

629
    // Trigger a redraw
UNCOV
630
    ui->openGLWidget->updateCanvas();
×
631

632
    std::cout << "Color changed for " << feature_key << " to " << hex_color << std::endl;
×
633
}
×
634

UNCOV
635
void DataViewer_Widget::_updateCoordinateDisplay(float time_coordinate, float canvas_y, QString const & series_info) {
×
636
    // Convert time coordinate to actual time using the time frame
UNCOV
637
    int const time_index = static_cast<int>(std::round(time_coordinate));
×
UNCOV
638
    int const actual_time = _time_frame->getTimeAtIndex(TimeFrameIndex(time_index));
×
639

640
    // Get canvas size for debugging
UNCOV
641
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
642

643
    QString coordinate_text;
×
644
    if (series_info.isEmpty()) {
×
UNCOV
645
        coordinate_text = QString("Coordinates: Time: %1 (index: %2), Canvas Y: %3 | Canvas: %4x%5")
×
UNCOV
646
                                  .arg(actual_time)
×
647
                                  .arg(time_index)
×
648
                                  .arg(canvas_y, 0, 'f', 1)
×
649
                                  .arg(canvas_width)
×
UNCOV
650
                                  .arg(canvas_height);
×
651
    } else {
652
        coordinate_text = QString("Coordinates: Time: %1 (index: %2), %3 | Canvas: %4x%5")
×
653
                                  .arg(actual_time)
×
UNCOV
654
                                  .arg(time_index)
×
655
                                  .arg(series_info)
×
UNCOV
656
                                  .arg(canvas_width)
×
UNCOV
657
                                  .arg(canvas_height);
×
658
    }
659

UNCOV
660
    ui->coordinate_label->setText(coordinate_text);
×
661
}
×
662

663
std::optional<NewAnalogTimeSeriesDisplayOptions *> DataViewer_Widget::getAnalogConfig(std::string const & key) const {
38✔
664
    return ui->openGLWidget->getAnalogConfig(key);
38✔
665
}
666

667
std::optional<NewDigitalEventSeriesDisplayOptions *> DataViewer_Widget::getDigitalEventConfig(std::string const & key) const {
×
UNCOV
668
    return ui->openGLWidget->getDigitalEventConfig(key);
×
669
}
670

UNCOV
671
std::optional<NewDigitalIntervalSeriesDisplayOptions *> DataViewer_Widget::getDigitalIntervalConfig(std::string const & key) const {
×
UNCOV
672
    return ui->openGLWidget->getDigitalIntervalConfig(key);
×
673
}
674

UNCOV
675
void DataViewer_Widget::_handleThemeChanged(int theme_index) {
×
UNCOV
676
    PlotTheme theme = (theme_index == 0) ? PlotTheme::Dark : PlotTheme::Light;
×
UNCOV
677
    ui->openGLWidget->setPlotTheme(theme);
×
678

679
    // Update coordinate label styling based on theme
UNCOV
680
    if (theme == PlotTheme::Dark) {
×
UNCOV
681
        ui->coordinate_label->setStyleSheet("background-color: rgba(0, 0, 0, 50); color: white; padding: 2px;");
×
682
    } else {
UNCOV
683
        ui->coordinate_label->setStyleSheet("background-color: rgba(255, 255, 255, 50); color: black; padding: 2px;");
×
684
    }
685

UNCOV
686
    std::cout << "Theme changed to: " << (theme_index == 0 ? "Dark" : "Light") << std::endl;
×
UNCOV
687
}
×
688

689
void DataViewer_Widget::_handleGridLinesToggled(bool enabled) {
×
UNCOV
690
    ui->openGLWidget->setGridLinesEnabled(enabled);
×
UNCOV
691
}
×
692

UNCOV
693
void DataViewer_Widget::_handleGridSpacingChanged(int spacing) {
×
UNCOV
694
    ui->openGLWidget->setGridSpacing(spacing);
×
UNCOV
695
}
×
696

697
void DataViewer_Widget::_handleVerticalSpacingChanged(double spacing) {
13✔
698
    ui->openGLWidget->setVerticalSpacing(static_cast<float>(spacing));
13✔
699

700
    // Also update PlottingManager vertical scale
701
    if (_plotting_manager) {
13✔
702
        // Convert spacing to a scale factor relative to default (0.1f)
703
        float const scale_factor = static_cast<float>(spacing) / 0.1f;
13✔
704
        _plotting_manager->setGlobalVerticalScale(scale_factor);
13✔
705

706
        // Apply updated positions to all registered series
707
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
13✔
708
        for (auto const & key: analog_keys) {
45✔
709
            _applyPlottingManagerAllocation(key);
32✔
710
        }
711
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
13✔
712
        for (auto const & key: event_keys) {
13✔
UNCOV
713
            _applyPlottingManagerAllocation(key);
×
714
        }
715
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
13✔
716
        for (auto const & key: interval_keys) {
13✔
717
            _applyPlottingManagerAllocation(key);
×
718
        }
719

720
        // Trigger canvas update
721
        ui->openGLWidget->updateCanvas();
13✔
722
    }
13✔
723
}
13✔
724

725
void DataViewer_Widget::_plotSelectedFeatureWithoutUpdate(std::string const & key) {
×
726
    std::cout << "Attempting to plot feature (batch): " << key << std::endl;
×
727

728
    if (key.empty()) {
×
UNCOV
729
        std::cerr << "Error: empty key in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
730
        return;
×
731
    }
732

733
    if (!_data_manager) {
×
734
        std::cerr << "Error: null data manager in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
735
        return;
×
736
    }
737

738
    // Get color from model
UNCOV
739
    std::string color = _feature_tree_model->getFeatureColor(key);
×
740
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
×
741

742
    auto data_type = _data_manager->getType(key);
×
743
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
×
744

UNCOV
745
    if (data_type == DM_DataType::Analog) {
×
UNCOV
746
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
×
747
        if (!series) {
×
UNCOV
748
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
749
            return;
×
750
        }
751

752
        auto time_key = _data_manager->getTimeKey(key);
×
753
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
754
        if (!time_frame) {
×
UNCOV
755
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
756
            return;
×
757
        }
758

759
        // Register with plotting manager for later allocation
760
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
×
UNCOV
761
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
×
UNCOV
762
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
×
763

UNCOV
764
    } else if (data_type == DM_DataType::DigitalEvent) {
×
765
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
766
        if (!series) {
×
767
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
UNCOV
768
            return;
×
769
        }
770

771
        auto time_key = _data_manager->getTimeKey(key);
×
772
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
773
        if (!time_frame) {
×
774
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
775
            return;
×
776
        }
777
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
778
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
779
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
×
780

UNCOV
781
    } else if (data_type == DM_DataType::DigitalInterval) {
×
782
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
783
        if (!series) {
×
784
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
UNCOV
785
            return;
×
786
        }
787

UNCOV
788
        auto time_key = _data_manager->getTimeKey(key);
×
789
        auto time_frame = _data_manager->getTime(time_key);
×
790
        if (!time_frame) {
×
791
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
792
            return;
×
793
        }
794
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
UNCOV
795
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
796
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
797

UNCOV
798
    } else {
×
UNCOV
799
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
800
        return;
×
801
    }
802

803
    // Note: No canvas update triggered - this is for batch operations
804
    std::cout << "Successfully added series to OpenGL widget (batch mode)" << std::endl;
×
805
}
×
806

UNCOV
807
void DataViewer_Widget::_removeSelectedFeatureWithoutUpdate(std::string const & key) {
×
UNCOV
808
    std::cout << "Attempting to remove feature (batch): " << key << std::endl;
×
809

UNCOV
810
    if (key.empty()) {
×
UNCOV
811
        std::cerr << "Error: empty key in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
812
        return;
×
813
    }
814

UNCOV
815
    if (!_data_manager) {
×
816
        std::cerr << "Error: null data manager in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
817
        return;
×
818
    }
819

820
    auto data_type = _data_manager->getType(key);
×
821

UNCOV
822
    if (data_type == DM_DataType::Analog) {
×
823
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
824
    } else if (data_type == DM_DataType::DigitalEvent) {
×
UNCOV
825
        ui->openGLWidget->removeDigitalEventSeries(key);
×
826
    } else if (data_type == DM_DataType::DigitalInterval) {
×
827
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
828
    } else {
UNCOV
829
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
830
        return;
×
831
    }
832

833
    // Note: No canvas update triggered - this is for batch operations
834
    std::cout << "Successfully removed series from OpenGL widget (batch mode)" << std::endl;
×
835
}
836

837
void DataViewer_Widget::_calculateOptimalScaling(std::vector<std::string> const & group_keys) {
×
UNCOV
838
    if (group_keys.empty()) {
×
UNCOV
839
        return;
×
840
    }
841

842
    std::cout << "Calculating optimal scaling for " << group_keys.size() << " analog channels..." << std::endl;
×
843

844
    // Get current canvas dimensions
UNCOV
845
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
UNCOV
846
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
847

848
    // Count total number of currently visible analog series (including the new group)
UNCOV
849
    int total_visible_analog_series = static_cast<int>(group_keys.size());
×
850

851
    // Add any other already visible analog series
852
    auto all_keys = _data_manager->getAllKeys();
×
UNCOV
853
    for (auto const & key: all_keys) {
×
854
        if (_data_manager->getType(key) == DM_DataType::Analog) {
×
855
            // Check if this key is already in our group (avoid double counting)
UNCOV
856
            bool in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
UNCOV
857
            if (!in_group) {
×
858
                // Check if this series is currently visible
859
                auto config = ui->openGLWidget->getAnalogConfig(key);
×
UNCOV
860
                if (config.has_value() && config.value()->is_visible) {
×
861
                    total_visible_analog_series++;
×
862
                }
863
            }
864
        }
865
    }
866

UNCOV
867
    std::cout << "Total visible analog series (including new group): " << total_visible_analog_series << std::endl;
×
868

UNCOV
869
    if (total_visible_analog_series <= 0) {
×
870
        return;// No series to scale
×
871
    }
872

873
    // Calculate optimal vertical spacing
874
    // Leave some margin at top and bottom (10% each = 20% total)
UNCOV
875
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
UNCOV
876
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_analog_series);
×
877

878
    // Convert to normalized coordinates (OpenGL widget uses normalized spacing)
879
    // Assuming the widget's view height is typically around 2.0 units in normalized coordinates
UNCOV
880
    float const normalized_spacing = (optimal_spacing / static_cast<float>(canvas_height)) * 2.0f;
×
881

882
    // Clamp to reasonable bounds
UNCOV
883
    float const min_spacing = 0.01f;
×
UNCOV
884
    float const max_spacing = 1.0f;
×
885
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
886

UNCOV
887
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
888
              << final_spacing << " normalized units" << std::endl;
×
889

890
    // Calculate optimal global gain based on standard deviations
UNCOV
891
    std::vector<float> std_devs;
×
892
    std_devs.reserve(group_keys.size());
×
893

894
    for (auto const & key: group_keys) {
×
UNCOV
895
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
×
UNCOV
896
        if (series) {
×
897
            float const std_dev = calculate_std_dev_approximate(*series);
×
898
            std_devs.push_back(std_dev);
×
UNCOV
899
            std::cout << "Series " << key << " std dev: " << std_dev << std::endl;
×
900
        }
901
    }
×
902

UNCOV
903
    if (!std_devs.empty()) {
×
904
        // Use the median standard deviation as reference for scaling
905
        std::sort(std_devs.begin(), std_devs.end());
×
906
        float const median_std_dev = std_devs[std_devs.size() / 2];
×
907

908
        // Calculate optimal global scale
909
        // Target: each series should use about 60% of its allocated vertical space
910
        float const target_amplitude_fraction = 0.6f;
×
911
        float const target_amplitude_in_pixels = optimal_spacing * target_amplitude_fraction;
×
912

913
        // Convert to normalized coordinates (3 standard deviations should fit in target amplitude)
UNCOV
914
        float const target_amplitude_normalized = (target_amplitude_in_pixels / static_cast<float>(canvas_height)) * 2.0f;
×
915
        float const three_sigma_target = target_amplitude_normalized;
×
916

917
        // Calculate scale factor needed
918
        float const optimal_global_scale = three_sigma_target / (3.0f * median_std_dev);
×
919

920
        // Clamp to reasonable bounds
UNCOV
921
        float const min_scale = 0.1f;
×
922
        float const max_scale = 100.0f;
×
UNCOV
923
        float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
×
924

925
        std::cout << "Median std dev: " << median_std_dev
×
926
                  << ", target amplitude: " << target_amplitude_in_pixels << " pixels"
×
927
                  << ", optimal global scale: " << final_scale << std::endl;
×
928

929
        // Apply the calculated settings
930
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
UNCOV
931
        ui->global_zoom->setValue(static_cast<double>(final_scale));
×
932

933
        std::cout << "Applied auto-scaling: vertical spacing = " << final_spacing
×
934
                  << ", global scale = " << final_scale << std::endl;
×
935

936
    } else {
937
        // If we can't calculate standard deviations, just apply spacing
UNCOV
938
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
UNCOV
939
        std::cout << "Applied auto-spacing only: vertical spacing = " << final_spacing << std::endl;
×
940
    }
UNCOV
941
}
×
942

943
void DataViewer_Widget::_calculateOptimalEventSpacing(std::vector<std::string> const & group_keys) {
×
UNCOV
944
    if (group_keys.empty()) {
×
UNCOV
945
        return;
×
946
    }
947

948
    std::cout << "Calculating optimal event spacing for " << group_keys.size() << " digital event series..." << std::endl;
×
949

950
    // Get current canvas dimensions
UNCOV
951
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
UNCOV
952
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
953

954
    // Count total number of currently visible digital event series (including the new group)
UNCOV
955
    int total_visible_event_series = static_cast<int>(group_keys.size());
×
956

957
    // Add any other already visible digital event series
958
    auto all_keys = _data_manager->getAllKeys();
×
UNCOV
959
    for (auto const & key: all_keys) {
×
UNCOV
960
        if (_data_manager->getType(key) == DM_DataType::DigitalEvent) {
×
961
            // Check if this key is already in our group (avoid double counting)
962
            bool const in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
963
            if (!in_group) {
×
964
                // Check if this series is currently visible
UNCOV
965
                auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
966
                if (config.has_value() && config.value()->is_visible) {
×
967
                    total_visible_event_series++;
×
968
                }
969
            }
970
        }
971
    }
972

973
    std::cout << "Total visible digital event series (including new group): " << total_visible_event_series << std::endl;
×
974

975
    if (total_visible_event_series <= 0) {
×
976
        return;// No series to scale
×
977
    }
978

979
    // Calculate optimal vertical spacing
980
    // Leave some margin at top and bottom (10% each = 20% total)
981
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
982
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_event_series);
×
983

984
    // Convert to normalized coordinates (OpenGL widget uses normalized spacing)
985
    // Assuming the widget's view height is typically around 2.0 units in normalized coordinates
UNCOV
986
    float const normalized_spacing = (optimal_spacing / static_cast<float>(canvas_height)) * 2.0f;
×
987

988
    // Clamp to reasonable bounds
UNCOV
989
    float const min_spacing = 0.01f;
×
UNCOV
990
    float const max_spacing = 1.0f;
×
UNCOV
991
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
992

993
    // Calculate optimal event height (should be smaller than spacing to avoid overlap)
UNCOV
994
    float const optimal_event_height = final_spacing * 0.6f;// 60% of spacing for visible separation
×
UNCOV
995
    float const min_height = 0.01f;
×
UNCOV
996
    float const max_height = 0.5f;
×
UNCOV
997
    float const final_height = std::clamp(optimal_event_height, min_height, max_height);
×
998

999
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
UNCOV
1000
              << final_spacing << " normalized units" << std::endl;
×
UNCOV
1001
    std::cout << "Calculated event height: " << final_height << " normalized units" << std::endl;
×
1002

1003
    // Apply the calculated settings to all event series in the group
UNCOV
1004
    for (auto const & key: group_keys) {
×
UNCOV
1005
        auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
UNCOV
1006
        if (config.has_value()) {
×
UNCOV
1007
            config.value()->vertical_spacing = final_spacing;
×
UNCOV
1008
            config.value()->event_height = final_height;
×
UNCOV
1009
            config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
×
1010
        }
1011
    }
1012

UNCOV
1013
    std::cout << "Applied auto-calculated event spacing: spacing = " << final_spacing
×
UNCOV
1014
              << ", height = " << final_height << std::endl;
×
UNCOV
1015
}
×
1016

1017
void DataViewer_Widget::autoArrangeVerticalSpacing() {
13✔
1018
    std::cout << "DataViewer_Widget: Auto-arranging with plotting manager..." << std::endl;
13✔
1019

1020
    // Update dimensions first
1021
    _updatePlottingManagerDimensions();
13✔
1022

1023
    // Apply new allocations to all registered series
1024
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
13✔
1025
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
13✔
1026
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
13✔
1027

1028
    for (auto const & key: analog_keys) {
45✔
1029
        _applyPlottingManagerAllocation(key);
32✔
1030
    }
1031
    for (auto const & key: event_keys) {
13✔
1032
        _applyPlottingManagerAllocation(key);
×
1033
    }
1034
    for (auto const & key: interval_keys) {
13✔
1035
        _applyPlottingManagerAllocation(key);
×
1036
    }
1037

1038
    // Calculate and apply optimal scaling to fill the canvas
1039
    _autoFillCanvas();
13✔
1040

1041
    // Update OpenGL widget view bounds based on content height
1042
    _updateViewBounds();
13✔
1043

1044
    // Trigger canvas update to show new positions
1045
    ui->openGLWidget->updateCanvas();
13✔
1046

1047
    auto total_keys = analog_keys.size() + event_keys.size() + interval_keys.size();
13✔
1048
    std::cout << "DataViewer_Widget: Auto-arrange completed for " << total_keys << " series" << std::endl;
13✔
1049
}
26✔
1050

1051
void DataViewer_Widget::_updateViewBounds() {
13✔
1052
    if (!_plotting_manager) {
13✔
UNCOV
1053
        return;
×
1054
    }
1055

1056
    // PlottingManager uses normalized coordinates, so view bounds are typically -1 to +1
1057
    // For now, use standard bounds but this enables future enhancement
1058
    std::cout << "DataViewer_Widget: Using standard view bounds with PlottingManager" << std::endl;
13✔
1059
}
1060

UNCOV
1061
std::string DataViewer_Widget::_convertDataType(DM_DataType dm_type) const {
×
1062
    switch (dm_type) {
×
UNCOV
1063
        case DM_DataType::Analog:
×
UNCOV
1064
            return "Analog";
×
UNCOV
1065
        case DM_DataType::DigitalEvent:
×
UNCOV
1066
            return "DigitalEvent";
×
UNCOV
1067
        case DM_DataType::DigitalInterval:
×
UNCOV
1068
            return "DigitalInterval";
×
UNCOV
1069
        default:
×
1070
            // For unsupported types, default to Analog
1071
            // This should be rare in practice given our type filters
UNCOV
1072
            std::cerr << "Warning: Unsupported data type " << convert_data_type_to_string(dm_type)
×
UNCOV
1073
                      << " defaulting to Analog for plotting manager" << std::endl;
×
UNCOV
1074
            return "Analog";
×
1075
    }
1076
}
1077

1078
void DataViewer_Widget::_updatePlottingManagerDimensions() {
26✔
1079
    if (!_plotting_manager) {
26✔
1080
        return;
×
1081
    }
1082

1083
    // Get current canvas dimensions from OpenGL widget
1084
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
26✔
1085

1086
    // PlottingManager works in normalized device coordinates, so no specific dimension update needed
1087
    // But we could update viewport bounds if needed in the future
1088

1089
    std::cout << "DataViewer_Widget: Updated plotting manager dimensions: "
26✔
1090
              << canvas_width << "x" << canvas_height << " pixels" << std::endl;
26✔
1091
}
1092

1093
void DataViewer_Widget::_applyPlottingManagerAllocation(std::string const & series_key) {
114✔
1094
    if (!_plotting_manager) {
114✔
UNCOV
1095
        return;
×
1096
    }
1097

1098
    auto data_type = _data_manager->getType(series_key);
114✔
1099

1100
    std::cout << "DataViewer_Widget: Applying plotting manager allocation for '" << series_key << "'" << std::endl;
114✔
1101

1102
    // For now, use a basic implementation that will be enhanced when we update OpenGLWidget
1103
    // The main goal is to get the compilation working first
1104

1105
    // Apply positioning based on data type
1106
    if (data_type == DM_DataType::Analog) {
114✔
1107
        auto config = ui->openGLWidget->getAnalogConfig(series_key);
114✔
1108
        if (config.has_value()) {
114✔
1109
            float yc, yh;
114✔
1110
            if (_plotting_manager->getAnalogSeriesAllocationForKey(series_key, yc, yh)) {
114✔
1111
                config.value()->allocated_y_center = yc;
114✔
1112
                config.value()->allocated_height = yh;
114✔
1113
            }
1114
        }
1115

UNCOV
1116
    } else if (data_type == DM_DataType::DigitalEvent) {
×
UNCOV
1117
        auto config = ui->openGLWidget->getDigitalEventConfig(series_key);
×
UNCOV
1118
        if (config.has_value()) {
×
1119
            // Basic allocation - will be properly implemented when OpenGL widget is updated
UNCOV
1120
            std::cout << "  Applied basic allocation to event '" << series_key << "'" << std::endl;
×
1121
        }
1122

1123
    } else if (data_type == DM_DataType::DigitalInterval) {
×
1124
        auto config = ui->openGLWidget->getDigitalIntervalConfig(series_key);
×
UNCOV
1125
        if (config.has_value()) {
×
1126
            // Basic allocation - will be properly implemented when OpenGL widget is updated
UNCOV
1127
            std::cout << "  Applied basic allocation to interval '" << series_key << "'" << std::endl;
×
1128
        }
1129
    }
1130
}
1131

1132
// ===== Context menu and configuration handling =====
UNCOV
1133
void DataViewer_Widget::_showGroupContextMenu(std::string const & group_name, QPoint const & global_pos) {
×
UNCOV
1134
    QMenu menu;
×
UNCOV
1135
    QMenu * loadMenu = menu.addMenu("Load configuration");
×
UNCOV
1136
    QAction * loadSpikeSorter = loadMenu->addAction("spikesorter configuration");
×
UNCOV
1137
    QAction * clearConfig = menu.addAction("Clear configuration");
×
1138

UNCOV
1139
    connect(loadSpikeSorter, &QAction::triggered, this, [this, group_name]() {
×
UNCOV
1140
        _loadSpikeSorterConfigurationForGroup(QString::fromStdString(group_name));
×
UNCOV
1141
    });
×
UNCOV
1142
    connect(clearConfig, &QAction::triggered, this, [this, group_name]() {
×
UNCOV
1143
        _clearConfigurationForGroup(QString::fromStdString(group_name));
×
UNCOV
1144
    });
×
1145

UNCOV
1146
    menu.exec(global_pos);
×
UNCOV
1147
}
×
1148

UNCOV
1149
void DataViewer_Widget::_loadSpikeSorterConfigurationForGroup(QString const & group_name) {
×
1150
    // For now, use a test constant string or file dialog; here we open a file dialog
UNCOV
1151
    QString path = QFileDialog::getOpenFileName(this, QString("Load spikesorter configuration for %1").arg(group_name), QString(), "Text Files (*.txt *.cfg *.conf);;All Files (*)");
×
1152
    if (path.isEmpty()) return;
×
UNCOV
1153
    QFile file(path);
×
UNCOV
1154
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return;
×
1155
    QByteArray data = file.readAll();
×
1156
    auto positions = _parseSpikeSorterConfig(data.toStdString());
×
1157
    if (positions.empty()) return;
×
1158
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
×
1159

1160
    // Re-apply allocation to visible analog keys and update
1161
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
UNCOV
1162
    for (auto const & key : analog_keys) {
×
UNCOV
1163
        _applyPlottingManagerAllocation(key);
×
1164
    }
UNCOV
1165
    ui->openGLWidget->updateCanvas();
×
UNCOV
1166
}
×
1167

UNCOV
1168
void DataViewer_Widget::_clearConfigurationForGroup(QString const & group_name) {
×
UNCOV
1169
    _plotting_manager->clearAnalogGroupConfiguration(group_name.toStdString());
×
1170
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
UNCOV
1171
    for (auto const & key : analog_keys) {
×
1172
        _applyPlottingManagerAllocation(key);
×
1173
    }
UNCOV
1174
    ui->openGLWidget->updateCanvas();
×
1175
}
×
1176

1177
std::vector<PlottingManager::AnalogGroupChannelPosition> DataViewer_Widget::_parseSpikeSorterConfig(std::string const & text) {
1✔
1178
    std::vector<PlottingManager::AnalogGroupChannelPosition> out;
1✔
1179
    std::istringstream ss(text);
1✔
1180
    std::string line;
1✔
1181
    bool first = true;
1✔
1182
    while (std::getline(ss, line)) {
6✔
1183
        if (line.empty()) continue;
5✔
1184
        if (first) { first = false; continue; } // skip header row (electrode name)
5✔
1185
        std::istringstream ls(line);
4✔
1186
        int row = 0; int ch = 0; float x = 0.0f; float y = 0.0f;
4✔
1187
        if (!(ls >> row >> ch >> x >> y)) continue;
4✔
1188
        // SpikeSorter is 1-based; convert to 0-based for our program
1189
        if (ch > 0) ch -= 1;
4✔
1190
        PlottingManager::AnalogGroupChannelPosition p; p.channel_id = ch; p.x = x; p.y = y;
4✔
1191
        out.push_back(p);
4✔
1192
    }
4✔
1193
    return out;
2✔
1194
}
1✔
1195

1196
void DataViewer_Widget::_loadSpikeSorterConfigurationFromText(QString const & group_name, QString const & text) {
1✔
1197
    auto positions = _parseSpikeSorterConfig(text.toStdString());
1✔
1198
    if (positions.empty()) {
1✔
UNCOV
1199
        std::cout << "No positions found in spike sorter configuration" << std::endl;
×
UNCOV
1200
        return;
×
1201
    }
1202
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
1✔
1203
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
1✔
1204
    for (auto const & key : analog_keys) {
5✔
1205
        _applyPlottingManagerAllocation(key);
4✔
1206
    }
1207
    ui->openGLWidget->updateCanvas();
1✔
1208
}
1✔
1209

1210
void DataViewer_Widget::_autoFillCanvas() {
13✔
1211
    std::cout << "DataViewer_Widget: Auto-filling canvas with PlottingManager..." << std::endl;
13✔
1212

1213
    if (!_plotting_manager) {
13✔
UNCOV
1214
        std::cout << "No plotting manager available" << std::endl;
×
UNCOV
1215
        return;
×
1216
    }
1217

1218
    // Get current canvas dimensions
1219
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
13✔
1220
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
13✔
1221

1222
    // Count visible series using PlottingManager
1223
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
13✔
1224
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
13✔
1225
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
13✔
1226

1227
    int visible_analog_count = static_cast<int>(analog_keys.size());
13✔
1228
    int visible_event_count = static_cast<int>(event_keys.size());
13✔
1229
    int visible_interval_count = static_cast<int>(interval_keys.size());
13✔
1230

1231
    int total_visible = visible_analog_count + visible_event_count + visible_interval_count;
13✔
1232
    std::cout << "Visible series: " << visible_analog_count << " analog, "
13✔
1233
              << visible_event_count << " events, " << visible_interval_count
13✔
1234
              << " intervals (total: " << total_visible << ")" << std::endl;
13✔
1235

1236
    if (total_visible == 0) {
13✔
UNCOV
1237
        std::cout << "No visible series to auto-scale" << std::endl;
×
UNCOV
1238
        return;
×
1239
    }
1240

1241
    // Calculate optimal vertical spacing to fill canvas
1242
    // Use 90% of canvas height, leaving 5% margin at top and bottom
1243
    float const usable_height = static_cast<float>(canvas_height) * 0.9f;
13✔
1244
    float const optimal_spacing_pixels = usable_height / static_cast<float>(total_visible);
13✔
1245

1246
    // Convert to normalized coordinates (assuming 2.0 total normalized height)
1247
    float const optimal_spacing_normalized = (optimal_spacing_pixels / static_cast<float>(canvas_height)) * 2.0f;
13✔
1248

1249
    // Clamp to reasonable bounds
1250
    float const min_spacing = 0.02f;
13✔
1251
    float const max_spacing = 1.5f;
13✔
1252
    float const final_spacing = std::clamp(optimal_spacing_normalized, min_spacing, max_spacing);
13✔
1253

1254
    std::cout << "Calculated optimal spacing: " << optimal_spacing_pixels << " pixels -> "
13✔
1255
              << final_spacing << " normalized units" << std::endl;
13✔
1256

1257
    // Apply the calculated vertical spacing
1258
    ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
13✔
1259

1260
    // Calculate and apply optimal event heights for digital event series
1261
    if (visible_event_count > 0) {
13✔
1262
        // Calculate optimal event height to fill most of the allocated space
1263
        // Use 80% of the spacing to leave some visual separation between events
UNCOV
1264
        float const optimal_event_height = final_spacing * 0.8f;
×
1265

UNCOV
1266
        std::cout << "Calculated optimal event height: " << optimal_event_height << " normalized units" << std::endl;
×
1267

1268
        // Apply optimal height to all visible digital event series
UNCOV
1269
        for (auto const & key: event_keys) {
×
UNCOV
1270
            auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
UNCOV
1271
            if (config.has_value() && config.value()->is_visible) {
×
UNCOV
1272
                config.value()->event_height = optimal_event_height;
×
1273
                config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
×
1274
                std::cout << "  Applied event height " << optimal_event_height
×
UNCOV
1275
                          << " to series '" << key << "'" << std::endl;
×
1276
            }
1277
        }
1278
    }
1279

1280
    // Calculate and apply optimal interval heights for digital interval series
1281
    if (visible_interval_count > 0) {
13✔
1282
        // Calculate optimal interval height to fill most of the allocated space
1283
        // Use 80% of the spacing to leave some visual separation between intervals
1284
        float const optimal_interval_height = final_spacing * 0.8f;
×
1285

UNCOV
1286
        std::cout << "Calculated optimal interval height: " << optimal_interval_height << " normalized units" << std::endl;
×
1287

1288
        // Apply optimal height to all visible digital interval series
1289
        for (auto const & key: interval_keys) {
×
1290
            auto config = ui->openGLWidget->getDigitalIntervalConfig(key);
×
1291
            if (config.has_value() && config.value()->is_visible) {
×
1292
                config.value()->interval_height = optimal_interval_height;
×
1293
                std::cout << "  Applied interval height " << optimal_interval_height
×
UNCOV
1294
                          << " to series '" << key << "'" << std::endl;
×
1295
            }
1296
        }
1297
    }
1298

1299
    // Calculate optimal global scale for analog series to use their allocated space effectively
1300
    if (visible_analog_count > 0) {
13✔
1301
        // Sample a few analog series to estimate appropriate scaling
1302
        std::vector<float> sample_std_devs;
13✔
1303
        sample_std_devs.reserve(std::min(5, visible_analog_count));// Sample up to 5 series
13✔
1304

1305
        int sampled = 0;
13✔
1306
        for (auto const & key: analog_keys) {
45✔
1307
            if (sampled >= 5) break;
32✔
1308

1309
            auto config = ui->openGLWidget->getAnalogConfig(key);
32✔
1310
            if (config.has_value() && config.value()->is_visible) {
32✔
1311
                auto series = _data_manager->getData<AnalogTimeSeries>(key);
32✔
1312
                if (series) {
32✔
1313
                    float std_dev = calculate_std_dev_approximate(*series);
32✔
1314
                    if (std_dev > 0.0f) {
32✔
1315
                        sample_std_devs.push_back(std_dev);
32✔
1316
                        sampled++;
32✔
1317
                    }
1318
                }
1319
            }
32✔
1320
        }
1321

1322
        if (!sample_std_devs.empty()) {
13✔
1323
            // Use median standard deviation for scaling calculation
1324
            std::sort(sample_std_devs.begin(), sample_std_devs.end());
13✔
1325
            float median_std_dev = sample_std_devs[sample_std_devs.size() / 2];
13✔
1326

1327
            // Calculate scale so that ±3 standard deviations use ~60% of allocated space
1328
            float const target_amplitude_fraction = 0.6f;
13✔
1329
            float const target_amplitude_pixels = optimal_spacing_pixels * target_amplitude_fraction;
13✔
1330
            float const target_amplitude_normalized = (target_amplitude_pixels / static_cast<float>(canvas_height)) * 2.0f;
13✔
1331

1332
            // For ±3σ coverage
1333
            float const three_sigma_coverage = target_amplitude_normalized;
13✔
1334
            float const optimal_global_scale = three_sigma_coverage / (6.0f * median_std_dev);
13✔
1335

1336
            // Clamp to reasonable bounds
1337
            float const min_scale = 0.01f;
13✔
1338
            float const max_scale = 100.0f;
13✔
1339
            float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
13✔
1340

1341
            std::cout << "Calculated optimal global scale: median_std_dev=" << median_std_dev
13✔
1342
                      << ", target_amplitude=" << target_amplitude_pixels << " pixels"
13✔
1343
                      << ", final_scale=" << final_scale << std::endl;
13✔
1344

1345
            // Apply the calculated global scale
1346
            ui->global_zoom->setValue(static_cast<double>(final_scale));
13✔
1347
        }
1348
    }
13✔
1349

1350
    std::cout << "Auto-fill canvas completed" << std::endl;
13✔
1351
}
13✔
1352

1353
void DataViewer_Widget::cleanupDeletedData() {
15✔
1354
    if (!_data_manager) {
15✔
UNCOV
1355
        return;
×
1356
    }
1357

1358
    // Collect keys that no longer exist in DataManager
1359
    std::vector<std::string> keys_to_cleanup;
15✔
1360

1361
    if (_plotting_manager) {
15✔
1362
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
15✔
1363
        for (auto const & key: analog_keys) {
18✔
1364
            if (!_data_manager->getData<AnalogTimeSeries>(key)) {
3✔
UNCOV
1365
                keys_to_cleanup.push_back(key);
×
1366
            }
1367
        }
1368
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
15✔
1369
        for (auto const & key: event_keys) {
15✔
UNCOV
1370
            if (!_data_manager->getData<DigitalEventSeries>(key)) {
×
UNCOV
1371
                keys_to_cleanup.push_back(key);
×
1372
            }
1373
        }
1374
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
15✔
1375
        for (auto const & key: interval_keys) {
15✔
UNCOV
1376
            if (!_data_manager->getData<DigitalIntervalSeries>(key)) {
×
UNCOV
1377
                keys_to_cleanup.push_back(key);
×
1378
            }
1379
        }
1380
    }
15✔
1381

1382
    if (keys_to_cleanup.empty()) {
15✔
1383
        return;
15✔
1384
    }
1385

1386
    // De-duplicate keys in case the same key appears in multiple lists
UNCOV
1387
    std::sort(keys_to_cleanup.begin(), keys_to_cleanup.end());
×
UNCOV
1388
    keys_to_cleanup.erase(std::unique(keys_to_cleanup.begin(), keys_to_cleanup.end()), keys_to_cleanup.end());
×
1389

1390
    // Post cleanup to OpenGLWidget's thread safely
UNCOV
1391
    QPointer<OpenGLWidget> glw = ui ? ui->openGLWidget : nullptr;
×
UNCOV
1392
    if (glw) {
×
UNCOV
1393
        QMetaObject::invokeMethod(glw, [glw, keys = keys_to_cleanup]() {
×
UNCOV
1394
            if (!glw) return;
×
UNCOV
1395
            for (auto const & key : keys) {
×
UNCOV
1396
                glw->removeAnalogTimeSeries(key);
×
UNCOV
1397
                glw->removeDigitalEventSeries(key);
×
UNCOV
1398
                glw->removeDigitalIntervalSeries(key);
×
1399
            } }, Qt::QueuedConnection);
1400
    }
1401

1402
    // Remove from PlottingManager defensively (all types) on our thread
UNCOV
1403
    if (_plotting_manager) {
×
UNCOV
1404
        for (auto const & key: keys_to_cleanup) {
×
UNCOV
1405
            (void) _plotting_manager->removeAnalogSeries(key);
×
UNCOV
1406
            (void) _plotting_manager->removeDigitalEventSeries(key);
×
UNCOV
1407
            (void) _plotting_manager->removeDigitalIntervalSeries(key);
×
1408
        }
1409
    }
1410

1411
    // Re-arrange remaining data
UNCOV
1412
    autoArrangeVerticalSpacing();
×
1413
}
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc