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

paulmthompson / WhiskerToolbox / 17810127916

17 Sep 2025 08:40PM UTC coverage: 71.967% (+0.3%) from 71.661%
17810127916

push

github

paulmthompson
fixed error in plotting digital events

283 of 300 new or added lines in 5 files covered. (94.33%)

314 existing lines in 3 files now uncovered.

39620 of 55053 relevant lines covered (71.97%)

1301.32 hits per line

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

44.91
/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 type column text "Group" and are under Analog data type parent
92
        QString const type_text = item->text(1);
×
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()) {
×
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
}
40✔
238

239
DataViewer_Widget::~DataViewer_Widget() {
39✔
240
    delete ui;
20✔
241
}
39✔
242

243
void DataViewer_Widget::openWidget() {
18✔
244
    std::cout << "DataViewer Widget Opened" << std::endl;
18✔
245

246
    // Tree is already populated by observer pattern in setDataManager()
247
    // Trigger refresh in case of manual opening
248
    ui->feature_tree_widget->refreshTree();
18✔
249

250
    this->show();
18✔
251
    _updateLabels();
18✔
252
}
18✔
253

254
void DataViewer_Widget::closeEvent(QCloseEvent * event) {
×
255
    static_cast<void>(event);
256

257
    std::cout << "Close event detected" << std::endl;
×
258
}
×
259

260
void DataViewer_Widget::resizeEvent(QResizeEvent * event) {
18✔
261
    QWidget::resizeEvent(event);
18✔
262

263
    // Update plotting manager dimensions when widget is resized
264
    _updatePlottingManagerDimensions();
18✔
265

266
    // The OpenGL widget will automatically get its resizeGL called by Qt
267
    // but we can trigger an additional update if needed
268
    if (ui->openGLWidget) {
18✔
269
        ui->openGLWidget->update();
18✔
270
    }
271
}
18✔
272

273
void DataViewer_Widget::_updatePlot(int time) {
1✔
274
    //std::cout << "Time is " << time;
275
    time = _data_manager->getTime(TimeKey("time"))->getTimeAtIndex(TimeFrameIndex(time));
1✔
276
    //std::cout << ""
277
    ui->openGLWidget->updateCanvas(time);
1✔
278

279
    _updateLabels();
1✔
280
}
1✔
281

282

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

286
    if (enabled) {
28✔
287
        _plotSelectedFeature(feature.toStdString());
28✔
288
    } else {
289
        _removeSelectedFeature(feature.toStdString());
×
290
    }
291
}
28✔
292

293
void DataViewer_Widget::_plotSelectedFeature(std::string const & key) {
28✔
294
    std::cout << "Attempting to plot feature: " << key << std::endl;
28✔
295

296
    if (key.empty()) {
28✔
297
        std::cerr << "Error: empty key in _plotSelectedFeature" << std::endl;
×
298
        return;
×
299
    }
300

301
    if (!_data_manager) {
28✔
302
        std::cerr << "Error: null data manager in _plotSelectedFeature" << std::endl;
×
303
        return;
×
304
    }
305

306
    // Get color from model
307
    std::string color = _feature_tree_model->getFeatureColor(key);
28✔
308
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
28✔
309

310
    auto data_type = _data_manager->getType(key);
28✔
311
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
28✔
312

313
    // Register with plotting manager for coordinated positioning
314
    if (_plotting_manager) {
28✔
315
        std::cout << "Registering series with plotting manager: " << key << std::endl;
28✔
316
    }
317

318
    if (data_type == DM_DataType::Analog) {
28✔
319

320
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
17✔
321
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
17✔
322
        if (!series) {
17✔
323
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
324
            return;
×
325
        }
326

327

328
        auto time_key = _data_manager->getTimeKey(key);
17✔
329
        std::cout << "Time frame key: " << time_key << std::endl;
17✔
330
        auto time_frame = _data_manager->getTime(time_key);
17✔
331
        if (!time_frame) {
17✔
332
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
333
            return;
×
334
        }
335

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

338
        // Add to plotting manager first
339
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
17✔
340

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

344
    } else if (data_type == DM_DataType::DigitalEvent) {
28✔
345

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

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

360
        // Add to plotting manager first
361
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
11✔
362

363
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
11✔
364

365
    } else if (data_type == DM_DataType::DigitalInterval) {
11✔
366

367
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
×
368
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
369
        if (!series) {
×
370
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
371
            return;
×
372
        }
373

374
        auto time_key = _data_manager->getTimeKey(key);
×
375
        auto time_frame = _data_manager->getTime(time_key);
×
376
        if (!time_frame) {
×
377
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
378
            return;
×
379
        }
380

381
        // Add to plotting manager first
382
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
383

384
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
385

386
    } else {
×
387
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
388
        return;
×
389
    }
390

391
    // Apply coordinated plotting manager allocation after adding to OpenGL widget
392
    if (_plotting_manager) {
28✔
393
        _applyPlottingManagerAllocation(key);
28✔
394
    }
395

396
    // Auto-arrange and auto-fill canvas to make optimal use of space
397
    if (!_is_batch_add) {
28✔
398
        std::cout << "Auto-arranging and filling canvas after adding series" << std::endl;
28✔
399
        autoArrangeVerticalSpacing();// This now includes auto-fill functionality
28✔
400
    }
401

402
    std::cout << "Series addition and auto-arrangement completed" << std::endl;
28✔
403
    // Trigger canvas update to show the new series
404
    if (!_is_batch_add) {
28✔
405
        std::cout << "Triggering canvas update" << std::endl;
28✔
406
        ui->openGLWidget->updateCanvas();
28✔
407
    }
408
    std::cout << "Canvas update completed" << std::endl;
28✔
409
}
28✔
410

