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

paulmthompson / WhiskerToolbox / 17471187288

04 Sep 2025 04:55PM UTC coverage: 70.97% (+0.1%) from 70.849%
17471187288

push

github

paulmthompson
fix failing test from old behavior

34456 of 48550 relevant lines covered (70.97%)

1301.42 hits per line

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

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

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

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

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

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

79
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeature, this, [this](std::string const & feature) {
16✔
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);
16✔
86
    connect(ui->feature_tree_widget->treeWidget(), &QTreeWidget::customContextMenuRequested, this, [this](QPoint const & pos) {
16✔
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) {
16✔
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) {
16✔
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) {
16✔
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);
16✔
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) {
16✔
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);
16✔
171

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

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

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

178
    //We should alwasy get the master clock because we plot
179
    // Check for master clock
180
    auto time_keys = _data_manager->getTimeFrameKeys();
16✔
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()) {
16✔
183
        std::cout << "No master clock found in DataManager" << std::endl;
16✔
184
        _time_frame = _data_manager->getTime(TimeKey("time"));
16✔
185
    } else {
186
        _time_frame = _data_manager->getTime(TimeKey("master"));
×
187
    }
188

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

192
    // Set the master time frame for proper coordinate conversion
193
    ui->openGLWidget->setMasterTimeFrame(_time_frame);
16✔
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());
16✔
197
    std::cout << "Setting x_axis_samples maximum to " << data_range << std::endl;
16✔
198
    ui->x_axis_samples->setMaximum(data_range);
16✔
199

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

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

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

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

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

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

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

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

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

239
DataViewer_Widget::~DataViewer_Widget() {
31✔
240
    delete ui;
16✔
241
}
31✔
242

243
void DataViewer_Widget::openWidget() {
14✔
244
    std::cout << "DataViewer Widget Opened" << std::endl;
14✔
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();
14✔
249

250
    this->show();
14✔
251
    _updateLabels();
14✔
252
}
14✔
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) {
14✔
261
    QWidget::resizeEvent(event);
14✔
262

263
    // Update plotting manager dimensions when widget is resized
264
    _updatePlottingManagerDimensions();
14✔
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) {
14✔
269
        ui->openGLWidget->update();
14✔
270
    }
271
}
14✔
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) {
13✔
284
    std::cout << "Feature toggle signal received: " << feature.toStdString() << " enabled: " << enabled << std::endl;
13✔
285

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

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

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

301
    if (!_data_manager) {
13✔
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);
13✔
308
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
13✔
309

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

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

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

320
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
13✔
321
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
13✔
322
        if (!series) {
13✔
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);
13✔
329
        std::cout << "Time frame key: " << time_key << std::endl;
13✔
330
        auto time_frame = _data_manager->getTime(time_key);
13✔
331
        if (!time_frame) {
13✔
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;
13✔
337

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

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

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

346
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
×
347
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
348
        if (!series) {
×
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);
×
354
        auto time_frame = _data_manager->getTime(time_key);
×
355
        if (!time_frame) {
×
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);
×
362

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

365
    } else if (data_type == DM_DataType::DigitalInterval) {
×
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) {
13✔
393
        _applyPlottingManagerAllocation(key);
13✔
394
    }
395

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

402
    std::cout << "Series addition and auto-arrangement completed" << std::endl;
13✔
403
    // Trigger canvas update to show the new series
404
    if (!_is_batch_add) {
13✔
405
        std::cout << "Triggering canvas update" << std::endl;
13✔
406
        ui->openGLWidget->updateCanvas();
13✔
407
    }
408
    std::cout << "Canvas update completed" << std::endl;
13✔
409
}
13✔
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) {
15✔
542
    ui->openGLWidget->setGlobalScale(static_cast<float>(scale));
15✔
543

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

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

562
        // Trigger canvas update
563
        ui->openGLWidget->updateCanvas();
15✔
564
    }
15✔
565
}
15✔
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() {
15✔
609
    auto x_axis = ui->openGLWidget->getXAxis();
15✔
610
    ui->neg_x_label->setText(QString::number(x_axis.getStart()));
15✔
611
    ui->pos_x_label->setText(QString::number(x_axis.getEnd()));
15✔
612
}
15✔
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 {
43✔
673
    return ui->openGLWidget->getAnalogConfig(key);
43✔
674
}
675

676
std::optional<NewDigitalEventSeriesDisplayOptions *> DataViewer_Widget::getDigitalEventConfig(std::string const & key) const {
×
677
    return ui->openGLWidget->getDigitalEventConfig(key);
×
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) {
14✔
707
    ui->openGLWidget->setVerticalSpacing(static_cast<float>(spacing));
14✔
708

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

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

729
        // Trigger canvas update
730
        ui->openGLWidget->updateCanvas();
14✔
731
    }
14✔
732
}
14✔
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 (should be smaller than spacing to avoid overlap)
1000
    float const optimal_event_height = final_spacing * 0.6f;// 60% of spacing for visible separation
×
1001
    float const min_height = 0.01f;
×
1002
    float const max_height = 0.5f;
×
1003
    float const final_height = std::clamp(optimal_event_height, min_height, max_height);
×
1004

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

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

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

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

1026
    // Update dimensions first
1027
    _updatePlottingManagerDimensions();
14✔
1028

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

1034
    for (auto const & key: analog_keys) {
51✔
1035
        _applyPlottingManagerAllocation(key);
37✔
1036
    }
1037
    for (auto const & key: event_keys) {
14✔
1038
        _applyPlottingManagerAllocation(key);
×
1039
    }
1040
    for (auto const & key: interval_keys) {
14✔
1041
        _applyPlottingManagerAllocation(key);
×
1042
    }
1043

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

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

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

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

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

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

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

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

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

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

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

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

1104
    auto data_type = _data_manager->getType(series_key);
129✔
1105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1266
    // Calculate and apply optimal event heights for digital event series
1267
    if (visible_event_count > 0) {
14✔
1268
        // Calculate optimal event height to fill most of the allocated space
1269
        // Use 80% of the spacing to leave some visual separation between events
1270
        float const optimal_event_height = final_spacing * 0.8f;
×
1271

1272
        std::cout << "Calculated optimal event height: " << optimal_event_height << " normalized units" << std::endl;
×
1273

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

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

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

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

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

1311
        int sampled = 0;
14✔
1312
        for (auto const & key: analog_keys) {
51✔
1313
            if (sampled >= 5) break;
37✔
1314

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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