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

paulmthompson / WhiskerToolbox / 18685379784

21 Oct 2025 01:25PM UTC coverage: 72.522% (+0.1%) from 72.391%
18685379784

push

github

paulmthompson
fix failing tests

18 of 40 new or added lines in 1 file covered. (45.0%)

1765 existing lines in 32 files now uncovered.

53998 of 74457 relevant lines covered (72.52%)

46177.73 hits per line

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

44.21
/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,
20✔
40
                                     TimeScrollBar * time_scrollbar,
41
                                     MainWindow * main_window,
42
                                     QWidget * parent)
20✔
43
    : QWidget(parent),
44
      _data_manager{std::move(data_manager)},
20✔
45
      _time_scrollbar{time_scrollbar},
20✔
46
      _main_window{main_window},
20✔
47
      ui(new Ui::DataViewer_Widget) {
40✔
48

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

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

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

57
    // Initialize feature tree model
58
    _feature_tree_model = std::make_unique<Feature_Tree_Model>(this);
20✔
59
    _feature_tree_model->setDataManager(_data_manager);
20✔
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]() {
20✔
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});
60✔
72
    ui->feature_tree_widget->setDataManager(_data_manager);
20✔
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) {
20✔
76
        _handleFeatureSelected(QString::fromStdString(feature));
×
77
    });
×
78

79
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeature, this, [this](std::string const & feature) {
20✔
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);
20✔
86
    connect(ui->feature_tree_widget->treeWidget(), &QTreeWidget::customContextMenuRequested, this, [this](QPoint const & pos) {
20✔
87
        auto * tw = ui->feature_tree_widget->treeWidget();
×
88
        QTreeWidgetItem * item = tw->itemAt(pos);
×
89
        if (!item) return;
×
90
        std::string const key = item->text(0).toStdString();
×
91
        // Group items have children and are under Analog data type parent
92
        bool const hasChildren = item->childCount() > 0;
×
93
        if (hasChildren) {
×
94
            // Determine if parent is Analog data type or children are analog keys
95
            bool isAnalogGroup = false;
×
96
            if (auto * parent = item->parent()) {
×
97
                if (parent->text(0) == QString::fromStdString(convert_data_type_to_string(DM_DataType::Analog))) {
×
98
                    isAnalogGroup = true;
×
99
                }
100
            }
101
            if (isAnalogGroup) {
×
102
                QPoint const global_pos = tw->viewport()->mapToGlobal(pos);
×
103
                _showGroupContextMenu(key, global_pos);
×
104
            }
105
        }
106
    });
×
107

108
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::removeFeature, this, [this](std::string const & feature) {
20✔
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) {
20✔
114
        std::cout << "Adding " << features.size() << " features as group" << std::endl;
1✔
115

116
        // Mark batch add to suppress per-series auto-arrange
117
        _is_batch_add = true;
1✔
118
        // Process all features in the group without triggering individual canvas updates
119
        for (auto const & key: features) {
6✔
120
            _plotSelectedFeatureWithoutUpdate(key);
5✔
121
        }
122
        _is_batch_add = false;
1✔
123

124
        // Auto-arrange and auto-fill once after batch
125
        if (!features.empty()) {
1✔
126
            std::cout << "Auto-arranging and filling canvas for group toggle" << std::endl;
1✔
127
            autoArrangeVerticalSpacing();// includes auto-fill functionality
1✔
128
        }
129

130
        // Trigger a single canvas update at the end
131
        if (!features.empty()) {
1✔
132
            std::cout << "Triggering single canvas update for group toggle" << std::endl;
1✔
133
            ui->openGLWidget->updateCanvas();
1✔
134
        }
135
    });
1✔
136

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

140
        _is_batch_add = true;
×
141
        // Process all features in the group without triggering individual canvas updates
142
        for (auto const & key: features) {
×
143
            _removeSelectedFeatureWithoutUpdate(key);
×
144
        }
145
        _is_batch_add = false;
×
146

147
        // Auto-arrange and auto-fill once after batch
148
        if (!features.empty()) {
×
149
            std::cout << "Auto-arranging and filling canvas for group toggle" << std::endl;
×
150
            autoArrangeVerticalSpacing();// includes auto-fill functionality
×
151
        }
152

153
        // Trigger a single canvas update at the end
154
        if (!features.empty()) {
×
155
            std::cout << "Triggering single canvas update for group toggle" << std::endl;
×
156
            ui->openGLWidget->updateCanvas();
×
157
        }
158
    });
×
159

160
    // Connect color change signals from the model
161
    connect(_feature_tree_model.get(), &Feature_Tree_Model::featureColorChanged, this, &DataViewer_Widget::_handleColorChanged);
20✔
162

163
    // Connect color change signals from the tree widget to the model
164
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::colorChangeFeatures, this, [this](std::vector<std::string> const & features, std::string const & hex_color) {
20✔
165
        for (auto const & feature: features) {
×
166
            _feature_tree_model->setFeatureColor(feature, hex_color);
×
167
        }
168
    });
×
169

170
    connect(ui->x_axis_samples, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleXAxisSamplesChanged);
20✔
171

172
    connect(ui->global_zoom, &QDoubleSpinBox::valueChanged, this, &DataViewer_Widget::_updateGlobalScale);
20✔
173

174
    connect(ui->theme_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DataViewer_Widget::_handleThemeChanged);
20✔
175

176
    connect(time_scrollbar, &TimeScrollBar::timeChanged, this, &DataViewer_Widget::_updatePlot);
20✔
177

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

189
    std::cout << "Setting GL limit to " << _time_frame->getTotalFrameCount() << std::endl;
20✔
190
    ui->openGLWidget->setXLimit(_time_frame->getTotalFrameCount());
20✔
191

192
    // Set the master time frame for proper coordinate conversion
193
    ui->openGLWidget->setMasterTimeFrame(_time_frame);
20✔
194

195
    // Set spinbox maximum to the actual data range (not the hardcoded UI limit)
196
    int const data_range = static_cast<int>(_time_frame->getTotalFrameCount());
20✔
197
    std::cout << "Setting x_axis_samples maximum to " << data_range << std::endl;
20✔
198
    ui->x_axis_samples->setMaximum(data_range);
20✔
199

200
    // Setup stacked widget with data-type specific viewers
201
    auto analog_widget = new AnalogViewer_Widget(_data_manager, ui->openGLWidget);
20✔
202
    auto interval_widget = new IntervalViewer_Widget(_data_manager, ui->openGLWidget);
20✔
203
    auto event_widget = new EventViewer_Widget(_data_manager, ui->openGLWidget);
20✔
204

205
    ui->stackedWidget->addWidget(analog_widget);
20✔
206
    ui->stackedWidget->addWidget(interval_widget);
20✔
207
    ui->stackedWidget->addWidget(event_widget);
20✔
208

209
    // Connect color change signals from sub-widgets
210
    connect(analog_widget, &AnalogViewer_Widget::colorChanged,
60✔
211
            this, &DataViewer_Widget::_handleColorChanged);
40✔
212
    connect(interval_widget, &IntervalViewer_Widget::colorChanged,
60✔
213
            this, &DataViewer_Widget::_handleColorChanged);
40✔
214
    connect(event_widget, &EventViewer_Widget::colorChanged,
60✔
215
            this, &DataViewer_Widget::_handleColorChanged);
40✔
216

217
    // Connect mouse hover signal from OpenGL widget
218
    connect(ui->openGLWidget, &OpenGLWidget::mouseHover,
60✔
219
            this, &DataViewer_Widget::_updateCoordinateDisplay);
40✔
220

221
    // Grid line connections
222
    connect(ui->grid_lines_enabled, &QCheckBox::toggled, this, &DataViewer_Widget::_handleGridLinesToggled);
20✔
223
    connect(ui->grid_spacing, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleGridSpacingChanged);
20✔
224

225
    // Vertical spacing connection
226
    connect(ui->vertical_spacing, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &DataViewer_Widget::_handleVerticalSpacingChanged);
20✔
227