411
void DataViewer_Widget::_removeSelectedFeature(std::string const & key) {
×
412
    std::cout << "Attempting to remove feature: " << key << std::endl;
×
413

414
    if (key.empty()) {
×
415
        std::cerr << "Error: empty key in _removeSelectedFeature" << std::endl;
×
416
        return;
×
417
    }
418

419
    if (!_data_manager) {
×
420
        std::cerr << "Error: null data manager in _removeSelectedFeature" << std::endl;
×
421
        return;
×
422
    }
423

424
    auto data_type = _data_manager->getType(key);
×
425

426
    // Remove from plotting manager first
427
    if (_plotting_manager) {
×
428
        bool removed = false;
×
429
        if (data_type == DM_DataType::Analog) {
×
430
            removed = _plotting_manager->removeAnalogSeries(key);
×
431
        } else if (data_type == DM_DataType::DigitalEvent) {
×
432
            removed = _plotting_manager->removeDigitalEventSeries(key);
×
433
        } else if (data_type == DM_DataType::DigitalInterval) {
×
434
            removed = _plotting_manager->removeDigitalIntervalSeries(key);
×
435
        }
436
        if (removed) {
×
437
            std::cout << "Unregistered '" << key << "' from plotting manager" << std::endl;
×
438
        }
439
    }
440

441
    if (data_type == DM_DataType::Analog) {
×
442
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
443
    } else if (data_type == DM_DataType::DigitalEvent) {
×
444
        ui->openGLWidget->removeDigitalEventSeries(key);
×
445
    } else if (data_type == DM_DataType::DigitalInterval) {
×
446
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
447
    } else {
448
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
449
        return;
×
450
    }
451

452
    // Auto-arrange and auto-fill canvas to rescale remaining elements
453
    std::cout << "Auto-arranging and filling canvas after removing series" << std::endl;
×
454
    autoArrangeVerticalSpacing();// This now includes auto-fill functionality
×
455

456
    std::cout << "Series removal and auto-arrangement completed" << std::endl;
×
457
    // Trigger canvas update to reflect the removal
458
    std::cout << "Triggering canvas update after removal" << std::endl;
×
459
    ui->openGLWidget->updateCanvas();
×
460
}
461

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

465
    if (feature.isEmpty()) {
×
466
        std::cerr << "Error: empty feature name in _handleFeatureSelected" << std::endl;
×
467
        return;
×
468
    }
469

470
    if (!_data_manager) {
×
471
        std::cerr << "Error: null data manager in _handleFeatureSelected" << std::endl;
×
472
        return;
×
473
    }
474

475
    _highlighted_available_feature = feature;
×
476

477
    // Switch stacked widget based on data type
478
    auto const type = _data_manager->getType(feature.toStdString());
×
479
    auto key = feature.toStdString();
×
480

481
    std::cout << "Feature type for selection: " << convert_data_type_to_string(type) << std::endl;
×
482

483
    if (type == DM_DataType::Analog) {
×
484
        int const stacked_widget_index = 1;// Analog widget is at index 1 (after empty page)
×
485
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
486
        auto analog_widget = dynamic_cast<AnalogViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
487
        if (analog_widget) {
×
488
            analog_widget->setActiveKey(key);
×
489
            std::cout << "Selected Analog Time Series: " << key << std::endl;
×
490
        } else {
491
            std::cerr << "Error: failed to cast to AnalogViewer_Widget" << std::endl;
×
492
        }
493

494
    } else if (type == DM_DataType::DigitalInterval) {
×
495
        int const stacked_widget_index = 2;// Interval widget is at index 2
×
496
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
497
        auto interval_widget = dynamic_cast<IntervalViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
498
        if (interval_widget) {
×
499
            interval_widget->setActiveKey(key);
×
500
            std::cout << "Selected Digital Interval Series: " << key << std::endl;
×
501
        } else {
502
            std::cerr << "Error: failed to cast to IntervalViewer_Widget" << std::endl;
×
503
        }
504

505
    } else if (type == DM_DataType::DigitalEvent) {
×
506
        int const stacked_widget_index = 3;// Event widget is at index 3
×
507
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
508
        auto event_widget = dynamic_cast<EventViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
509
        if (event_widget) {
×
510
            event_widget->setActiveKey(key);
×
511
            std::cout << "Selected Digital Event Series: " << key << std::endl;
×
512
        } else {
513
            std::cerr << "Error: failed to cast to EventViewer_Widget" << std::endl;
×
514
        }
515

516
    } else {
517
        // No specific widget for this type, don't change the current index
518
        std::cout << "Unsupported feature type for detailed view: " << convert_data_type_to_string(type) << std::endl;
×
519
    }
520
}
×
521

522
void DataViewer_Widget::_handleXAxisSamplesChanged(int value) {
2✔
523
    // Use setRangeWidth for spinbox changes (absolute value)
524
    std::cout << "Spinbox requested range width: " << value << std::endl;
2✔
525
    int64_t const actual_range = ui->openGLWidget->setRangeWidth(static_cast<int64_t>(value));
2✔
526
    std::cout << "Actual range width achieved: " << actual_range << std::endl;
2✔
527

528
    // Update the spinbox with the actual range width achieved (in case it was clamped)
529
    if (actual_range != value) {
2✔
530
        std::cout << "Range was clamped, updating spinbox to: " << actual_range << std::endl;
1✔
531
        updateXAxisSamples(static_cast<int>(actual_range));
1✔
532
    }
533
}
2✔
534

535
void DataViewer_Widget::updateXAxisSamples(int value) {
1✔
536
    ui->x_axis_samples->blockSignals(true);
1✔
537
    ui->x_axis_samples->setValue(value);
1✔
538
    ui->x_axis_samples->blockSignals(false);
1✔
539
}
1✔
540

541
void DataViewer_Widget::_updateGlobalScale(double scale) {
23✔
542
    ui->openGLWidget->setGlobalScale(static_cast<float>(scale));
23✔
543

544
    // Also update PlottingManager zoom factor
545
    if (_plotting_manager) {
23✔
546
        _plotting_manager->setGlobalZoom(static_cast<float>(scale));
23✔
547

548
        // Apply updated positions to all registered series
549
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
23✔
550
        for (auto const & key: analog_keys) {
76✔
551
            _applyPlottingManagerAllocation(key);
53✔
552
        }
553
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
23✔
554
        for (auto const & key: event_keys) {
29✔
555
            _applyPlottingManagerAllocation(key);
6✔
556
        }
557
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
23✔
558
        for (auto const & key: interval_keys) {
23✔
559
            _applyPlottingManagerAllocation(key);
×
560
        }
561

562
        // Trigger canvas update
563
        ui->openGLWidget->updateCanvas();
23✔
564
    }
23✔
565
}
23✔
566

567
void DataViewer_Widget::wheelEvent(QWheelEvent * event) {
×
568
    // Disable zooming while dragging intervals
569
    if (ui->openGLWidget->isDraggingInterval()) {
×
570
        return;
×
571
    }
572

573
    auto const numDegrees = static_cast<float>(event->angleDelta().y()) / 8.0f;
×
574
    auto const numSteps = numDegrees / 15.0f;
×
575

576
    auto const current_range = ui->x_axis_samples->value();
×
577

578
    float rangeFactor;
×
579
    if (_zoom_scaling_mode == ZoomScalingMode::Adaptive) {
×
580
        // Adaptive scaling: range factor is proportional to current range width
581
        // This makes adjustments more sensitive when zoomed in (small range), less sensitive when zoomed out (large range)
582
        rangeFactor = static_cast<float>(current_range) * 0.1f;// 10% of current range width
×
583

584
        // Clamp range factor to reasonable bounds
585
        rangeFactor = std::max(1.0f, std::min(rangeFactor, static_cast<float>(_time_frame->getTotalFrameCount()) / 100.0f));
×
586
    } else {
587
        // Fixed scaling (original behavior)
588
        rangeFactor = static_cast<float>(_time_frame->getTotalFrameCount()) / 10000.0f;
×
589
    }
590

591
    // Calculate range delta
592
    // Wheel up (positive numSteps) should zoom IN (decrease range width)
593
    // Wheel down (negative numSteps) should zoom OUT (increase range width)
594
    auto const range_delta = static_cast<int64_t>(-numSteps * rangeFactor);
×
595

596
    // Apply range delta and get the actual achieved range
597
    ui->openGLWidget->changeRangeWidth(range_delta);
×
598

599
    // Get the actual range that was achieved (may be different due to clamping)
600
    auto x_axis = ui->openGLWidget->getXAxis();
×
601
    auto const actual_range = static_cast<int>(x_axis.getEnd() - x_axis.getStart());
×
602

603
    // Update spinbox with the actual achieved range (not the requested range)
604
    updateXAxisSamples(actual_range);
×
605
    _updateLabels();
×
606
}
607

608
void DataViewer_Widget::_updateLabels() {
19✔
609
    auto x_axis = ui->openGLWidget->getXAxis();
19✔
610
    ui->neg_x_label->setText(QString::number(x_axis.getStart()));
19✔
611
    ui->pos_x_label->setText(QString::number(x_axis.getEnd()));
19✔
612
}
19✔
613

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

617
    auto const type = _data_manager->getType(feature_key);
×
618

619
    if (type == DM_DataType::Analog) {
×
620
        auto config = ui->openGLWidget->getAnalogConfig(feature_key);
×
621
        if (config.has_value()) {
×
622
            config.value()->hex_color = hex_color;
×
623
        }
624

625
    } else if (type == DM_DataType::DigitalEvent) {
×
626
        auto config = ui->openGLWidget->getDigitalEventConfig(feature_key);
×
627
        if (config.has_value()) {
×
628
            config.value()->hex_color = hex_color;
×
629
        }
630

631
    } else if (type == DM_DataType::DigitalInterval) {
×
632
        auto config = ui->openGLWidget->getDigitalIntervalConfig(feature_key);
×
633
        if (config.has_value()) {
×
634
            config.value()->hex_color = hex_color;
×
635
        }
636
    }
637

638
    // Trigger a redraw
639
    ui->openGLWidget->updateCanvas();
×
640

641
    std::cout << "Color changed for " << feature_key << " to " << hex_color << std::endl;
×
642
}
×
643

644
void DataViewer_Widget::_updateCoordinateDisplay(float time_coordinate, float canvas_y, QString const & series_info) {
×
645
    // Convert time coordinate to actual time using the time frame
646
    int const time_index = static_cast<int>(std::round(time_coordinate));
×
647
    int const actual_time = _time_frame->getTimeAtIndex(TimeFrameIndex(time_index));
×
648

649
    // Get canvas size for debugging
650
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
651

652
    QString coordinate_text;
×
653
    if (series_info.isEmpty()) {
×
654
        coordinate_text = QString("Coordinates: Time: %1 (index: %2), Canvas Y: %3 | Canvas: %4x%5")
×
655
                                  .arg(actual_time)
×
656
                                  .arg(time_index)
×
657
                                  .arg(canvas_y, 0, 'f', 1)
×
658
                                  .arg(canvas_width)
×
659
                                  .arg(canvas_height);
×
660
    } else {
661
        coordinate_text = QString("Coordinates: Time: %1 (index: %2), %3 | Canvas: %4x%5")
×
662
                                  .arg(actual_time)
×
663
                                  .arg(time_index)
×
664
                                  .arg(series_info)
×
665
                                  .arg(canvas_width)
×
666
                                  .arg(canvas_height);
×
667
    }
668

669
    ui->coordinate_label->setText(coordinate_text);
×
670
}
×
671