228
    // Auto-arrange button connection
229
    connect(ui->auto_arrange_button, &QPushButton::clicked, this, &DataViewer_Widget::autoArrangeVerticalSpacing);
20✔
230

231
    // Initialize grid line UI to match OpenGLWidget defaults
232
    ui->grid_lines_enabled->setChecked(ui->openGLWidget->getGridLinesEnabled());
20✔
233
    ui->grid_spacing->setValue(ui->openGLWidget->getGridSpacing());
20✔
234

235
    // Initialize vertical spacing UI to match OpenGLWidget defaults
236
    ui->vertical_spacing->setValue(static_cast<double>(ui->openGLWidget->getVerticalSpacing()));
20✔
237

238
    // Configure splitter behavior
239
    ui->main_splitter->setStretchFactor(0, 0);  // Properties panel doesn't stretch
20✔
240
    ui->main_splitter->setStretchFactor(1, 1);  // Plot area stretches
20✔
241
    
242
    // Set initial sizes: properties panel ~20%, plot area ~80%
243
    ui->main_splitter->setSizes({250, 1000});
20✔
244
    
245
    // Prevent plot area from collapsing, but allow properties panel to collapse
246
    ui->main_splitter->setCollapsible(0, true);  // Properties panel can collapse
20✔
247
    ui->main_splitter->setCollapsible(1, false); // Plot area cannot collapse
20✔
248
    
249
    // Connect hide button (on properties panel)
250
    connect(ui->hide_properties_button, &QPushButton::clicked, this, [this]() {
20✔
UNCOV
251
        _hidePropertiesPanel();
×
UNCOV
252
    });
×
253
    
254
    // Connect show button (on plot side) - initially hidden
255
    connect(ui->show_properties_button, &QPushButton::clicked, this, [this]() {
20✔
UNCOV
256
        _showPropertiesPanel();
×
257
    });
×
258
    
259
    // Initially hide the show button since properties are visible
260
    ui->show_properties_button->hide();
20✔
261
}
40✔
262

263
DataViewer_Widget::~DataViewer_Widget() {
39✔
264
    delete ui;
20✔
265
}
39✔
266

267
void DataViewer_Widget::openWidget() {
18✔
268
    std::cout << "DataViewer Widget Opened" << std::endl;
18✔
269

270
    // Tree is already populated by observer pattern in setDataManager()
271
    // Trigger refresh in case of manual opening
272
    ui->feature_tree_widget->refreshTree();
18✔
273

274
    this->show();
18✔
275
    _updateLabels();
18✔
276
}
18✔
277

UNCOV
278
void DataViewer_Widget::closeEvent(QCloseEvent * event) {
×
279
    static_cast<void>(event);
280

UNCOV
281
    std::cout << "Close event detected" << std::endl;
×
UNCOV
282
}
×
283

284
void DataViewer_Widget::resizeEvent(QResizeEvent * event) {
18✔
285
    QWidget::resizeEvent(event);
18✔
286

287
    // Update plotting manager dimensions when widget is resized
288
    _updatePlottingManagerDimensions();
18✔
289

290
    // The OpenGL widget will automatically get its resizeGL called by Qt
291
    // but we can trigger an additional update if needed
292
    if (ui->openGLWidget) {
18✔
293
        ui->openGLWidget->update();
18✔
294
    }
295
}
18✔
296

297
void DataViewer_Widget::_updatePlot(int time) {
1✔
298
    //std::cout << "Time is " << time;
299
    time = _data_manager->getTime(TimeKey("time"))->getTimeAtIndex(TimeFrameIndex(time));
1✔
300
    //std::cout << ""
301
    ui->openGLWidget->updateCanvas(time);
1✔
302

303
    _updateLabels();
1✔
304
}
1✔
305

306

307
void DataViewer_Widget::_addFeatureToModel(QString const & feature, bool enabled) {
28✔
308
    std::cout << "Feature toggle signal received: " << feature.toStdString() << " enabled: " << enabled << std::endl;
28✔
309

310
    if (enabled) {
28✔
311
        _plotSelectedFeature(feature.toStdString());
28✔
312
    } else {
UNCOV
313
        _removeSelectedFeature(feature.toStdString());
×
314
    }
315
}
28✔
316

317
void DataViewer_Widget::_plotSelectedFeature(std::string const & key) {
28✔
318
    std::cout << "Attempting to plot feature: " << key << std::endl;
28✔
319

320
    if (key.empty()) {
28✔
UNCOV
321
        std::cerr << "Error: empty key in _plotSelectedFeature" << std::endl;
×
UNCOV
322
        return;
×
323
    }
324

325
    if (!_data_manager) {
28✔
UNCOV
326
        std::cerr << "Error: null data manager in _plotSelectedFeature" << std::endl;
×
UNCOV
327
        return;
×
328
    }
329

330
    // Get color from model
331
    std::string color = _feature_tree_model->getFeatureColor(key);
28✔
332
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
28✔
333

334
    auto data_type = _data_manager->getType(key);
28✔
335
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
28✔
336

337
    // Register with plotting manager for coordinated positioning
338
    if (_plotting_manager) {
28✔
339
        std::cout << "Registering series with plotting manager: " << key << std::endl;
28✔
340
    }
341

342
    if (data_type == DM_DataType::Analog) {
28✔
343

344
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
17✔
345
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
17✔
346
        if (!series) {
17✔
UNCOV
347
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
UNCOV
348
            return;
×
349
        }
350

351

352
        auto time_key = _data_manager->getTimeKey(key);
17✔
353
        std::cout << "Time frame key: " << time_key << std::endl;
17✔
354
        auto time_frame = _data_manager->getTime(time_key);
17✔
355
        if (!time_frame) {
17✔
356
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
357
            return;
×
358
        }
359

360
        std::cout << "Time frame has " << time_frame->getTotalFrameCount() << " frames" << std::endl;
17✔
361

362
        // Add to plotting manager first
363
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
17✔
364

365
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
17✔
366
        std::cout << "Successfully added analog series to PlottingManager and OpenGL widget" << std::endl;
17✔
367

368
    } else if (data_type == DM_DataType::DigitalEvent) {
28✔
369

370
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
11✔
371
        auto series = _data_manager->getData<DigitalEventSeries>(key);
11✔
372
        if (!series) {
11✔
UNCOV
373
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
374
            return;
×
375
        }
376

377
        auto time_key = _data_manager->getTimeKey(key);
11✔
378
        auto time_frame = _data_manager->getTime(time_key);
11✔
379
        if (!time_frame) {
11✔
UNCOV
380
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
381
            return;
×
382
        }
383

384
        // Add to plotting manager first
385
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
11✔
386

387
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
11✔
388

389
    } else if (data_type == DM_DataType::DigitalInterval) {
11✔
390

UNCOV
391
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
×
UNCOV
392
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
UNCOV
393
        if (!series) {
×
UNCOV
394
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
UNCOV
395
            return;
×
396
        }
397

UNCOV
398
        auto time_key = _data_manager->getTimeKey(key);
×
UNCOV
399
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
400
        if (!time_frame) {
×
UNCOV
401
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
402
            return;
×
403
        }
404

405
        // Add to plotting manager first
UNCOV
406
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
407

UNCOV
408
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
409

UNCOV
410
    } else {
×
411
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
412
        return;
×
413
    }
414

415
    // Apply coordinated plotting manager allocation after adding to OpenGL widget
416
    if (_plotting_manager) {
28✔
417
        _applyPlottingManagerAllocation(key);
28✔
418
    }
419

420
    // Auto-arrange and auto-fill canvas to make optimal use of space
421
    if (!_is_batch_add) {
28✔
422
        std::cout << "Auto-arranging and filling canvas after adding series" << std::endl;
28✔
423
        autoArrangeVerticalSpacing();// This now includes auto-fill functionality
28✔
424
    }
425

426
    std::cout << "Series addition and auto-arrangement completed" << std::endl;
28✔
427
    // Trigger canvas update to show the new series
428
    if (!_is_batch_add) {
28✔
429
        std::cout << "Triggering canvas update" << std::endl;
28✔
430
        ui->openGLWidget->updateCanvas();
28✔
431
    }
432
    std::cout << "Canvas update completed" << std::endl;
28✔
433
}
28✔
434