672
std::optional<NewAnalogTimeSeriesDisplayOptions *> DataViewer_Widget::getAnalogConfig(std::string const & key) const {
46✔
673
    return ui->openGLWidget->getAnalogConfig(key);
46✔
674
}
675

676
std::optional<NewDigitalEventSeriesDisplayOptions *> DataViewer_Widget::getDigitalEventConfig(std::string const & key) const {
29✔
677
    return ui->openGLWidget->getDigitalEventConfig(key);
29✔
678
}
679

680
std::optional<NewDigitalIntervalSeriesDisplayOptions *> DataViewer_Widget::getDigitalIntervalConfig(std::string const & key) const {
×
681
    return ui->openGLWidget->getDigitalIntervalConfig(key);
×
682
}
683

684
void DataViewer_Widget::_handleThemeChanged(int theme_index) {
×
685
    PlotTheme theme = (theme_index == 0) ? PlotTheme::Dark : PlotTheme::Light;
×
686
    ui->openGLWidget->setPlotTheme(theme);
×
687

688
    // Update coordinate label styling based on theme
689
    if (theme == PlotTheme::Dark) {
×
690
        ui->coordinate_label->setStyleSheet("background-color: rgba(0, 0, 0, 50); color: white; padding: 2px;");
×
691
    } else {
692
        ui->coordinate_label->setStyleSheet("background-color: rgba(255, 255, 255, 50); color: black; padding: 2px;");
×
693
    }
694

695
    std::cout << "Theme changed to: " << (theme_index == 0 ? "Dark" : "Light") << std::endl;
×
696
}
×
697

698
void DataViewer_Widget::_handleGridLinesToggled(bool enabled) {
×
699
    ui->openGLWidget->setGridLinesEnabled(enabled);
×
700
}
×
701

702
void DataViewer_Widget::_handleGridSpacingChanged(int spacing) {
×
703
    ui->openGLWidget->setGridSpacing(spacing);
×
704
}
×
705

706
void DataViewer_Widget::_handleVerticalSpacingChanged(double spacing) {
29✔
707
    ui->openGLWidget->setVerticalSpacing(static_cast<float>(spacing));
29✔
708

709
    // Also update PlottingManager vertical scale
710
    if (_plotting_manager) {
29✔
711
        // Convert spacing to a scale factor relative to default (0.1f)
712
        float const scale_factor = static_cast<float>(spacing) / 0.1f;
29✔
713
        _plotting_manager->setGlobalVerticalScale(scale_factor);
29✔
714

715
        // Apply updated positions to all registered series
716
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
717
        for (auto const & key: analog_keys) {
81✔
718
            _applyPlottingManagerAllocation(key);
52✔
719
        }
720
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
721
        for (auto const & key: event_keys) {
53✔
722
            _applyPlottingManagerAllocation(key);
24✔
723
        }
724
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
725
        for (auto const & key: interval_keys) {
29✔
726
            _applyPlottingManagerAllocation(key);
×
727
        }
728

729
        // Trigger canvas update
730
        ui->openGLWidget->updateCanvas();
29✔
731
    }
29✔
732
}
29✔
733

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

737
    if (key.empty()) {
5✔
738
        std::cerr << "Error: empty key in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
739
        return;
×
740
    }
741

742
    if (!_data_manager) {
5✔
743
        std::cerr << "Error: null data manager in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
744
        return;
×
745
    }
746

747
    // Get color from model
748
    std::string color = _feature_tree_model->getFeatureColor(key);
5✔
749
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
5✔
750

751
    auto data_type = _data_manager->getType(key);
5✔
752
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
5✔
753

754
    if (data_type == DM_DataType::Analog) {
5✔
755
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
5✔
756
        if (!series) {
5✔
757
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
758
            return;
×
759
        }
760

761
        auto time_key = _data_manager->getTimeKey(key);
5✔
762
        auto time_frame = _data_manager->getTime(time_key);
5✔
763
        if (!time_frame) {
5✔
764
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
765
            return;
×
766
        }
767

768
        // Register with plotting manager for later allocation (single call)
769
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
5✔
770
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
5✔
771

772
    } else if (data_type == DM_DataType::DigitalEvent) {
5✔
773
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
774
        if (!series) {
×
775
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
776
            return;
×
777
        }
778

779
        auto time_key = _data_manager->getTimeKey(key);
×
780
        auto time_frame = _data_manager->getTime(time_key);
×
781
        if (!time_frame) {
×
782
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
783
            return;
×
784
        }
785
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
786
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
×
787

788
    } else if (data_type == DM_DataType::DigitalInterval) {
×
789
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
790
        if (!series) {
×
791
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
792
            return;
×
793
        }
794

795
        auto time_key = _data_manager->getTimeKey(key);
×
796
        auto time_frame = _data_manager->getTime(time_key);
×
797
        if (!time_frame) {
×
798
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
799
            return;
×
800
        }
801
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
802
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
803

804
    } else {
×
805
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
806
        return;
×
807
    }
808

809
    // Note: No canvas update triggered - this is for batch operations
810
    std::cout << "Successfully added series to OpenGL widget (batch mode)" << std::endl;
5✔
811
}
5✔
812