UNCOV
435
void DataViewer_Widget::_removeSelectedFeature(std::string const & key) {
×
436
    std::cout << "Attempting to remove feature: " << key << std::endl;
×
437

UNCOV
438
    if (key.empty()) {
×
UNCOV
439
        std::cerr << "Error: empty key in _removeSelectedFeature" << std::endl;
×
UNCOV
440
        return;
×
441
    }
442

443
    if (!_data_manager) {
×
444
        std::cerr << "Error: null data manager in _removeSelectedFeature" << std::endl;
×
445
        return;
×
446
    }
447

448
    auto data_type = _data_manager->getType(key);
×
449

450
    // Remove from plotting manager first
UNCOV
451
    if (_plotting_manager) {
×
UNCOV
452
        bool removed = false;
×
453
        if (data_type == DM_DataType::Analog) {
×
454
            removed = _plotting_manager->removeAnalogSeries(key);
×
UNCOV
455
        } else if (data_type == DM_DataType::DigitalEvent) {
×
456
            removed = _plotting_manager->removeDigitalEventSeries(key);
×
UNCOV
457
        } else if (data_type == DM_DataType::DigitalInterval) {
×
458
            removed = _plotting_manager->removeDigitalIntervalSeries(key);
×
459
        }
UNCOV
460
        if (removed) {
×
UNCOV
461
            std::cout << "Unregistered '" << key << "' from plotting manager" << std::endl;
×
462
        }
463
    }
464

465
    if (data_type == DM_DataType::Analog) {
×
466
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
467
    } else if (data_type == DM_DataType::DigitalEvent) {
×
UNCOV
468
        ui->openGLWidget->removeDigitalEventSeries(key);
×
UNCOV
469
    } else if (data_type == DM_DataType::DigitalInterval) {
×
470
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
471
    } else {
472
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
473
        return;
×
474
    }
475

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

UNCOV
480
    std::cout << "Series removal and auto-arrangement completed" << std::endl;
×
481
    // Trigger canvas update to reflect the removal
UNCOV
482
    std::cout << "Triggering canvas update after removal" << std::endl;
×
483
    ui->openGLWidget->updateCanvas();
×
484
}
485

486
void DataViewer_Widget::_handleFeatureSelected(QString const & feature) {
×
487
    std::cout << "Feature selected signal received: " << feature.toStdString() << std::endl;
×
488

489
    if (feature.isEmpty()) {
×
UNCOV
490
        std::cerr << "Error: empty feature name in _handleFeatureSelected" << std::endl;
×
491
        return;
×
492
    }
493

494
    if (!_data_manager) {
×
495
        std::cerr << "Error: null data manager in _handleFeatureSelected" << std::endl;
×
496
        return;
×
497
    }
498

499
    _highlighted_available_feature = feature;
×
500

501
    // Switch stacked widget based on data type
502
    auto const type = _data_manager->getType(feature.toStdString());
×
UNCOV
503
    auto key = feature.toStdString();
×
504

505
    std::cout << "Feature type for selection: " << convert_data_type_to_string(type) << std::endl;
×
506

507
    if (type == DM_DataType::Analog) {
×
508
        int const stacked_widget_index = 1;// Analog widget is at index 1 (after empty page)
×
509
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
510
        auto analog_widget = dynamic_cast<AnalogViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
511
        if (analog_widget) {
×
UNCOV
512
            analog_widget->setActiveKey(key);
×
513
            std::cout << "Selected Analog Time Series: " << key << std::endl;
×
514
        } else {
UNCOV
515
            std::cerr << "Error: failed to cast to AnalogViewer_Widget" << std::endl;
×
516
        }
517

518
    } else if (type == DM_DataType::DigitalInterval) {
×
UNCOV
519
        int const stacked_widget_index = 2;// Interval widget is at index 2
×
520
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
521
        auto interval_widget = dynamic_cast<IntervalViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
522
        if (interval_widget) {
×
UNCOV
523
            interval_widget->setActiveKey(key);
×
UNCOV
524
            std::cout << "Selected Digital Interval Series: " << key << std::endl;
×
525
        } else {
UNCOV
526
            std::cerr << "Error: failed to cast to IntervalViewer_Widget" << std::endl;
×
527
        }
528

UNCOV
529
    } else if (type == DM_DataType::DigitalEvent) {
×
UNCOV
530
        int const stacked_widget_index = 3;// Event widget is at index 3
×
UNCOV
531
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
532
        auto event_widget = dynamic_cast<EventViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
533
        if (event_widget) {
×
UNCOV
534
            event_widget->setActiveKey(key);
×
UNCOV
535
            std::cout << "Selected Digital Event Series: " << key << std::endl;
×
536
        } else {
UNCOV
537
            std::cerr << "Error: failed to cast to EventViewer_Widget" << std::endl;
×
538
        }
539

540
    } else {
541
        // No specific widget for this type, don't change the current index
UNCOV
542
        std::cout << "Unsupported feature type for detailed view: " << convert_data_type_to_string(type) << std::endl;
×
543
    }
UNCOV
544
}
×
545

546
void DataViewer_Widget::_handleXAxisSamplesChanged(int value) {
2✔
547
    // Use setRangeWidth for spinbox changes (absolute value)
548
    std::cout << "Spinbox requested range width: " << value << std::endl;
2✔
549
    int64_t const actual_range = ui->openGLWidget->setRangeWidth(static_cast<int64_t>(value));
2✔
550
    std::cout << "Actual range width achieved: " << actual_range << std::endl;
2✔
551

552
    // Update the spinbox with the actual range width achieved (in case it was clamped)
553
    if (actual_range != value) {
2✔
554
        std::cout << "Range was clamped, updating spinbox to: " << actual_range << std::endl;
1✔
555
        updateXAxisSamples(static_cast<int>(actual_range));
1✔
556
    }
557
}
2✔
558

559
void DataViewer_Widget::updateXAxisSamples(int value) {
1✔
560
    ui->x_axis_samples->blockSignals(true);
1✔
561
    ui->x_axis_samples->setValue(value);
1✔
562
    ui->x_axis_samples->blockSignals(false);
1✔
563
}
1✔
564

565
void DataViewer_Widget::_updateGlobalScale(double scale) {
23✔
566
    ui->openGLWidget->setGlobalScale(static_cast<float>(scale));
23✔
567

568
    // Also update PlottingManager zoom factor
569
    if (_plotting_manager) {
23✔
570
        _plotting_manager->setGlobalZoom(static_cast<float>(scale));
23✔
571

572
        // Apply updated positions to all registered series
573
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
23✔
574
        for (auto const & key: analog_keys) {
76✔
575
            _applyPlottingManagerAllocation(key);
53✔
576
        }
577
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
23✔
578
        for (auto const & key: event_keys) {
29✔
579
            _applyPlottingManagerAllocation(key);
6✔
580
        }
581
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
23✔
582
        for (auto const & key: interval_keys) {
23✔
UNCOV
583
            _applyPlottingManagerAllocation(key);
×
584
        }
585

586
        // Trigger canvas update
587
        ui->openGLWidget->updateCanvas();
23✔
588
    }
23✔
589
}
23✔
590