813
void DataViewer_Widget::_removeSelectedFeatureWithoutUpdate(std::string const & key) {
×
814
    std::cout << "Attempting to remove feature (batch): " << key << std::endl;
×
815

816
    if (key.empty()) {
×
817
        std::cerr << "Error: empty key in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
818
        return;
×
819
    }
820

821
    if (!_data_manager) {
×
822
        std::cerr << "Error: null data manager in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
823
        return;
×
824
    }
825

826
    auto data_type = _data_manager->getType(key);
×
827

828
    if (data_type == DM_DataType::Analog) {
×
829
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
830
    } else if (data_type == DM_DataType::DigitalEvent) {
×
831
        ui->openGLWidget->removeDigitalEventSeries(key);
×
832
    } else if (data_type == DM_DataType::DigitalInterval) {
×
833
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
834
    } else {
835
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
836
        return;
×
837
    }
838

839
    // Note: No canvas update triggered - this is for batch operations
840
    std::cout << "Successfully removed series from OpenGL widget (batch mode)" << std::endl;
×
841
}
842

843
void DataViewer_Widget::_calculateOptimalScaling(std::vector<std::string> const & group_keys) {
×
844
    if (group_keys.empty()) {
×
845
        return;
×
846
    }
847

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

850
    // Get current canvas dimensions
851
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
852
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
853

854
    // Count total number of currently visible analog series (including the new group)
855
    int total_visible_analog_series = static_cast<int>(group_keys.size());
×
856

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

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

875
    if (total_visible_analog_series <= 0) {
×
876
        return;// No series to scale
×
877
    }
878

879
    // Calculate optimal vertical spacing
880
    // Leave some margin at top and bottom (10% each = 20% total)
881
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
882
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_analog_series);
×
883

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

888
    // Clamp to reasonable bounds
889
    float const min_spacing = 0.01f;
×
890
    float const max_spacing = 1.0f;
×
891
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
892

893
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
894
              << final_spacing << " normalized units" << std::endl;
×
895

896
    // Calculate optimal global gain based on standard deviations
897
    std::vector<float> std_devs;
×
898
    std_devs.reserve(group_keys.size());
×
899

900
    for (auto const & key: group_keys) {
×
901
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
×
902
        if (series) {
×
903
            float const std_dev = calculate_std_dev_approximate(*series);
×
904
            std_devs.push_back(std_dev);
×
905
            std::cout << "Series " << key << " std dev: " << std_dev << std::endl;
×
906
        }
907
    }
×
908

909
    if (!std_devs.empty()) {
×
910
        // Use the median standard deviation as reference for scaling
911
        std::sort(std_devs.begin(), std_devs.end());
×
912
        float const median_std_dev = std_devs[std_devs.size() / 2];
×
913

914
        // Calculate optimal global scale
915
        // Target: each series should use about 60% of its allocated vertical space
916
        float const target_amplitude_fraction = 0.6f;
×
917
        float const target_amplitude_in_pixels = optimal_spacing * target_amplitude_fraction;
×
918

919
        // Convert to normalized coordinates (3 standard deviations should fit in target amplitude)
920
        float const target_amplitude_normalized = (target_amplitude_in_pixels / static_cast<float>(canvas_height)) * 2.0f;
×
921
        float const three_sigma_target = target_amplitude_normalized;
×
922

923
        // Calculate scale factor needed
924
        float const optimal_global_scale = three_sigma_target / (3.0f * median_std_dev);
×
925

926
        // Clamp to reasonable bounds
927
        float const min_scale = 0.1f;
×
928
        float const max_scale = 100.0f;
×
929
        float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
×
930

931
        std::cout << "Median std dev: " << median_std_dev
×
932
                  << ", target amplitude: " << target_amplitude_in_pixels << " pixels"
×
933
                  << ", optimal global scale: " << final_scale << std::endl;
×
934

935
        // Apply the calculated settings
936
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
937
        ui->global_zoom->setValue(static_cast<double>(final_scale));
×
938

939
        std::cout << "Applied auto-scaling: vertical spacing = " << final_spacing
×
940
                  << ", global scale = " << final_scale << std::endl;
×
941

942
    } else {
943
        // If we can't calculate standard deviations, just apply spacing
944
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
945
        std::cout << "Applied auto-spacing only: vertical spacing = " << final_spacing << std::endl;
×
946
    }
947
}
×
948

949
void DataViewer_Widget::_calculateOptimalEventSpacing(std::vector<std::string> const & group_keys) {
×
950
    if (group_keys.empty()) {
×
951
        return;
×
952
    }
953

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

956
    // Get current canvas dimensions
957
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
958
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
×
959

960
    // Count total number of currently visible digital event series (including the new group)
961
    int total_visible_event_series = static_cast<int>(group_keys.size());
×
962

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

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

981
    if (total_visible_event_series <= 0) {
×
982
        return;// No series to scale
×
983
    }
984

985
    // Calculate optimal vertical spacing
986
    // Leave some margin at top and bottom (10% each = 20% total)
987
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
988
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_event_series);
×
989

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

994
    // Clamp to reasonable bounds
995
    float const min_spacing = 0.01f;
×
996
    float const max_spacing = 1.0f;
×
997
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
998

999
    // Calculate optimal event height (keep events clearly within their lane)
1000
    // Use a conservative fraction of spacing so multiple stacked series remain visually distinct
NEW
1001
    float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
×
1002
    float const min_height = 0.01f;
×
1003
    float const max_height = 0.5f;
×
1004
    float const final_height = std::clamp(optimal_event_height, min_height, max_height);
×
1005

1006
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
1007
              << final_spacing << " normalized units" << std::endl;