UNCOV
591
void DataViewer_Widget::wheelEvent(QWheelEvent * event) {
×
592
    // Disable zooming while dragging intervals
UNCOV
593
    if (ui->openGLWidget->isDraggingInterval()) {
×
594
        return;
×
595
    }
596

597
    auto const numDegrees = static_cast<float>(event->angleDelta().y()) / 8.0f;
×
UNCOV
598
    auto const numSteps = numDegrees / 15.0f;
×
599

600
    auto const current_range = ui->x_axis_samples->value();
×
601

UNCOV
602
    float rangeFactor;
×
UNCOV
603
    if (_zoom_scaling_mode == ZoomScalingMode::Adaptive) {
×
604
        // Adaptive scaling: range factor is proportional to current range width
605
        // This makes adjustments more sensitive when zoomed in (small range), less sensitive when zoomed out (large range)
UNCOV
606
        rangeFactor = static_cast<float>(current_range) * 0.1f;// 10% of current range width
×
607

608
        // Clamp range factor to reasonable bounds
UNCOV
609
        rangeFactor = std::max(1.0f, std::min(rangeFactor, static_cast<float>(_time_frame->getTotalFrameCount()) / 100.0f));
×
610
    } else {
611
        // Fixed scaling (original behavior)
UNCOV
612
        rangeFactor = static_cast<float>(_time_frame->getTotalFrameCount()) / 10000.0f;
×
613
    }
614

615
    // Calculate range delta
616
    // Wheel up (positive numSteps) should zoom IN (decrease range width)
617
    // Wheel down (negative numSteps) should zoom OUT (increase range width)
UNCOV
618
    auto const range_delta = static_cast<int64_t>(-numSteps * rangeFactor);
×
619

620
    // Apply range delta and get the actual achieved range
621
    ui->openGLWidget->changeRangeWidth(range_delta);
×
622

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

627
    // Update spinbox with the actual achieved range (not the requested range)
628
    updateXAxisSamples(actual_range);
×
UNCOV
629
    _updateLabels();
×
630
}
631

632
void DataViewer_Widget::_updateLabels() {
19✔
633
    auto x_axis = ui->openGLWidget->getXAxis();
19✔
634
    ui->neg_x_label->setText(QString::number(x_axis.getStart()));
19✔
635
    ui->pos_x_label->setText(QString::number(x_axis.getEnd()));
19✔
636
}
19✔
637

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

641
    auto const type = _data_manager->getType(feature_key);
×
642

UNCOV
643
    if (type == DM_DataType::Analog) {
×
644
        auto config = ui->openGLWidget->getAnalogConfig(feature_key);
×
UNCOV
645
        if (config.has_value()) {
×
646
            config.value()->hex_color = hex_color;
×
647
        }
648

UNCOV
649
    } else if (type == DM_DataType::DigitalEvent) {
×
650
        auto config = ui->openGLWidget->getDigitalEventConfig(feature_key);
×
UNCOV
651
        if (config.has_value()) {
×
652
            config.value()->hex_color = hex_color;
×
653
        }
654

655
    } else if (type == DM_DataType::DigitalInterval) {
×
656
        auto config = ui->openGLWidget->getDigitalIntervalConfig(feature_key);
×
657
        if (config.has_value()) {
×
658
            config.value()->hex_color = hex_color;
×
659
        }
660
    }
661

662
    // Trigger a redraw
663
    ui->openGLWidget->updateCanvas();
×
664

665
    std::cout << "Color changed for " << feature_key << " to " << hex_color << std::endl;
×
666
}
×
667

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

673
    // Get canvas size for debugging
UNCOV
674
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
675

UNCOV
676
    QString coordinate_text;
×
UNCOV
677
    if (series_info.isEmpty()) {
×
UNCOV
678
        coordinate_text = QString("Coordinates: Time: %1 (index: %2), Canvas Y: %3 | Canvas: %4x%5")
×
UNCOV
679
                                  .arg(actual_time)
×
680
                                  .arg(time_index)
×
681
                                  .arg(canvas_y, 0, 'f', 1)
×
UNCOV
682
                                  .arg(canvas_width)
×
UNCOV
683
                                  .arg(canvas_height);
×
684
    } else {
685
        coordinate_text = QString("Coordinates: Time: %1 (index: %2), %3 | Canvas: %4x%5")
×
686
                                  .arg(actual_time)
×
UNCOV
687
                                  .arg(time_index)
×
UNCOV
688
                                  .arg(series_info)
×
689
                                  .arg(canvas_width)
×
690
                                  .arg(canvas_height);
×
691
    }
692

UNCOV
693
    ui->coordinate_label->setText(coordinate_text);
×
UNCOV
694
}
×
695

696
std::optional<NewAnalogTimeSeriesDisplayOptions *> DataViewer_Widget::getAnalogConfig(std::string const & key) const {
46✔
697
    return ui->openGLWidget->getAnalogConfig(key);
46✔
698
}
699

700
std::optional<NewDigitalEventSeriesDisplayOptions *> DataViewer_Widget::getDigitalEventConfig(std::string const & key) const {
29✔
701
    return ui->openGLWidget->getDigitalEventConfig(key);
29✔
702
}
703

704
std::optional<NewDigitalIntervalSeriesDisplayOptions *> DataViewer_Widget::getDigitalIntervalConfig(std::string const & key) const {
×
UNCOV
705
    return ui->openGLWidget->getDigitalIntervalConfig(key);
×
706
}
707

UNCOV
708
void DataViewer_Widget::_handleThemeChanged(int theme_index) {
×
UNCOV
709
    PlotTheme theme = (theme_index == 0) ? PlotTheme::Dark : PlotTheme::Light;
×
UNCOV
710
    ui->openGLWidget->setPlotTheme(theme);
×
711

712
    // Update coordinate label styling based on theme
UNCOV
713
    if (theme == PlotTheme::Dark) {
×
UNCOV
714
        ui->coordinate_label->setStyleSheet("background-color: rgba(0, 0, 0, 50); color: white; padding: 2px;");
×
715
    } else {
UNCOV
716
        ui->coordinate_label->setStyleSheet("background-color: rgba(255, 255, 255, 50); color: black; padding: 2px;");
×
717
    }
718

UNCOV
719
    std::cout << "Theme changed to: " << (theme_index == 0 ? "Dark" : "Light") << std::endl;
×
UNCOV
720
}
×
721

UNCOV
722
void DataViewer_Widget::_handleGridLinesToggled(bool enabled) {
×
UNCOV
723
    ui->openGLWidget->setGridLinesEnabled(enabled);
×
UNCOV
724
}
×
725

726
void DataViewer_Widget::_handleGridSpacingChanged(int spacing) {
×
UNCOV
727
    ui->openGLWidget->setGridSpacing(spacing);
×
UNCOV
728
}
×
729

730
void DataViewer_Widget::_handleVerticalSpacingChanged(double spacing) {
29✔
731
    ui->openGLWidget->setVerticalSpacing(static_cast<float>(spacing));
29✔
732

733
    // Also update PlottingManager vertical scale
734
    if (_plotting_manager) {
29✔
735
        // Convert spacing to a scale factor relative to default (0.1f)
736
        float const scale_factor = static_cast<float>(spacing) / 0.1f;
29✔
737
        _plotting_manager->setGlobalVerticalScale(scale_factor);
29✔
738

739
        // Apply updated positions to all registered series
740
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
741
        for (auto const & key: analog_keys) {
81✔
742
            _applyPlottingManagerAllocation(key);
52✔
743
        }
744
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
745
        for (auto const & key: event_keys) {
53✔
746
            _applyPlottingManagerAllocation(key);
24✔
747
        }
748
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
749
        for (auto const & key: interval_keys) {
29✔
UNCOV
750
            _applyPlottingManagerAllocation(key);
×
751
        }
752

753
        // Trigger canvas update
754
        ui->openGLWidget->updateCanvas();
29✔
755
    }
29✔
756
}
29✔
757