×
1008
    std::cout << "Calculated event height: " << final_height << " normalized units" << std::endl;
×
1009

1010
    // Apply the calculated settings to all event series in the group
1011
    for (auto const & key: group_keys) {
×
1012
        auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
1013
        if (config.has_value()) {
×
1014
            config.value()->vertical_spacing = final_spacing;
×
1015
            config.value()->event_height = final_height;
×
1016
            config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
×
1017
        }
1018
    }
1019

1020
    std::cout << "Applied auto-calculated event spacing: spacing = " << final_spacing
×
1021
              << ", height = " << final_height << std::endl;
×
1022
}
×
1023

1024
void DataViewer_Widget::autoArrangeVerticalSpacing() {
29✔
1025
    std::cout << "DataViewer_Widget: Auto-arranging with plotting manager..." << std::endl;
29✔
1026

1027
    // Update dimensions first
1028
    _updatePlottingManagerDimensions();
29✔
1029

1030
    // Apply new allocations to all registered series
1031
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
1032
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
1033
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
1034

1035
    for (auto const & key: analog_keys) {
81✔
1036
        _applyPlottingManagerAllocation(key);
52✔
1037
    }
1038
    for (auto const & key: event_keys) {
53✔
1039
        _applyPlottingManagerAllocation(key);
24✔
1040
    }
1041
    for (auto const & key: interval_keys) {
29✔
1042
        _applyPlottingManagerAllocation(key);
×
1043
    }
1044

1045
    // Calculate and apply optimal scaling to fill the canvas
1046
    _autoFillCanvas();
29✔
1047

1048
    // Update OpenGL widget view bounds based on content height
1049
    _updateViewBounds();
29✔
1050

1051
    // Trigger canvas update to show new positions
1052
    ui->openGLWidget->updateCanvas();
29✔
1053

1054
    auto total_keys = analog_keys.size() + event_keys.size() + interval_keys.size();
29✔
1055
    std::cout << "DataViewer_Widget: Auto-arrange completed for " << total_keys << " series" << std::endl;
29✔
1056
}
58✔
1057

1058
void DataViewer_Widget::_updateViewBounds() {
29✔
1059
    if (!_plotting_manager) {
29✔
1060
        return;
×
1061
    }
1062

1063
    // PlottingManager uses normalized coordinates, so view bounds are typically -1 to +1
1064
    // For now, use standard bounds but this enables future enhancement
1065
    std::cout << "DataViewer_Widget: Using standard view bounds with PlottingManager" << std::endl;
29✔
1066
}
1067

1068
std::string DataViewer_Widget::_convertDataType(DM_DataType dm_type) const {
×
1069
    switch (dm_type) {
×
1070
        case DM_DataType::Analog:
×
1071
            return "Analog";
×
1072
        case DM_DataType::DigitalEvent:
×
1073
            return "DigitalEvent";
×
1074
        case DM_DataType::DigitalInterval:
×
1075
            return "DigitalInterval";
×
1076
        default:
×
1077
            // For unsupported types, default to Analog
1078
            // This should be rare in practice given our type filters
1079
            std::cerr << "Warning: Unsupported data type " << convert_data_type_to_string(dm_type)
×
1080
                      << " defaulting to Analog for plotting manager" << std::endl;
×
1081
            return "Analog";
×
1082
    }
1083
}
1084

1085
void DataViewer_Widget::_updatePlottingManagerDimensions() {
47✔
1086
    if (!_plotting_manager) {
47✔
1087
        return;
×
1088
    }
1089

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

1093
    // PlottingManager works in normalized device coordinates, so no specific dimension update needed
1094
    // But we could update viewport bounds if needed in the future
1095

1096
    std::cout << "DataViewer_Widget: Updated plotting manager dimensions: "
47✔
1097
              << canvas_width << "x" << canvas_height << " pixels" << std::endl;
47✔
1098
}
1099

1100
void DataViewer_Widget::_applyPlottingManagerAllocation(std::string const & series_key) {
243✔
1101
    if (!_plotting_manager) {
243✔
1102
        return;
×
1103
    }
1104

1105
    auto data_type = _data_manager->getType(series_key);
243✔
1106

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

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

1112
    // Apply positioning based on data type
1113
    if (data_type == DM_DataType::Analog) {
243✔
1114
        auto config = ui->openGLWidget->getAnalogConfig(series_key);
178✔
1115
        if (config.has_value()) {
178✔
1116
            float yc, yh;
178✔
1117
            if (_plotting_manager->getAnalogSeriesAllocationForKey(series_key, yc, yh)) {
178✔
1118
                config.value()->allocated_y_center = yc;
178✔
1119
                config.value()->allocated_height = yh;
178✔
1120
            }
1121
        }
1122

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

1130
    } else if (data_type == DM_DataType::DigitalInterval) {
×
1131
        auto config = ui->openGLWidget->getDigitalIntervalConfig(series_key);
×
1132
        if (config.has_value()) {
×
1133
            // Basic allocation - will be properly implemented when OpenGL widget is updated
1134
            std::cout << "  Applied basic allocation to interval '" << series_key << "'" << std::endl;
×
1135
        }
1136
    }
1137
}
1138

1139
// ===== Context menu and configuration handling =====
1140
void DataViewer_Widget::_showGroupContextMenu(std::string const & group_name, QPoint const & global_pos) {
×
1141
    QMenu menu;
×
1142
    QMenu * loadMenu = menu.addMenu("Load configuration");
×
1143
    QAction * loadSpikeSorter = loadMenu->addAction("spikesorter configuration");
×
1144
    QAction * clearConfig = menu.addAction("Clear configuration");
×
1145

1146
    connect(loadSpikeSorter, &QAction::triggered, this, [this, group_name]() {
×
1147
        _loadSpikeSorterConfigurationForGroup(QString::fromStdString(group_name));
×
1148
    });
×
1149
    connect(clearConfig, &QAction::triggered, this, [this, group_name]() {
×
1150
        _clearConfigurationForGroup(QString::fromStdString(group_name));
×
1151
    });
×
1152

1153
    menu.exec(global_pos);
×
1154
}
×
1155

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

1167
    // Re-apply allocation to visible analog keys and update
1168
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
1169
    for (auto const & key : analog_keys) {
×
1170
        _applyPlottingManagerAllocation(key);
×
1171
    }
1172
    ui->openGLWidget->updateCanvas();
×
1173
}
×
1174

1175
void DataViewer_Widget::_clearConfigurationForGroup(QString const & group_name) {
×
1176
    _plotting_manager->clearAnalogGroupConfiguration(group_name.toStdString());
×
1177
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
1178
    for (auto const & key : analog_keys) {
×
1179
        _applyPlottingManagerAllocation(key);
×
1180
    }
1181
    ui->openGLWidget->updateCanvas();
×
1182
}
×
1183

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

1203
void DataViewer_Widget::_loadSpikeSorterConfigurationFromText(QString const & group_name, QString const & text) {
1✔
1204
    auto positions = _parseSpikeSorterConfig(text.toStdString());
1✔
1205
    if (positions.empty()) {
1✔
1206
        std::cout << "No positions found in spike sorter configuration" << std::endl;
×
1207
        return;
×
1208
    }
1209
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
1✔
1210
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
1✔
1211
    for (auto const & key : analog_keys) {
5✔
1212
        _applyPlottingManagerAllocation(key);
4✔
1213
    }
1214
    ui->openGLWidget->updateCanvas();
1✔
1215
}
1✔
1216

1217
void DataViewer_Widget::_autoFillCanvas() {
29✔
1218
    std::cout << "DataViewer_Widget: Auto-filling canvas with PlottingManager..." << std::endl;
29✔
1219

1220
    if (!_plotting_manager) {
29✔
1221
        std::cout << "No plotting manager available" << std::endl;
×
1222
        return;
×
1223
    }
1224

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

1229
    // Count visible series using PlottingManager
1230
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
1231
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
1232
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
1233

1234
    int visible_analog_count = static_cast<int>(analog_keys.size());
29✔
1235
    int visible_event_count = static_cast<int>(event_keys.size());
29✔
1236
    int visible_interval_count = static_cast<int>(interval_keys.size());
29✔
1237

1238
    int total_visible = visible_analog_count + visible_event_count + visible_interval_count;
29✔
1239
    std::cout << "Visible series: " << visible_analog_count << " analog, "
29✔
1240
              << visible_event_count << " events, " << visible_interval_count
29✔
1241
              << " intervals (total: " << total_visible << ")" << std::endl;
29✔
1242

1243
    if (total_visible == 0) {
29✔
1244
        std::cout << "No visible series to auto-scale" << std::endl;
×
1245
        return;
×
1246
    }
1247

1248
    // Calculate optimal vertical spacing to fill canvas
1249
    // Use 90% of canvas height, leaving 5% margin at top and bottom
1250
    float const usable_height = static_cast<float>(canvas_height) * 0.9f;
29✔
1251
    float const optimal_spacing_pixels = usable_height / static_cast<float>(total_visible);
29✔
1252

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

1256
    // Clamp to reasonable bounds
1257
    float const min_spacing = 0.02f;
29✔
1258
    float const max_spacing = 1.5f;
29✔
1259
    float const final_spacing = std::clamp(optimal_spacing_normalized, min_spacing, max_spacing);
29✔
1260

1261
    std::cout << "Calculated optimal spacing: " << optimal_spacing_pixels << " pixels -> "
29✔
1262
              << final_spacing << " normalized units" << std::endl;
29✔
1263

1264
    // Apply the calculated vertical spacing
1265
    ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
29✔
1266

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

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

1275
        // Apply optimal height to all visible digital event series
1276
        for (auto const & key: event_keys) {
35✔
1277
            auto config = ui->openGLWidget->getDigitalEventConfig(key);
24✔
1278
            if (config.has_value() && config.value()->is_visible) {
24✔
1279
                config.value()->event_height = optimal_event_height;
24✔
1280
                config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
24✔
1281
                std::cout << "  Applied event height " << optimal_event_height
24✔
1282
                          << " to series '" << key << "'" << std::endl;
24✔
1283
            }
1284
        }
1285
    }
1286

1287
    // Calculate and apply optimal interval heights for digital interval series
1288
    if (visible_interval_count > 0) {
29✔
1289
        // Calculate optimal interval height to fill most of the allocated space
1290
        // Use 80% of the spacing to leave some visual separation between intervals
1291
        float const optimal_interval_height = final_spacing * 0.8f;
×
1292

1293
        std::cout << "Calculated optimal interval height: " << optimal_interval_height << " normalized units" << std::endl;
×
1294

1295
        // Apply optimal height to all visible digital interval series
1296
        for (auto const & key: interval_keys) {
×
1297
            auto config = ui->openGLWidget->getDigitalIntervalConfig(key);
×
1298
            if (config.has_value() && config.value()->is_visible) {
×
1299
                config.value()->interval_height = optimal_interval_height;
×
1300
                std::cout << "  Applied interval height " << optimal_interval_height
×
1301
                          << " to series '" << key << "'" << std::endl;
×
1302
            }
1303
        }
1304
    }
1305

1306
    // Calculate optimal global scale for analog series to use their allocated space effectively