758
void DataViewer_Widget::_plotSelectedFeatureWithoutUpdate(std::string const & key) {
5✔
759
    std::cout << "Attempting to plot feature (batch): " << key << std::endl;
5✔
760

761
    if (key.empty()) {
5✔
UNCOV
762
        std::cerr << "Error: empty key in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
763
        return;
×
764
    }
765

766
    if (!_data_manager) {
5✔
UNCOV
767
        std::cerr << "Error: null data manager in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
768
        return;
×
769
    }
770

771
    // Get color from model
772
    std::string color = _feature_tree_model->getFeatureColor(key);
5✔
773
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
5✔
774

775
    auto data_type = _data_manager->getType(key);
5✔
776
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
5✔
777

778
    if (data_type == DM_DataType::Analog) {
5✔
779
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
5✔
780
        if (!series) {
5✔
781
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
782
            return;
×
783
        }
784

785
        auto time_key = _data_manager->getTimeKey(key);
5✔
786
        auto time_frame = _data_manager->getTime(time_key);
5✔
787
        if (!time_frame) {
5✔
788
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
789
            return;
×
790
        }
791

792
        // Register with plotting manager for later allocation (single call)
793
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
5✔
794
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
5✔
795

796
    } else if (data_type == DM_DataType::DigitalEvent) {
5✔
797
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
798
        if (!series) {
×
799
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
UNCOV
800
            return;
×
801
        }
802

UNCOV
803
        auto time_key = _data_manager->getTimeKey(key);
×
804
        auto time_frame = _data_manager->getTime(time_key);
×
805
        if (!time_frame) {
×
806
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
807
            return;
×
808
        }
UNCOV
809
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
UNCOV
810
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
×
811

UNCOV
812
    } else if (data_type == DM_DataType::DigitalInterval) {
×
813
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
814
        if (!series) {
×
UNCOV
815
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
816
            return;
×
817
        }
818

UNCOV
819
        auto time_key = _data_manager->getTimeKey(key);
×
UNCOV
820
        auto time_frame = _data_manager->getTime(time_key);
×
821
        if (!time_frame) {
×
822
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
823
            return;
×
824
        }
UNCOV
825
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
826
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
827

828
    } else {
×
829
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
830
        return;
×
831
    }
832

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

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

840
    if (key.empty()) {
×
UNCOV
841
        std::cerr << "Error: empty key in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
842
        return;
×
843
    }
844

845
    if (!_data_manager) {
×
UNCOV
846
        std::cerr << "Error: null data manager in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
847
        return;
×
848
    }
849

UNCOV
850
    auto data_type = _data_manager->getType(key);
×
851

852
    if (data_type == DM_DataType::Analog) {
×
UNCOV
853
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
UNCOV
854
    } else if (data_type == DM_DataType::DigitalEvent) {
×
855
        ui->openGLWidget->removeDigitalEventSeries(key);
×
UNCOV
856
    } else if (data_type == DM_DataType::DigitalInterval) {
×
UNCOV
857
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
858
    } else {
859
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
860
        return;
×
861
    }
862

863
    // Note: No canvas update triggered - this is for batch operations
UNCOV
864
    std::cout << "Successfully removed series from OpenGL widget (batch mode)" << std::endl;
×
865
}
866

867
void DataViewer_Widget::_calculateOptimalScaling(std::vector<std::string> const & group_keys) {
×
UNCOV
868
    if (group_keys.empty()) {
×
UNCOV
869
        return;
×
870
    }
871

UNCOV
872
    std::cout << "Calculating optimal scaling for " << group_keys.size() << " analog channels..." << std::endl;
×
873

874
    // Get current canvas dimensions
875
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
876
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
877

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

881
    // Add any other already visible analog series
882
    auto all_keys = _data_manager->getAllKeys();
×
UNCOV
883
    for (auto const & key: all_keys) {
×
UNCOV
884
        if (_data_manager->getType(key) == DM_DataType::Analog) {
×
885
            // Check if this key is already in our group (avoid double counting)
886
            bool in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
UNCOV
887
            if (!in_group) {
×
888
                // Check if this series is currently visible
889
                auto config = ui->openGLWidget->getAnalogConfig(key);
×
890
                if (config.has_value() && config.value()->is_visible) {
×
891
                    total_visible_analog_series++;
×
892
                }
893
            }
894
        }
895
    }
896

897
    std::cout << "Total visible analog series (including new group): " << total_visible_analog_series << std::endl;
×
898

UNCOV
899
    if (total_visible_analog_series <= 0) {
×
900
        return;// No series to scale
×
901
    }
902

903
    // Calculate optimal vertical spacing
904
    // Leave some margin at top and bottom (10% each = 20% total)
905
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
UNCOV
906
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_analog_series);
×
907

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

912
    // Clamp to reasonable bounds
UNCOV
913
    float const min_spacing = 0.01f;
×
UNCOV
914
    float const max_spacing = 1.0f;
×
UNCOV
915
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
916

917
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
UNCOV
918
              << final_spacing << " normalized units" << std::endl;
×
919

920
    // Calculate optimal global gain based on standard deviations
921
    std::vector<float> std_devs;
×
UNCOV
922
    std_devs.reserve(group_keys.size());
×
923

924
    for (auto const & key: group_keys) {
×
UNCOV
925
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
×
UNCOV
926
        if (series) {
×
927
            float const std_dev = calculate_std_dev_approximate(*series);
×
928
            std_devs.push_back(std_dev);
×
929
            std::cout << "Series " << key << " std dev: " << std_dev << std::endl;
×
930
        }
931
    }
×
932

933
    if (!std_devs.empty()) {
×
934
        // Use the median standard deviation as reference for scaling
UNCOV
935
        std::sort(std_devs.begin(), std_devs.end());
×
936
        float const median_std_dev = std_devs[std_devs.size() / 2];
×
937

938
        // Calculate optimal global scale
939
        // Target: each series should use about 60% of its allocated vertical space
940
        float const target_amplitude_fraction = 0.6f;
×
UNCOV
941
        float const target_amplitude_in_pixels = optimal_spacing * target_amplitude_fraction;
×
942

943
        // Convert to normalized coordinates (3 standard deviations should fit in target amplitude)
944
        float const target_amplitude_normalized = (target_amplitude_in_pixels / static_cast<float>(canvas_height)) * 2.0f;
×
945
        float const three_sigma_target = target_amplitude_normalized;
×
946

947
        // Calculate scale factor needed
UNCOV
948
        float const optimal_global_scale = three_sigma_target / (3.0f * median_std_dev);
×
949

950
        // Clamp to reasonable bounds
951
        float const min_scale = 0.1f;
×
UNCOV
952
        float const max_scale = 100.0f;
×
UNCOV
953
        float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
×
954

UNCOV
955
        std::cout << "Median std dev: " << median_std_dev
×
UNCOV
956
                  << ", target amplitude: " << target_amplitude_in_pixels << " pixels"
×
957
                  << ", optimal global scale: " << final_scale << std::endl;
×
958

959
        // Apply the calculated settings
UNCOV
960
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
961
        ui->global_zoom->setValue(static_cast<double>(final_scale));
×
962

UNCOV
963
        std::cout << "Applied auto-scaling: vertical spacing = " << final_spacing
×
964
                  << ", global scale = " << final_scale << std::endl;
×
965

966
    } else {
967
        // If we can't calculate standard deviations, just apply spacing
968
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
969
        std::cout << "Applied auto-spacing only: vertical spacing = " << final_spacing << std::endl;
×
970
    }
971
}
×
972

973
void DataViewer_Widget::_calculateOptimalEventSpacing(std::vector<std::string> const & group_keys) {
×
UNCOV
974
    if (group_keys.empty()) {
×
UNCOV
975
        return;
×
976
    }
977

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

980
    // Get current canvas dimensions
981
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
982
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
983

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

987
    // Add any other already visible digital event series
988
    auto all_keys = _data_manager->getAllKeys();
×
UNCOV
989
    for (auto const & key: all_keys) {
×
UNCOV
990
        if (_data_manager->getType(key) == DM_DataType::DigitalEvent) {
×
991
            // Check if this key is already in our group (avoid double counting)
992
            bool const in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
UNCOV
993
            if (!in_group) {
×
994
                // Check if this series is currently visible
995
                auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
996
                if (config.has_value() && config.value()->is_visible) {
×
997
                    total_visible_event_series++;
×
998
                }
999
            }
1000
        }
1001
    }
1002

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

UNCOV
1005
    if (total_visible_event_series <= 0) {
×
1006
        return;// No series to scale
×
1007
    }
1008

1009
    // Calculate optimal vertical spacing
1010
    // Leave some margin at top and bottom (10% each = 20% total)
1011
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
1012
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_event_series);
×
1013

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