1307
    if (visible_analog_count > 0) {
29✔
1308
        // Sample a few analog series to estimate appropriate scaling
1309
        std::vector<float> sample_std_devs;
22✔
1310
        sample_std_devs.reserve(std::min(5, visible_analog_count));// Sample up to 5 series
22✔
1311

1312
        int sampled = 0;
22✔
1313
        for (auto const & key: analog_keys) {
74✔
1314
            if (sampled >= 5) break;
52✔
1315

1316
            auto config = ui->openGLWidget->getAnalogConfig(key);
52✔
1317
            if (config.has_value() && config.value()->is_visible) {
52✔
1318
                auto series = _data_manager->getData<AnalogTimeSeries>(key);
52✔
1319
                if (series) {
52✔
1320
                    float std_dev = calculate_std_dev_approximate(*series);
52✔
1321
                    if (std_dev > 0.0f) {
52✔
1322
                        sample_std_devs.push_back(std_dev);
52✔
1323
                        sampled++;
52✔
1324
                    }
1325
                }
1326
            }
52✔
1327
        }
1328

1329
        if (!sample_std_devs.empty()) {
22✔
1330
            // Use median standard deviation for scaling calculation
1331
            std::sort(sample_std_devs.begin(), sample_std_devs.end());
22✔
1332
            float median_std_dev = sample_std_devs[sample_std_devs.size() / 2];
22✔
1333

1334
            // Calculate scale so that ±3 standard deviations use ~60% of allocated space
1335
            float const target_amplitude_fraction = 0.6f;
22✔
1336
            float const target_amplitude_pixels = optimal_spacing_pixels * target_amplitude_fraction;
22✔
1337
            float const target_amplitude_normalized = (target_amplitude_pixels / static_cast<float>(canvas_height)) * 2.0f;
22✔
1338

1339
            // For ±3σ coverage
1340
            float const three_sigma_coverage = target_amplitude_normalized;
22✔
1341
            float const optimal_global_scale = three_sigma_coverage / (6.0f * median_std_dev);
22✔
1342

1343
            // Clamp to reasonable bounds
1344
            float const min_scale = 0.01f;
22✔
1345
            float const max_scale = 100.0f;
22✔
1346
            float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
22✔
1347

1348
            std::cout << "Calculated optimal global scale: median_std_dev=" << median_std_dev
22✔
1349
                      << ", target_amplitude=" << target_amplitude_pixels << " pixels"
22✔
1350
                      << ", final_scale=" << final_scale << std::endl;
22✔
1351

1352
            // Apply the calculated global scale
1353
            ui->global_zoom->setValue(static_cast<double>(final_scale));
22✔
1354
        }
1355
    }
22✔
1356

1357
    std::cout << "Auto-fill canvas completed" << std::endl;
29✔
1358
}
29✔
1359

1360
void DataViewer_Widget::cleanupDeletedData() {
15✔
1361
    if (!_data_manager) {
15✔
1362
        return;
×
1363
    }
1364

1365
    // Collect keys that no longer exist in DataManager
1366
    std::vector<std::string> keys_to_cleanup;
15✔
1367

1368
    if (_plotting_manager) {
15✔
1369
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
15✔
1370
        for (auto const & key: analog_keys) {
18✔
1371
            if (!_data_manager->getData<AnalogTimeSeries>(key)) {
3✔
1372
                keys_to_cleanup.push_back(key);
×
1373
            }
1374
        }
1375
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
15✔
1376
        for (auto const & key: event_keys) {
15✔
1377
            if (!_data_manager->getData<DigitalEventSeries>(key)) {
×
1378
                keys_to_cleanup.push_back(key);
×
1379
            }
1380
        }
1381
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
15✔
1382
        for (auto const & key: interval_keys) {
15✔
1383
            if (!_data_manager->getData<DigitalIntervalSeries>(key)) {
×
1384
                keys_to_cleanup.push_back(key);
×
1385
            }
1386
        }
1387
    }
15✔
1388

1389
    if (keys_to_cleanup.empty()) {
15✔
1390
        return;
15✔
1391
    }
1392

1393
    // De-duplicate keys in case the same key appears in multiple lists
1394
    std::sort(keys_to_cleanup.begin(), keys_to_cleanup.end());
×
1395
    keys_to_cleanup.erase(std::unique(keys_to_cleanup.begin(), keys_to_cleanup.end()), keys_to_cleanup.end());
×
1396

1397
    // Post cleanup to OpenGLWidget's thread safely
1398
    QPointer<OpenGLWidget> glw = ui ? ui->openGLWidget : nullptr;
×
1399
    if (glw) {
×
1400
        QMetaObject::invokeMethod(glw, [glw, keys = keys_to_cleanup]() {
×
1401
            if (!glw) return;
×
1402
            for (auto const & key : keys) {
×
1403
                glw->removeAnalogTimeSeries(key);
×
1404
                glw->removeDigitalEventSeries(key);
×
1405
                glw->removeDigitalIntervalSeries(key);
×
1406
            } }, Qt::QueuedConnection);
1407
    }
1408

1409
    // Remove from PlottingManager defensively (all types) on our thread
1410
    if (_plotting_manager) {
×
1411
        for (auto const & key: keys_to_cleanup) {
×
1412
            (void) _plotting_manager->removeAnalogSeries(key);
×
1413
            (void) _plotting_manager->removeDigitalEventSeries(key);
×
1414
            (void) _plotting_manager->removeDigitalIntervalSeries(key);
×
1415
        }
1416
    }
1417

1418
    // Re-arrange remaining data
1419
    autoArrangeVerticalSpacing();
×
1420
}
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