1018
    // Clamp to reasonable bounds
UNCOV
1019
    float const min_spacing = 0.01f;
×
1020
    float const max_spacing = 1.0f;
×
1021
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
1022

1023
    // Calculate optimal event height (keep events clearly within their lane)
1024
    // Use a conservative fraction of spacing so multiple stacked series remain visually distinct
UNCOV
1025
    float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
×
UNCOV
1026
    float const min_height = 0.01f;
×
UNCOV
1027
    float const max_height = 0.5f;
×
UNCOV
1028
    float const final_height = std::clamp(optimal_event_height, min_height, max_height);
×
1029

UNCOV
1030
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
UNCOV
1031
              << final_spacing << " normalized units" << std::endl;
×
UNCOV
1032
    std::cout << "Calculated event height: " << final_height << " normalized units" << std::endl;
×
1033

1034
    // Apply the calculated settings to all event series in the group
UNCOV
1035
    for (auto const & key: group_keys) {
×
UNCOV
1036
        auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
UNCOV
1037
        if (config.has_value()) {
×
UNCOV
1038
            config.value()->vertical_spacing = final_spacing;
×
UNCOV
1039
            config.value()->event_height = final_height;
×
UNCOV
1040
            config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
×
1041
        }
1042
    }
1043

UNCOV
1044
    std::cout << "Applied auto-calculated event spacing: spacing = " << final_spacing
×
UNCOV
1045
              << ", height = " << final_height << std::endl;
×
UNCOV
1046
}
×
1047

1048
void DataViewer_Widget::autoArrangeVerticalSpacing() {
29✔
1049
    std::cout << "DataViewer_Widget: Auto-arranging with plotting manager..." << std::endl;
29✔
1050

1051
    // Update dimensions first
1052
    _updatePlottingManagerDimensions();
29✔
1053

1054
    // Apply new allocations to all registered series
1055
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
1056
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
1057
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
1058

1059
    for (auto const & key: analog_keys) {
81✔
1060
        _applyPlottingManagerAllocation(key);
52✔
1061
    }
1062
    for (auto const & key: event_keys) {
53✔
1063
        _applyPlottingManagerAllocation(key);
24✔
1064
    }
1065
    for (auto const & key: interval_keys) {
29✔
UNCOV
1066
        _applyPlottingManagerAllocation(key);
×
1067
    }
1068

1069
    // Calculate and apply optimal scaling to fill the canvas
1070
    _autoFillCanvas();
29✔
1071

1072
    // Update OpenGL widget view bounds based on content height
1073
    _updateViewBounds();
29✔
1074

1075
    // Trigger canvas update to show new positions
1076
    ui->openGLWidget->updateCanvas();
29✔
1077

1078
    auto total_keys = analog_keys.size() + event_keys.size() + interval_keys.size();
29✔
1079
    std::cout << "DataViewer_Widget: Auto-arrange completed for " << total_keys << " series" << std::endl;
29✔
1080
}
58✔
1081

1082
void DataViewer_Widget::_updateViewBounds() {
29✔
1083
    if (!_plotting_manager) {
29✔
UNCOV
1084
        return;
×
1085
    }
1086

1087
    // PlottingManager uses normalized coordinates, so view bounds are typically -1 to +1
1088
    // For now, use standard bounds but this enables future enhancement
1089
    std::cout << "DataViewer_Widget: Using standard view bounds with PlottingManager" << std::endl;
29✔
1090
}
1091

UNCOV
1092
std::string DataViewer_Widget::_convertDataType(DM_DataType dm_type) const {
×
UNCOV
1093
    switch (dm_type) {
×
UNCOV
1094
        case DM_DataType::Analog:
×
UNCOV
1095
            return "Analog";
×
UNCOV
1096
        case DM_DataType::DigitalEvent:
×
UNCOV
1097
            return "DigitalEvent";
×
UNCOV
1098
        case DM_DataType::DigitalInterval:
×
UNCOV
1099
            return "DigitalInterval";
×
UNCOV
1100
        default:
×
1101
            // For unsupported types, default to Analog
1102
            // This should be rare in practice given our type filters
UNCOV
1103
            std::cerr << "Warning: Unsupported data type " << convert_data_type_to_string(dm_type)
×
UNCOV
1104
                      << " defaulting to Analog for plotting manager" << std::endl;
×
UNCOV
1105
            return "Analog";
×
1106
    }
1107
}
1108

1109
void DataViewer_Widget::_updatePlottingManagerDimensions() {
47✔
1110
    if (!_plotting_manager) {
47✔
UNCOV
1111
        return;
×
1112
    }
1113

1114
    // Get current canvas dimensions from OpenGL widget
1115
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
47✔
1116

1117
    // PlottingManager works in normalized device coordinates, so no specific dimension update needed
1118
    // But we could update viewport bounds if needed in the future
1119

1120
    std::cout << "DataViewer_Widget: Updated plotting manager dimensions: "
47✔
1121
              << canvas_width << "x" << canvas_height << " pixels" << std::endl;
47✔
1122
}
1123

1124
void DataViewer_Widget::_applyPlottingManagerAllocation(std::string const & series_key) {
243✔
1125
    if (!_plotting_manager) {
243✔
UNCOV
1126
        return;
×
1127
    }
1128

1129
    auto data_type = _data_manager->getType(series_key);
243✔
1130

1131
    std::cout << "DataViewer_Widget: Applying plotting manager allocation for '" << series_key << "'" << std::endl;
243✔
1132

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

1136
    // Apply positioning based on data type
1137
    if (data_type == DM_DataType::Analog) {
243✔
1138
        auto config = ui->openGLWidget->getAnalogConfig(series_key);
178✔
1139
        if (config.has_value()) {
178✔
1140
            float yc, yh;
178✔
1141
            if (_plotting_manager->getAnalogSeriesAllocationForKey(series_key, yc, yh)) {
178✔
1142
                config.value()->allocated_y_center = yc;
178✔
1143
                config.value()->allocated_height = yh;
178✔
1144
            }
1145
        }
1146

1147
    } else if (data_type == DM_DataType::DigitalEvent) {
65✔
1148
        auto config = ui->openGLWidget->getDigitalEventConfig(series_key);
65✔
1149
        if (config.has_value()) {
65✔
1150
            // Basic allocation - will be properly implemented when OpenGL widget is updated
1151
            std::cout << "  Applied basic allocation to event '" << series_key << "'" << std::endl;
65✔
1152
        }
1153

1154
    } else if (data_type == DM_DataType::DigitalInterval) {
×
UNCOV
1155
        auto config = ui->openGLWidget->getDigitalIntervalConfig(series_key);
×
1156
        if (config.has_value()) {
×
1157
            // Basic allocation - will be properly implemented when OpenGL widget is updated
1158
            std::cout << "  Applied basic allocation to interval '" << series_key << "'" << std::endl;
×
1159
        }
1160
    }
1161
}
1162

1163
// ===== Context menu and configuration handling =====
1164
void DataViewer_Widget::_showGroupContextMenu(std::string const & group_name, QPoint const & global_pos) {
×
1165
    QMenu menu;
×
UNCOV
1166
    QMenu * loadMenu = menu.addMenu("Load configuration");
×
UNCOV
1167
    QAction * loadSpikeSorter = loadMenu->addAction("spikesorter configuration");
×
1168
    QAction * clearConfig = menu.addAction("Clear configuration");
×
1169

1170
    connect(loadSpikeSorter, &QAction::triggered, this, [this, group_name]() {
×
UNCOV
1171
        _loadSpikeSorterConfigurationForGroup(QString::fromStdString(group_name));
×
1172
    });
×
1173
    connect(clearConfig, &QAction::triggered, this, [this, group_name]() {
×
UNCOV
1174
        _clearConfigurationForGroup(QString::fromStdString(group_name));
×
1175
    });
×
1176

1177
    menu.exec(global_pos);
×
1178
}
×
1179

UNCOV
1180
void DataViewer_Widget::_loadSpikeSorterConfigurationForGroup(QString const & group_name) {
×
1181
    // For now, use a test constant string or file dialog; here we open a file dialog
1182
    QString path = QFileDialog::getOpenFileName(this, QString("Load spikesorter configuration for %1").arg(group_name), QString(), "Text Files (*.txt *.cfg *.conf);;All Files (*)");
×
UNCOV
1183
    if (path.isEmpty()) return;
×
UNCOV
1184
    QFile file(path);
×
UNCOV
1185
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return;
×
UNCOV
1186
    QByteArray data = file.readAll();
×
UNCOV
1187
    auto positions = _parseSpikeSorterConfig(data.toStdString());
×
UNCOV
1188
    if (positions.empty()) return;
×
UNCOV
1189
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
×
1190

1191
    // Re-apply allocation to visible analog keys and update
UNCOV
1192
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
UNCOV
1193
    for (auto const & key : analog_keys) {
×
UNCOV
1194
        _applyPlottingManagerAllocation(key);
×
1195
    }
UNCOV
1196
    ui->openGLWidget->updateCanvas();
×
UNCOV
1197
}
×
1198

UNCOV
1199
void DataViewer_Widget::_clearConfigurationForGroup(QString const & group_name) {
×
UNCOV
1200
    _plotting_manager->clearAnalogGroupConfiguration(group_name.toStdString());
×
UNCOV
1201
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
UNCOV
1202
    for (auto const & key : analog_keys) {
×
UNCOV
1203
        _applyPlottingManagerAllocation(key);
×
1204
    }
UNCOV
1205
    ui->openGLWidget->updateCanvas();
×
1206
}
×
1207

1208
std::vector<PlottingManager::AnalogGroupChannelPosition> DataViewer_Widget::_parseSpikeSorterConfig(std::string const & text) {
1✔
1209
    std::vector<PlottingManager::AnalogGroupChannelPosition> out;
1✔
1210
    std::istringstream ss(text);
1✔
1211
    std::string line;
1✔
1212
    bool first = true;
1✔
1213
    while (std::getline(ss, line)) {
6✔
1214
        if (line.empty()) continue;
5✔
1215
        if (first) { first = false; continue; } // skip header row (electrode name)
5✔
1216
        std::istringstream ls(line);
4✔
1217
        int row = 0; int ch = 0; float x = 0.0f; float y = 0.0f;
4✔
1218
        if (!(ls >> row >> ch >> x >> y)) continue;
4✔
1219
        // SpikeSorter is 1-based; convert to 0-based for our program
1220
        if (ch > 0) ch -= 1;
4✔
1221
        PlottingManager::AnalogGroupChannelPosition p; p.channel_id = ch; p.x = x; p.y = y;
4✔
1222
        out.push_back(p);
4✔
1223
    }
4✔
1224
    return out;
2✔
1225
}
1✔
1226

1227
void DataViewer_Widget::_loadSpikeSorterConfigurationFromText(QString const & group_name, QString const & text) {
1✔
1228
    auto positions = _parseSpikeSorterConfig(text.toStdString());
1✔
1229
    if (positions.empty()) {
1✔
UNCOV
1230
        std::cout << "No positions found in spike sorter configuration" << std::endl;
×
UNCOV
1231
        return;
×
1232
    }
1233
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
1✔
1234
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
1✔
1235
    for (auto const & key : analog_keys) {
5✔
1236
        _applyPlottingManagerAllocation(key);
4✔
1237
    }
1238
    ui->openGLWidget->updateCanvas();
1✔
1239
}
1✔
1240

1241
void DataViewer_Widget::_autoFillCanvas() {
29✔
1242
    std::cout << "DataViewer_Widget: Auto-filling canvas with PlottingManager..." << std::endl;
29✔
1243

1244
    if (!_plotting_manager) {
29✔
1245
        std::cout << "No plotting manager available" << std::endl;
×
UNCOV
1246
        return;
×
1247
    }
1248

1249
    // Get current canvas dimensions
1250
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
29✔
1251
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
29✔
1252

1253
    // Count visible series using PlottingManager
1254
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
1255
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
1256
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
1257

1258
    int visible_analog_count = static_cast<int>(analog_keys.size());
29✔
1259
    int visible_event_count = static_cast<int>(event_keys.size());
29✔
1260
    int visible_interval_count = static_cast<int>(interval_keys.size());
29✔
1261

1262
    int total_visible = visible_analog_count + visible_event_count + visible_interval_count;
29✔
1263
    std::cout << "Visible series: " << visible_analog_count << " analog, "
29✔
1264
              << visible_event_count << " events, " << visible_interval_count
29✔
1265
              << " intervals (total: " << total_visible << ")" << std::endl;
29✔
1266

1267
    if (total_visible == 0) {
29✔
UNCOV
1268
        std::cout << "No visible series to auto-scale" << std::endl;
×
UNCOV
1269
        return;
×
1270
    }
1271

1272
    // Calculate optimal vertical spacing to fill canvas
1273
    // Use 90% of canvas height, leaving 5% margin at top and bottom
1274
    float const usable_height = static_cast<float>(canvas_height) * 0.9f;
29✔
1275
    float const optimal_spacing_pixels = usable_height / static_cast<float>(total_visible);
29✔
1276

1277
    // Convert to normalized coordinates (assuming 2.0 total normalized height)
1278
    float const optimal_spacing_normalized = (optimal_spacing_pixels / static_cast<float>(canvas_height)) * 2.0f;
29✔
1279

1280
    // Clamp to reasonable bounds
1281
    float const min_spacing = 0.02f;
29✔
1282
    float const max_spacing = 1.5f;
29✔
1283
    float const final_spacing = std::clamp(optimal_spacing_normalized, min_spacing, max_spacing);
29✔
1284

1285
    std::cout << "Calculated optimal spacing: " << optimal_spacing_pixels << " pixels -> "
29✔
1286
              << final_spacing << " normalized units" << std::endl;
29✔
1287

1288
    // Apply the calculated vertical spacing
1289
    ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
29✔
1290

1291
    // Calculate and apply optimal event heights for digital event series
1292
    if (visible_event_count > 0) {
29✔
1293
        // Calculate event height conservatively to avoid near full-lane rendering
1294
        // Use 30% of the spacing and cap at 0.2 to keep consistent scale across zooms
1295
        float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
11✔
1296

1297
        std::cout << "Calculated optimal event height: " << optimal_event_height << " normalized units" << std::endl;
11✔
1298

1299
        // Apply optimal height to all visible digital event series
1300
        for (auto const & key: event_keys) {
35✔
1301
            auto config = ui->openGLWidget->getDigitalEventConfig(key);
24✔
1302
            if (config.has_value() && config.value()->is_visible) {
24✔
1303
                config.value()->event_height = optimal_event_height;
24✔
1304
                config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
24✔
1305
                std::cout << "  Applied event height " << optimal_event_height
24✔
1306
                          << " to series '" << key << "'" << std::endl;
24✔
1307
            }
1308
        }
1309
    }
1310

1311
    // Calculate and apply optimal interval heights for digital interval series
1312
    if (visible_interval_count > 0) {
29✔
1313
        // Calculate optimal interval height to fill most of the allocated space
1314
        // Use 80% of the spacing to leave some visual separation between intervals
UNCOV
1315
        float const optimal_interval_height = final_spacing * 0.8f;
×
1316

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

1319
        // Apply optimal height to all visible digital interval series
UNCOV
1320
        for (auto const & key: interval_keys) {
×
UNCOV
1321
            auto config = ui->openGLWidget->getDigitalIntervalConfig(key);
×
UNCOV
1322
            if (config.has_value() && config.value()->is_visible) {
×
UNCOV
1323
                config.value()->interval_height = optimal_interval_height;
×
UNCOV
1324
                std::cout << "  Applied interval height " << optimal_interval_height
×
UNCOV
1325
                          << " to series '" << key << "'" << std::endl;
×
1326
            }
1327
        }
1328
    }
1329

1330
    // Calculate optimal global scale for analog series to use their allocated space effectively
1331
    if (visible_analog_count > 0) {
29✔
1332
        // Sample a few analog series to estimate appropriate scaling
1333
        std::vector<float> sample_std_devs;
22✔
1334
        sample_std_devs.reserve(std::min(5, visible_analog_count));// Sample up to 5 series
22✔
1335

1336
        int sampled = 0;
22✔
1337
        for (auto const & key: analog_keys) {
74✔
1338
            if (sampled >= 5) break;
52✔
1339

1340
            auto config = ui->openGLWidget->getAnalogConfig(key);
52✔
1341
            if (config.has_value() && config.value()->is_visible) {
52✔
1342
                auto series = _data_manager->getData<AnalogTimeSeries>(key);
52✔
1343
                if (series) {
52✔
1344
                    float std_dev = calculate_std_dev_approximate(*series);
52✔
1345
                    if (std_dev > 0.0f) {
52✔
1346
                        sample_std_devs.push_back(std_dev);
52✔
1347
                        sampled++;
52✔
1348
                    }
1349
                }
1350
            }
52✔
1351
        }
1352

1353
        if (!sample_std_devs.empty()) {
22✔
1354
            // Use median standard deviation for scaling calculation
1355
            std::sort(sample_std_devs.begin(), sample_std_devs.end());
22✔
1356
            float median_std_dev = sample_std_devs[sample_std_devs.size() / 2];
22✔
1357

1358
            // Calculate scale so that ±3 standard deviations use ~60% of allocated space
1359
            float const target_amplitude_fraction = 0.6f;
22✔
1360
            float const target_amplitude_pixels = optimal_spacing_pixels * target_amplitude_fraction;
22✔
1361
            float const target_amplitude_normalized = (target_amplitude_pixels / static_cast<float>(canvas_height)) * 2.0f;
22✔
1362

1363
            // For ±3σ coverage
1364
            float const three_sigma_coverage = target_amplitude_normalized;
22✔
1365
            float const optimal_global_scale = three_sigma_coverage / (6.0f * median_std_dev);
22✔
1366

1367
            // Clamp to reasonable bounds
1368
            float const min_scale = 0.01f;
22✔
1369
            float const max_scale = 100.0f;
22✔
1370
            float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
22✔
1371

1372
            std::cout << "Calculated optimal global scale: median_std_dev=" << median_std_dev
22✔
1373
                      << ", target_amplitude=" << target_amplitude_pixels << " pixels"
22✔
1374
                      << ", final_scale=" << final_scale << std::endl;
22✔
1375

1376
            // Apply the calculated global scale
1377
            ui->global_zoom->setValue(static_cast<double>(final_scale));
22✔
1378
        }
1379
    }
22✔
1380

1381
    std::cout << "Auto-fill canvas completed" << std::endl;
29✔
1382
}
29✔
1383

1384
void DataViewer_Widget::cleanupDeletedData() {
15✔
1385
    if (!_data_manager) {
15✔
UNCOV
1386
        return;
×
1387
    }
1388

1389
    // Collect keys that no longer exist in DataManager
1390
    std::vector<std::string> keys_to_cleanup;
15✔
1391

1392
    if (_plotting_manager) {
15✔
1393
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
15✔
1394
        for (auto const & key: analog_keys) {
18✔
1395
            if (!_data_manager->getData<AnalogTimeSeries>(key)) {
3✔
UNCOV
1396
                keys_to_cleanup.push_back(key);
×
1397
            }
1398
        }
1399
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
15✔
1400
        for (auto const & key: event_keys) {
15✔
1401
            if (!_data_manager->getData<DigitalEventSeries>(key)) {
×
1402
                keys_to_cleanup.push_back(key);
×
1403
            }
1404
        }
1405
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
15✔
1406
        for (auto const & key: interval_keys) {
15✔
UNCOV
1407
            if (!_data_manager->getData<DigitalIntervalSeries>(key)) {
×
UNCOV
1408
                keys_to_cleanup.push_back(key);
×
1409
            }
1410
        }
1411
    }
15✔
1412

1413
    if (keys_to_cleanup.empty()) {
15✔
1414
        return;
15✔
1415
    }
1416

1417
    // De-duplicate keys in case the same key appears in multiple lists
UNCOV
1418
    std::sort(keys_to_cleanup.begin(), keys_to_cleanup.end());
×
1419
    keys_to_cleanup.erase(std::unique(keys_to_cleanup.begin(), keys_to_cleanup.end()), keys_to_cleanup.end());
×
1420

1421
    // Post cleanup to OpenGLWidget's thread safely
UNCOV
1422
    QPointer<OpenGLWidget> glw = ui ? ui->openGLWidget : nullptr;
×
UNCOV
1423
    if (glw) {
×
UNCOV
1424
        QMetaObject::invokeMethod(glw, [glw, keys = keys_to_cleanup]() {
×
UNCOV
1425
            if (!glw) return;
×
UNCOV
1426
            for (auto const & key : keys) {
×
UNCOV
1427
                glw->removeAnalogTimeSeries(key);
×
UNCOV
1428
                glw->removeDigitalEventSeries(key);
×
UNCOV
1429
                glw->removeDigitalIntervalSeries(key);
×
1430
            } }, Qt::QueuedConnection);
1431
    }
1432

1433
    // Remove from PlottingManager defensively (all types) on our thread
UNCOV
1434
    if (_plotting_manager) {
×
UNCOV
1435
        for (auto const & key: keys_to_cleanup) {
×
UNCOV
1436
            (void) _plotting_manager->removeAnalogSeries(key);
×
UNCOV
1437
            (void) _plotting_manager->removeDigitalEventSeries(key);
×
UNCOV
1438
            (void) _plotting_manager->removeDigitalIntervalSeries(key);
×
1439
        }
1440
    }
1441

1442
    // Re-arrange remaining data
UNCOV
1443
    autoArrangeVerticalSpacing();
×
1444
}
15✔
1445

UNCOV
1446
void DataViewer_Widget::_hidePropertiesPanel() {
×
1447
    // Save current splitter sizes before hiding
UNCOV
1448
    _saved_splitter_sizes = ui->main_splitter->sizes();
×
1449
    
1450
    // Collapse the properties panel to 0 width
UNCOV
1451
    ui->main_splitter->setSizes({0, ui->main_splitter->sizes()[1]});
×
1452
    
1453
    // Hide the properties panel and show the reveal button
UNCOV
1454
    ui->properties_container->hide();
×
UNCOV
1455
    ui->show_properties_button->show();
×
1456
    
UNCOV
1457
    _properties_panel_collapsed = true;
×
1458
    
UNCOV
1459
    std::cout << "Properties panel hidden" << std::endl;
×
1460
    
1461
    // Trigger a canvas update to adjust to new size
UNCOV
1462
    ui->openGLWidget->update();
×
UNCOV
1463
}
×
1464

UNCOV
1465
void DataViewer_Widget::_showPropertiesPanel() {
×
1466
    // Show the properties panel
UNCOV
1467
    ui->properties_container->show();
×
1468
    
1469
    // Restore saved splitter sizes
UNCOV
1470
    if (!_saved_splitter_sizes.isEmpty()) {
×
UNCOV
1471
        ui->main_splitter->setSizes(_saved_splitter_sizes);
×
1472
    } else {
1473
        // Default sizes if no saved sizes (250px for properties, rest for plot)
UNCOV
1474
        ui->main_splitter->setSizes({250, 1000});
×
1475
    }
1476
    
1477
    // Hide the reveal button
UNCOV
1478
    ui->show_properties_button->hide();
×
1479
    
UNCOV
1480
    _properties_panel_collapsed = false;
×
1481
    
UNCOV
1482
    std::cout << "Properties panel shown" << std::endl;
×
1483
    
1484
    // Trigger a canvas update to adjust to new size
UNCOV
1485
    ui->openGLWidget->update();
×
UNCOV
1486
}
×
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