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

paulmthompson / WhiskerToolbox / 18888855713

28 Oct 2025 06:31PM UTC coverage: 73.01% (-0.05%) from 73.058%
18888855713

push

github

paulmthompson
horizontal scale bar export added

1 of 69 new or added lines in 3 files covered. (1.45%)

318 existing lines in 3 files now uncovered.

56336 of 77162 relevant lines covered (73.01%)

44772.2 hits per line

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

46.44
/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 "SVGExporter.hpp"
20
#include "TimeFrame/TimeFrame.hpp"
21
#include "TimeScrollBar/TimeScrollBar.hpp"
22

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

27
#include <QMetaObject>
28
#include <QPointer>
29
#include <QTableWidget>
30
#include <QWheelEvent>
31
#include <QMenu>
32
#include <QFileDialog>
33
#include <QMessageBox>
34
#include <QFile>
35
#include <QTextStream>
36

37
#include <algorithm>
38
#include <cmath>
39
#include <iostream>
40
#include <sstream>
41

42
DataViewer_Widget::DataViewer_Widget(std::shared_ptr<DataManager> data_manager,
23✔
43
                                     TimeScrollBar * time_scrollbar,
44
                                     MainWindow * main_window,
45
                                     QWidget * parent)
23✔
46
    : QWidget(parent),
47
      _data_manager{std::move(data_manager)},
23✔
48
      _time_scrollbar{time_scrollbar},
23✔
49
      _main_window{main_window},
23✔
50
      ui(new Ui::DataViewer_Widget) {
46✔
51

52
    ui->setupUi(this);
23✔
53

54
    // Initialize plotting manager with default viewport
55
    _plotting_manager = std::make_unique<PlottingManager>();
23✔
56

57
    // Provide PlottingManager reference to OpenGL widget
58
    ui->openGLWidget->setPlottingManager(_plotting_manager.get());
23✔
59

60
    // Initialize feature tree model
61
    _feature_tree_model = std::make_unique<Feature_Tree_Model>(this);
23✔
62
    _feature_tree_model->setDataManager(_data_manager);
23✔
63

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

73
    // Configure Feature_Tree_Widget
74
    ui->feature_tree_widget->setTypeFilters({DM_DataType::Analog, DM_DataType::DigitalEvent, DM_DataType::DigitalInterval});
69✔
75
    ui->feature_tree_widget->setDataManager(_data_manager);
23✔
76

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

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

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

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

116
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::addFeatures, this, [this](std::vector<std::string> const & features) {
23✔
117
        std::cout << "Adding " << features.size() << " features as group" << std::endl;
2✔
118

119
        // Mark batch add to suppress per-series auto-arrange
120
        _is_batch_add = true;
2✔
121
        // Process all features in the group without triggering individual canvas updates
122
        for (auto const & key: features) {
12✔
123
            _plotSelectedFeatureWithoutUpdate(key);
10✔
124
        }
125
        _is_batch_add = false;
2✔
126

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

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

140
    connect(ui->feature_tree_widget, &Feature_Tree_Widget::removeFeatures, this, [this](std::vector<std::string> const & features) {
23✔
141
        std::cout << "Removing " << features.size() << " features as group" << std::endl;
1✔
142

143
        _is_batch_add = true;
1✔
144
        // Process all features in the group without triggering individual canvas updates
145
        for (auto const & key: features) {
6✔
146
            _removeSelectedFeatureWithoutUpdate(key);
5✔
147
        }
148
        _is_batch_add = false;
1✔
149

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

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

163
    // Connect color change signals from the model
164
    connect(_feature_tree_model.get(), &Feature_Tree_Model::featureColorChanged, this, &DataViewer_Widget::_handleColorChanged);
23✔
165

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

173
    connect(ui->x_axis_samples, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleXAxisSamplesChanged);
23✔
174

175
    connect(ui->global_zoom, &QDoubleSpinBox::valueChanged, this, &DataViewer_Widget::_updateGlobalScale);
23✔
176

177
    connect(ui->theme_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &DataViewer_Widget::_handleThemeChanged);
23✔
178

179
    connect(time_scrollbar, &TimeScrollBar::timeChanged, this, &DataViewer_Widget::_updatePlot);
23✔
180

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

192
    std::cout << "Setting GL limit to " << _time_frame->getTotalFrameCount() << std::endl;
23✔
193
    ui->openGLWidget->setXLimit(_time_frame->getTotalFrameCount());
23✔
194

195
    // Set the master time frame for proper coordinate conversion
196
    ui->openGLWidget->setMasterTimeFrame(_time_frame);
23✔
197

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

203
    // Setup stacked widget with data-type specific viewers
204
    auto analog_widget = new AnalogViewer_Widget(_data_manager, ui->openGLWidget);
23✔
205
    auto interval_widget = new IntervalViewer_Widget(_data_manager, ui->openGLWidget);
23✔
206
    auto event_widget = new EventViewer_Widget(_data_manager, ui->openGLWidget);
23✔
207

208
    ui->stackedWidget->addWidget(analog_widget);
23✔
209
    ui->stackedWidget->addWidget(interval_widget);
23✔
210
    ui->stackedWidget->addWidget(event_widget);
23✔
211

212
    // Connect color change signals from sub-widgets
213
    connect(analog_widget, &AnalogViewer_Widget::colorChanged,
69✔
214
            this, &DataViewer_Widget::_handleColorChanged);
46✔
215
    connect(interval_widget, &IntervalViewer_Widget::colorChanged,
69✔
216
            this, &DataViewer_Widget::_handleColorChanged);
46✔
217
    connect(event_widget, &EventViewer_Widget::colorChanged,
69✔
218
            this, &DataViewer_Widget::_handleColorChanged);
46✔
219

220
    // Connect mouse hover signal from OpenGL widget
221
    connect(ui->openGLWidget, &OpenGLWidget::mouseHover,
69✔
222
            this, &DataViewer_Widget::_updateCoordinateDisplay);
46✔
223

224
    // Grid line connections
225
    connect(ui->grid_lines_enabled, &QCheckBox::toggled, this, &DataViewer_Widget::_handleGridLinesToggled);
23✔
226
    connect(ui->grid_spacing, QOverload<int>::of(&QSpinBox::valueChanged), this, &DataViewer_Widget::_handleGridSpacingChanged);
23✔
227

228
    // Vertical spacing connection
229
    connect(ui->vertical_spacing, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &DataViewer_Widget::_handleVerticalSpacingChanged);
23✔
230

231
    // Auto-arrange button connection
232
    connect(ui->auto_arrange_button, &QPushButton::clicked, this, &DataViewer_Widget::autoArrangeVerticalSpacing);
23✔
233

234
    // Initialize grid line UI to match OpenGLWidget defaults
235
    ui->grid_lines_enabled->setChecked(ui->openGLWidget->getGridLinesEnabled());
23✔
236
    ui->grid_spacing->setValue(ui->openGLWidget->getGridSpacing());
23✔
237

238
    // Initialize vertical spacing UI to match OpenGLWidget defaults
239
    ui->vertical_spacing->setValue(static_cast<double>(ui->openGLWidget->getVerticalSpacing()));
23✔
240

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

265
    // Connect SVG export button
266
    connect(ui->export_svg_button, &QPushButton::clicked, this, &DataViewer_Widget::_exportToSVG);
23✔
267

268
    // Connect scalebar checkbox to enable/disable the length spinbox
269
    connect(ui->svg_scalebar_checkbox, &QCheckBox::toggled, this, [this](bool checked) {
23✔
NEW
270
        ui->scalebar_length_spinbox->setEnabled(checked);
×
NEW
271
        ui->scalebar_length_label->setEnabled(checked);
×
NEW
272
    });
×
273
}
46✔
274

275
DataViewer_Widget::~DataViewer_Widget() {
45✔
276
    delete ui;
23✔
277
}
45✔
278

279
void DataViewer_Widget::openWidget() {
21✔
280
    std::cout << "DataViewer Widget Opened" << std::endl;
21✔
281

282
    // Tree is already populated by observer pattern in setDataManager()
283
    // Trigger refresh in case of manual opening
284
    ui->feature_tree_widget->refreshTree();
21✔
285

286
    this->show();
21✔
287
    _updateLabels();
21✔
288
}
21✔
289

UNCOV
290
void DataViewer_Widget::closeEvent(QCloseEvent * event) {
×
291
    static_cast<void>(event);
292

UNCOV
293
    std::cout << "Close event detected" << std::endl;
×
UNCOV
294
}
×
295

296
void DataViewer_Widget::resizeEvent(QResizeEvent * event) {
21✔
297
    QWidget::resizeEvent(event);
21✔
298

299
    // Update plotting manager dimensions when widget is resized
300
    _updatePlottingManagerDimensions();
21✔
301

302
    // The OpenGL widget will automatically get its resizeGL called by Qt
303
    // but we can trigger an additional update if needed
304
    if (ui->openGLWidget) {
21✔
305
        ui->openGLWidget->update();
21✔
306
    }
307
}
21✔
308

309
void DataViewer_Widget::_updatePlot(int time) {
1✔
310
    //std::cout << "Time is " << time;
311
    time = _data_manager->getTime(TimeKey("time"))->getTimeAtIndex(TimeFrameIndex(time));
1✔
312
    //std::cout << ""
313
    ui->openGLWidget->updateCanvas(time);
1✔
314

315
    _updateLabels();
1✔
316
}
1✔
317

318

319
void DataViewer_Widget::_addFeatureToModel(QString const & feature, bool enabled) {
35✔
320
    std::cout << "Feature toggle signal received: " << feature.toStdString() << " enabled: " << enabled << std::endl;
35✔
321

322
    if (enabled) {
35✔
323
        _plotSelectedFeature(feature.toStdString());
35✔
324
    } else {
UNCOV
325
        _removeSelectedFeature(feature.toStdString());
×
326
    }
327
}
35✔
328

329
void DataViewer_Widget::_plotSelectedFeature(std::string const & key) {
35✔
330
    std::cout << "Attempting to plot feature: " << key << std::endl;
35✔
331

332
    if (key.empty()) {
35✔
333
        std::cerr << "Error: empty key in _plotSelectedFeature" << std::endl;
×
UNCOV
334
        return;
×
335
    }
336

337
    if (!_data_manager) {
35✔
UNCOV
338
        std::cerr << "Error: null data manager in _plotSelectedFeature" << std::endl;
×
UNCOV
339
        return;
×
340
    }
341

342
    // Get color from model
343
    std::string color = _feature_tree_model->getFeatureColor(key);
35✔
344
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
35✔
345

346
    auto data_type = _data_manager->getType(key);
35✔
347
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
35✔
348

349
    // Register with plotting manager for coordinated positioning
350
    if (_plotting_manager) {
35✔
351
        std::cout << "Registering series with plotting manager: " << key << std::endl;
35✔
352
    }
353

354
    if (data_type == DM_DataType::Analog) {
35✔
355

356
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
22✔
357
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
22✔
358
        if (!series) {
22✔
UNCOV
359
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
UNCOV
360
            return;
×
361
        }
362

363

364
        auto time_key = _data_manager->getTimeKey(key);
22✔
365
        std::cout << "Time frame key: " << time_key << std::endl;
22✔
366
        auto time_frame = _data_manager->getTime(time_key);
22✔
367
        if (!time_frame) {
22✔
UNCOV
368
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
369
            return;
×
370
        }
371

372
        std::cout << "Time frame has " << time_frame->getTotalFrameCount() << " frames" << std::endl;
22✔
373

374
        // Add to plotting manager first
375
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
22✔
376

377
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
22✔
378
        std::cout << "Successfully added analog series to PlottingManager and OpenGL widget" << std::endl;
22✔
379

380
    } else if (data_type == DM_DataType::DigitalEvent) {
35✔
381

382
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
11✔
383
        auto series = _data_manager->getData<DigitalEventSeries>(key);
11✔
384
        if (!series) {
11✔
UNCOV
385
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
386
            return;
×
387
        }
388

389
        auto time_key = _data_manager->getTimeKey(key);
11✔
390
        auto time_frame = _data_manager->getTime(time_key);
11✔
391
        if (!time_frame) {
11✔
UNCOV
392
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
UNCOV
393
            return;
×
394
        }
395

396
        // Add to plotting manager first
397
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
11✔
398

399
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
11✔
400

401
    } else if (data_type == DM_DataType::DigitalInterval) {
13✔
402

403
        std::cout << "Adding << " << key << " to PlottingManager and OpenGLWidget" << std::endl;
2✔
404
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
2✔
405
        if (!series) {
2✔
406
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
407
            return;
×
408
        }
409

410
        auto time_key = _data_manager->getTimeKey(key);
2✔
411
        auto time_frame = _data_manager->getTime(time_key);
2✔
412
        if (!time_frame) {
2✔
UNCOV
413
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
414
            return;
×
415
        }
416

417
        // Add to plotting manager first
418
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
2✔
419

420
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
2✔
421

422
    } else {
2✔
UNCOV
423
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
424
        return;
×
425
    }
426

427
    // Apply coordinated plotting manager allocation after adding to OpenGL widget
428
    if (_plotting_manager) {
35✔
429
        _applyPlottingManagerAllocation(key);
35✔
430
    }
431

432
    // Auto-arrange and auto-fill canvas to make optimal use of space
433
    // IMPORTANT: Do not auto-arrange when adding DigitalInterval series, since intervals are
434
    // drawn full-canvas and should not affect analog/event stacking or global zoom.
435
    if (!_is_batch_add) {
35✔
436
        if (data_type == DM_DataType::DigitalInterval) {
35✔
437
            std::cout << "Skipping auto-arrange after adding DigitalInterval to preserve analog zoom" << std::endl;
2✔
438
        } else {
439
            std::cout << "Auto-arranging and filling canvas after adding series" << std::endl;
33✔
440
            autoArrangeVerticalSpacing();// This now includes auto-fill functionality
33✔
441
        }
442
    }
443

444
    std::cout << "Series addition and auto-arrangement completed" << std::endl;
35✔
445
    // Trigger canvas update to show the new series
446
    if (!_is_batch_add) {
35✔
447
        std::cout << "Triggering canvas update" << std::endl;
35✔
448
        ui->openGLWidget->updateCanvas();
35✔
449
    }
450
    std::cout << "Canvas update completed" << std::endl;
35✔
451
}
35✔
452

UNCOV
453
void DataViewer_Widget::_removeSelectedFeature(std::string const & key) {
×
454
    std::cout << "Attempting to remove feature: " << key << std::endl;
×
455

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

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

466
    auto data_type = _data_manager->getType(key);
×
467

468
    // Remove from plotting manager first
UNCOV
469
    if (_plotting_manager) {
×
UNCOV
470
        bool removed = false;
×
471
        if (data_type == DM_DataType::Analog) {
×
472
            removed = _plotting_manager->removeAnalogSeries(key);
×
473
        } else if (data_type == DM_DataType::DigitalEvent) {
×
474
            removed = _plotting_manager->removeDigitalEventSeries(key);
×
475
        } else if (data_type == DM_DataType::DigitalInterval) {
×
476
            removed = _plotting_manager->removeDigitalIntervalSeries(key);
×
477
        }
478
        if (removed) {
×
479
            std::cout << "Unregistered '" << key << "' from plotting manager" << std::endl;
×
480
        }
481
    }
482

483
    if (data_type == DM_DataType::Analog) {
×
484
        ui->openGLWidget->removeAnalogTimeSeries(key);
×
UNCOV
485
    } else if (data_type == DM_DataType::DigitalEvent) {
×
486
        ui->openGLWidget->removeDigitalEventSeries(key);
×
UNCOV
487
    } else if (data_type == DM_DataType::DigitalInterval) {
×
488
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
489
    } else {
UNCOV
490
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
491
        return;
×
492
    }
493

494
    // Auto-arrange and auto-fill canvas to rescale remaining elements
495
    // IMPORTANT: Do not auto-arrange when removing DigitalInterval series; removing an overlay
496
    // interval should not change analog/event stacking or global zoom.
497
    if (data_type == DM_DataType::DigitalInterval) {
×
UNCOV
498
        std::cout << "Skipping auto-arrange after removing DigitalInterval to preserve analog zoom" << std::endl;
×
499
    } else {
500
        std::cout << "Auto-arranging and filling canvas after removing series" << std::endl;
×
501
        autoArrangeVerticalSpacing();// This now includes auto-fill functionality
×
502
    }
503

UNCOV
504
    std::cout << "Series removal and auto-arrangement completed" << std::endl;
×
505
    // Trigger canvas update to reflect the removal
UNCOV
506
    std::cout << "Triggering canvas update after removal" << std::endl;
×
UNCOV
507
    ui->openGLWidget->updateCanvas();
×
508
}
509

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

513
    if (feature.isEmpty()) {
×
514
        std::cerr << "Error: empty feature name in _handleFeatureSelected" << std::endl;
×
515
        return;
×
516
    }
517

518
    if (!_data_manager) {
×
519
        std::cerr << "Error: null data manager in _handleFeatureSelected" << std::endl;
×
UNCOV
520
        return;
×
521
    }
522

UNCOV
523
    _highlighted_available_feature = feature;
×
524

525
    // Switch stacked widget based on data type
526
    auto const type = _data_manager->getType(feature.toStdString());
×
527
    auto key = feature.toStdString();
×
528

529
    std::cout << "Feature type for selection: " << convert_data_type_to_string(type) << std::endl;
×
530

UNCOV
531
    if (type == DM_DataType::Analog) {
×
532
        int const stacked_widget_index = 1;// Analog widget is at index 1 (after empty page)
×
UNCOV
533
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
534
        auto analog_widget = dynamic_cast<AnalogViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
535
        if (analog_widget) {
×
536
            analog_widget->setActiveKey(key);
×
537
            std::cout << "Selected Analog Time Series: " << key << std::endl;
×
538
        } else {
539
            std::cerr << "Error: failed to cast to AnalogViewer_Widget" << std::endl;
×
540
        }
541

UNCOV
542
    } else if (type == DM_DataType::DigitalInterval) {
×
543
        int const stacked_widget_index = 2;// Interval widget is at index 2
×
UNCOV
544
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
545
        auto interval_widget = dynamic_cast<IntervalViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
546
        if (interval_widget) {
×
UNCOV
547
            interval_widget->setActiveKey(key);
×
548
            std::cout << "Selected Digital Interval Series: " << key << std::endl;
×
549
        } else {
550
            std::cerr << "Error: failed to cast to IntervalViewer_Widget" << std::endl;
×
551
        }
552

UNCOV
553
    } else if (type == DM_DataType::DigitalEvent) {
×
UNCOV
554
        int const stacked_widget_index = 3;// Event widget is at index 3
×
UNCOV
555
        ui->stackedWidget->setCurrentIndex(stacked_widget_index);
×
UNCOV
556
        auto event_widget = dynamic_cast<EventViewer_Widget *>(ui->stackedWidget->widget(stacked_widget_index));
×
UNCOV
557
        if (event_widget) {
×
UNCOV
558
            event_widget->setActiveKey(key);
×
UNCOV
559
            std::cout << "Selected Digital Event Series: " << key << std::endl;
×
560
        } else {
UNCOV
561
            std::cerr << "Error: failed to cast to EventViewer_Widget" << std::endl;
×
562
        }
563

564
    } else {
565
        // No specific widget for this type, don't change the current index
UNCOV
566
        std::cout << "Unsupported feature type for detailed view: " << convert_data_type_to_string(type) << std::endl;
×
567
    }
UNCOV
568
}
×
569

570
void DataViewer_Widget::_handleXAxisSamplesChanged(int value) {
2✔
571
    // Use setRangeWidth for spinbox changes (absolute value)
572
    std::cout << "Spinbox requested range width: " << value << std::endl;
2✔
573
    int64_t const actual_range = ui->openGLWidget->setRangeWidth(static_cast<int64_t>(value));
2✔
574
    std::cout << "Actual range width achieved: " << actual_range << std::endl;
2✔
575

576
    // Update the spinbox with the actual range width achieved (in case it was clamped)
577
    if (actual_range != value) {
2✔
578
        std::cout << "Range was clamped, updating spinbox to: " << actual_range << std::endl;
1✔
579
        updateXAxisSamples(static_cast<int>(actual_range));
1✔
580
    }
581
}
2✔
582

583
void DataViewer_Widget::updateXAxisSamples(int value) {
1✔
584
    ui->x_axis_samples->blockSignals(true);
1✔
585
    ui->x_axis_samples->setValue(value);
1✔
586
    ui->x_axis_samples->blockSignals(false);
1✔
587
}
1✔
588

589
void DataViewer_Widget::_updateGlobalScale(double scale) {
29✔
590
    ui->openGLWidget->setGlobalScale(static_cast<float>(scale));
29✔
591

592
    // Also update PlottingManager zoom factor
593
    if (_plotting_manager) {
29✔
594
        _plotting_manager->setGlobalZoom(static_cast<float>(scale));
29✔
595

596
        // Apply updated positions to all registered series
597
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
29✔
598
        for (auto const & key: analog_keys) {
94✔
599
            _applyPlottingManagerAllocation(key);
65✔
600
        }
601
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
29✔
602
        for (auto const & key: event_keys) {
35✔
603
            _applyPlottingManagerAllocation(key);
6✔
604
        }
605
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
29✔
606
        for (auto const & key: interval_keys) {
29✔
UNCOV
607
            _applyPlottingManagerAllocation(key);
×
608
        }
609

610
        // Trigger canvas update
611
        ui->openGLWidget->updateCanvas();
29✔
612
    }
29✔
613
}
29✔
614

615
void DataViewer_Widget::wheelEvent(QWheelEvent * event) {
×
616
    // Disable zooming while dragging intervals
UNCOV
617
    if (ui->openGLWidget->isDraggingInterval()) {
×
618
        return;
×
619
    }
620

UNCOV
621
    auto const numDegrees = static_cast<float>(event->angleDelta().y()) / 8.0f;
×
UNCOV
622
    auto const numSteps = numDegrees / 15.0f;
×
623

624
    auto const current_range = ui->x_axis_samples->value();
×
625

UNCOV
626
    float rangeFactor;
×
627
    if (_zoom_scaling_mode == ZoomScalingMode::Adaptive) {
×
628
        // Adaptive scaling: range factor is proportional to current range width
629
        // This makes adjustments more sensitive when zoomed in (small range), less sensitive when zoomed out (large range)
630
        rangeFactor = static_cast<float>(current_range) * 0.1f;// 10% of current range width
×
631

632
        // Clamp range factor to reasonable bounds
UNCOV
633
        rangeFactor = std::max(1.0f, std::min(rangeFactor, static_cast<float>(_time_frame->getTotalFrameCount()) / 100.0f));
×
634
    } else {
635
        // Fixed scaling (original behavior)
UNCOV
636
        rangeFactor = static_cast<float>(_time_frame->getTotalFrameCount()) / 10000.0f;
×
637
    }
638

639
    // Calculate range delta
640
    // Wheel up (positive numSteps) should zoom IN (decrease range width)
641
    // Wheel down (negative numSteps) should zoom OUT (increase range width)
UNCOV
642
    auto const range_delta = static_cast<int64_t>(-numSteps * rangeFactor);
×
643

644
    // Apply range delta and get the actual achieved range
UNCOV
645
    ui->openGLWidget->changeRangeWidth(range_delta);
×
646

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

651
    // Update spinbox with the actual achieved range (not the requested range)
652
    updateXAxisSamples(actual_range);
×
UNCOV
653
    _updateLabels();
×
654
}
655

656
void DataViewer_Widget::_updateLabels() {
22✔
657
    auto x_axis = ui->openGLWidget->getXAxis();
22✔
658
    ui->neg_x_label->setText(QString::number(x_axis.getStart()));
22✔
659
    ui->pos_x_label->setText(QString::number(x_axis.getEnd()));
22✔
660
}
22✔
661

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

UNCOV
665
    auto const type = _data_manager->getType(feature_key);
×
666

UNCOV
667
    if (type == DM_DataType::Analog) {
×
UNCOV
668
        auto config = ui->openGLWidget->getAnalogConfig(feature_key);
×
669
        if (config.has_value()) {
×
UNCOV
670
            config.value()->hex_color = hex_color;
×
671
        }
672

UNCOV
673
    } else if (type == DM_DataType::DigitalEvent) {
×
674
        auto config = ui->openGLWidget->getDigitalEventConfig(feature_key);
×
UNCOV
675
        if (config.has_value()) {
×
676
            config.value()->hex_color = hex_color;
×
677
        }
678

UNCOV
679
    } else if (type == DM_DataType::DigitalInterval) {
×
680
        auto config = ui->openGLWidget->getDigitalIntervalConfig(feature_key);
×
UNCOV
681
        if (config.has_value()) {
×
UNCOV
682
            config.value()->hex_color = hex_color;
×
683
        }
684
    }
685

686
    // Trigger a redraw
687
    ui->openGLWidget->updateCanvas();
×
688

689
    std::cout << "Color changed for " << feature_key << " to " << hex_color << std::endl;
×
690
}
×
691

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

697
    // Get canvas size for debugging
698
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
×
699

700
    // Use fixed-width formatting to prevent label resizing
701
    // Reserve space for reasonable max values (time: 10 digits, index: 10 digits, Y: 8 chars, canvas: 5x5 digits)
702
    QString coordinate_text;
×
703
    if (series_info.isEmpty()) {
×
UNCOV
704
        coordinate_text = QString("Time: %1  Index: %2  Y: %3  Canvas: %4x%5")
×
UNCOV
705
                                  .arg(actual_time, 10)           // Right-aligned, width 10
×
UNCOV
706
                                  .arg(time_index, 10)             // Right-aligned, width 10
×
UNCOV
707
                                  .arg(canvas_y, 8, 'f', 1)       // Right-aligned, width 8, 1 decimal
×
UNCOV
708
                                  .arg(canvas_width, 5)            // Right-aligned, width 5
×
UNCOV
709
                                  .arg(canvas_height, 5);          // Right-aligned, width 5
×
710
    } else {
711
        // For series info, still use fixed-width for numeric values but allow series info to vary
UNCOV
712
        coordinate_text = QString("Time: %1  Index: %2  %3  Canvas: %4x%5")
×
713
                                  .arg(actual_time, 10)
×
714
                                  .arg(time_index, 10)
×
UNCOV
715
                                  .arg(series_info, -30)           // Left-aligned, min width 30
×
UNCOV
716
                                  .arg(canvas_width, 5)
×
717
                                  .arg(canvas_height, 5);
×
718
    }
719

UNCOV
720
    ui->coordinate_label->setText(coordinate_text);
×
UNCOV
721
}
×
722

723
std::optional<NewAnalogTimeSeriesDisplayOptions *> DataViewer_Widget::getAnalogConfig(std::string const & key) const {
65✔
724
    return ui->openGLWidget->getAnalogConfig(key);
65✔
725
}
726

727
std::optional<NewDigitalEventSeriesDisplayOptions *> DataViewer_Widget::getDigitalEventConfig(std::string const & key) const {
29✔
728
    return ui->openGLWidget->getDigitalEventConfig(key);
29✔
729
}
730

731
std::optional<NewDigitalIntervalSeriesDisplayOptions *> DataViewer_Widget::getDigitalIntervalConfig(std::string const & key) const {
1✔
732
    return ui->openGLWidget->getDigitalIntervalConfig(key);
1✔
733
}
734

735
void DataViewer_Widget::_handleThemeChanged(int theme_index) {
×
736
    PlotTheme theme = (theme_index == 0) ? PlotTheme::Dark : PlotTheme::Light;
×
737
    ui->openGLWidget->setPlotTheme(theme);
×
738

739
    // Update coordinate label styling based on theme
UNCOV
740
    if (theme == PlotTheme::Dark) {
×
UNCOV
741
        ui->coordinate_label->setStyleSheet("background-color: rgba(0, 0, 0, 50); color: white; padding: 2px;");
×
742
    } else {
UNCOV
743
        ui->coordinate_label->setStyleSheet("background-color: rgba(255, 255, 255, 50); color: black; padding: 2px;");
×
744
    }
745

UNCOV
746
    std::cout << "Theme changed to: " << (theme_index == 0 ? "Dark" : "Light") << std::endl;
×
UNCOV
747
}
×
748

UNCOV
749
void DataViewer_Widget::_handleGridLinesToggled(bool enabled) {
×
UNCOV
750
    ui->openGLWidget->setGridLinesEnabled(enabled);
×
UNCOV
751
}
×
752

UNCOV
753
void DataViewer_Widget::_handleGridSpacingChanged(int spacing) {
×
UNCOV
754
    ui->openGLWidget->setGridSpacing(spacing);
×
UNCOV
755
}
×
756

757
void DataViewer_Widget::_handleVerticalSpacingChanged(double spacing) {
35✔
758
    ui->openGLWidget->setVerticalSpacing(static_cast<float>(spacing));
35✔
759

760
    // Also update PlottingManager vertical scale
761
    if (_plotting_manager) {
35✔
762
        // Convert spacing to a scale factor relative to default (0.1f)
763
        float const scale_factor = static_cast<float>(spacing) / 0.1f;
35✔
764
        _plotting_manager->setGlobalVerticalScale(scale_factor);
35✔
765

766
        // Apply updated positions to all registered series
767
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
35✔
768
        for (auto const & key: analog_keys) {
99✔
769
            _applyPlottingManagerAllocation(key);
64✔
770
        }
771
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
35✔
772
        for (auto const & key: event_keys) {
59✔
773
            _applyPlottingManagerAllocation(key);
24✔
774
        }
775
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
35✔
776
        for (auto const & key: interval_keys) {
35✔
777
            _applyPlottingManagerAllocation(key);
×
778
        }
779

780
        // Trigger canvas update
781
        ui->openGLWidget->updateCanvas();
35✔
782
    }
35✔
783
}
35✔
784

785
void DataViewer_Widget::_plotSelectedFeatureWithoutUpdate(std::string const & key) {
10✔
786
    std::cout << "Attempting to plot feature (batch): " << key << std::endl;
10✔
787

788
    if (key.empty()) {
10✔
UNCOV
789
        std::cerr << "Error: empty key in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
790
        return;
×
791
    }
792

793
    if (!_data_manager) {
10✔
UNCOV
794
        std::cerr << "Error: null data manager in _plotSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
795
        return;
×
796
    }
797

798
    // Get color from model
799
    std::string color = _feature_tree_model->getFeatureColor(key);
10✔
800
    std::cout << "Using color: " << color << " for series: " << key << std::endl;
10✔
801

802
    auto data_type = _data_manager->getType(key);
10✔
803
    std::cout << "Feature type: " << convert_data_type_to_string(data_type) << std::endl;
10✔
804

805
    if (data_type == DM_DataType::Analog) {
10✔
806
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
10✔
807
        if (!series) {
10✔
808
            std::cerr << "Error: failed to get AnalogTimeSeries for key: " << key << std::endl;
×
809
            return;
×
810
        }
811

812
        auto time_key = _data_manager->getTimeKey(key);
10✔
813
        auto time_frame = _data_manager->getTime(time_key);
10✔
814
        if (!time_frame) {
10✔
815
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
816
            return;
×
817
        }
818

819
        // Register with plotting manager for later allocation (single call)
820
        _plotting_manager->addAnalogSeries(key, series, time_frame, color);
10✔
821
        ui->openGLWidget->addAnalogTimeSeries(key, series, time_frame, color);
10✔
822

823
    } else if (data_type == DM_DataType::DigitalEvent) {
10✔
824
        auto series = _data_manager->getData<DigitalEventSeries>(key);
×
825
        if (!series) {
×
UNCOV
826
            std::cerr << "Error: failed to get DigitalEventSeries for key: " << key << std::endl;
×
UNCOV
827
            return;
×
828
        }
829

830
        auto time_key = _data_manager->getTimeKey(key);
×
831
        auto time_frame = _data_manager->getTime(time_key);
×
832
        if (!time_frame) {
×
UNCOV
833
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
834
            return;
×
835
        }
UNCOV
836
        _plotting_manager->addDigitalEventSeries(key, series, time_frame, color);
×
837
        ui->openGLWidget->addDigitalEventSeries(key, series, time_frame, color);
×
838

839
    } else if (data_type == DM_DataType::DigitalInterval) {
×
UNCOV
840
        auto series = _data_manager->getData<DigitalIntervalSeries>(key);
×
UNCOV
841
        if (!series) {
×
UNCOV
842
            std::cerr << "Error: failed to get DigitalIntervalSeries for key: " << key << std::endl;
×
UNCOV
843
            return;
×
844
        }
845

846
        auto time_key = _data_manager->getTimeKey(key);
×
847
        auto time_frame = _data_manager->getTime(time_key);
×
UNCOV
848
        if (!time_frame) {
×
849
            std::cerr << "Error: failed to get TimeFrame for key: " << key << std::endl;
×
850
            return;
×
851
        }
UNCOV
852
        _plotting_manager->addDigitalIntervalSeries(key, series, time_frame, color);
×
UNCOV
853
        ui->openGLWidget->addDigitalIntervalSeries(key, series, time_frame, color);
×
854

855
    } else {
×
856
        std::cout << "Feature type not supported: " << convert_data_type_to_string(data_type) << std::endl;
×
UNCOV
857
        return;
×
858
    }
859

860
    // Note: No canvas update triggered - this is for batch operations
861
    std::cout << "Successfully added series to OpenGL widget (batch mode)" << std::endl;
10✔
862
}
10✔
863

864
void DataViewer_Widget::_removeSelectedFeatureWithoutUpdate(std::string const & key) {
5✔
865
    std::cout << "Attempting to remove feature (batch): " << key << std::endl;
5✔
866

867
    if (key.empty()) {
5✔
868
        std::cerr << "Error: empty key in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
869
        return;
×
870
    }
871

872
    if (!_data_manager) {
5✔
873
        std::cerr << "Error: null data manager in _removeSelectedFeatureWithoutUpdate" << std::endl;
×
UNCOV
874
        return;
×
875
    }
876

877
    auto data_type = _data_manager->getType(key);
5✔
878

879
    // Also unregister from the plotting manager so counts and ordering stay consistent
880
    if (_plotting_manager) {
5✔
881
        if (data_type == DM_DataType::Analog) {
5✔
882
            (void) _plotting_manager->removeAnalogSeries(key);
5✔
UNCOV
883
        } else if (data_type == DM_DataType::DigitalEvent) {
×
884
            (void) _plotting_manager->removeDigitalEventSeries(key);
×
885
        } else if (data_type == DM_DataType::DigitalInterval) {
×
UNCOV
886
            (void) _plotting_manager->removeDigitalIntervalSeries(key);
×
887
        }
888
    }
889

890
    if (data_type == DM_DataType::Analog) {
5✔
891
        ui->openGLWidget->removeAnalogTimeSeries(key);
5✔
892
    } else if (data_type == DM_DataType::DigitalEvent) {
×
893
        ui->openGLWidget->removeDigitalEventSeries(key);
×
UNCOV
894
    } else if (data_type == DM_DataType::DigitalInterval) {
×
895
        ui->openGLWidget->removeDigitalIntervalSeries(key);
×
896
    } else {
UNCOV
897
        std::cout << "Feature type not supported for removal: " << convert_data_type_to_string(data_type) << std::endl;
×
898
        return;
×
899
    }
900

901
    // Note: No canvas update triggered - this is for batch operations
902
    std::cout << "Successfully removed series from OpenGL widget (batch mode)" << std::endl;
5✔
903
}
904

UNCOV
905
void DataViewer_Widget::_calculateOptimalScaling(std::vector<std::string> const & group_keys) {
×
906
    if (group_keys.empty()) {
×
UNCOV
907
        return;
×
908
    }
909

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

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

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

919
    // Add any other already visible analog series
UNCOV
920
    auto all_keys = _data_manager->getAllKeys();
×
UNCOV
921
    for (auto const & key: all_keys) {
×
922
        if (_data_manager->getType(key) == DM_DataType::Analog) {
×
923
            // Check if this key is already in our group (avoid double counting)
924
            bool in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
UNCOV
925
            if (!in_group) {
×
926
                // Check if this series is currently visible
927
                auto config = ui->openGLWidget->getAnalogConfig(key);
×
UNCOV
928
                if (config.has_value() && config.value()->is_visible) {
×
UNCOV
929
                    total_visible_analog_series++;
×
930
                }
931
            }
932
        }
933
    }
934

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

937
    if (total_visible_analog_series <= 0) {
×
938
        return;// No series to scale
×
939
    }
940

941
    // Calculate optimal vertical spacing
942
    // Leave some margin at top and bottom (10% each = 20% total)
UNCOV
943
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
944
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_analog_series);
×
945

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

950
    // Clamp to reasonable bounds
UNCOV
951
    float const min_spacing = 0.01f;
×
UNCOV
952
    float const max_spacing = 1.0f;
×
953
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
954

UNCOV
955
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
UNCOV
956
              << final_spacing << " normalized units" << std::endl;
×
957

958
    // Calculate optimal global gain based on standard deviations
UNCOV
959
    std::vector<float> std_devs;
×
960
    std_devs.reserve(group_keys.size());
×
961

962
    for (auto const & key: group_keys) {
×
UNCOV
963
        auto series = _data_manager->getData<AnalogTimeSeries>(key);
×
964
        if (series) {
×
965
            float const std_dev = calculate_std_dev_approximate(*series);
×
966
            std_devs.push_back(std_dev);
×
UNCOV
967
            std::cout << "Series " << key << " std dev: " << std_dev << std::endl;
×
968
        }
969
    }
×
970

UNCOV
971
    if (!std_devs.empty()) {
×
972
        // Use the median standard deviation as reference for scaling
973
        std::sort(std_devs.begin(), std_devs.end());
×
UNCOV
974
        float const median_std_dev = std_devs[std_devs.size() / 2];
×
975

976
        // Calculate optimal global scale
977
        // Target: each series should use about 60% of its allocated vertical space
978
        float const target_amplitude_fraction = 0.6f;
×
UNCOV
979
        float const target_amplitude_in_pixels = optimal_spacing * target_amplitude_fraction;
×
980

981
        // Convert to normalized coordinates (3 standard deviations should fit in target amplitude)
982
        float const target_amplitude_normalized = (target_amplitude_in_pixels / static_cast<float>(canvas_height)) * 2.0f;
×
983
        float const three_sigma_target = target_amplitude_normalized;
×
984

985
        // Calculate scale factor needed
UNCOV
986
        float const optimal_global_scale = three_sigma_target / (3.0f * median_std_dev);
×
987

988
        // Clamp to reasonable bounds
UNCOV
989
        float const min_scale = 0.1f;
×
990
        float const max_scale = 100.0f;
×
991
        float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
×
992

UNCOV
993
        std::cout << "Median std dev: " << median_std_dev
×
994
                  << ", target amplitude: " << target_amplitude_in_pixels << " pixels"
×
UNCOV
995
                  << ", optimal global scale: " << final_scale << std::endl;
×
996

997
        // Apply the calculated settings
998
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
999
        ui->global_zoom->setValue(static_cast<double>(final_scale));
×
1000

1001
        std::cout << "Applied auto-scaling: vertical spacing = " << final_spacing
×
1002
                  << ", global scale = " << final_scale << std::endl;
×
1003

1004
    } else {
1005
        // If we can't calculate standard deviations, just apply spacing
1006
        ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
×
UNCOV
1007
        std::cout << "Applied auto-spacing only: vertical spacing = " << final_spacing << std::endl;
×
1008
    }
UNCOV
1009
}
×
1010

UNCOV
1011
void DataViewer_Widget::_calculateOptimalEventSpacing(std::vector<std::string> const & group_keys) {
×
1012
    if (group_keys.empty()) {
×
UNCOV
1013
        return;
×
1014
    }
1015

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

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

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

1025
    // Add any other already visible digital event series
UNCOV
1026
    auto all_keys = _data_manager->getAllKeys();
×
UNCOV
1027
    for (auto const & key: all_keys) {
×
1028
        if (_data_manager->getType(key) == DM_DataType::DigitalEvent) {
×
1029
            // Check if this key is already in our group (avoid double counting)
1030
            bool const in_group = std::find(group_keys.begin(), group_keys.end(), key) != group_keys.end();
×
UNCOV
1031
            if (!in_group) {
×
1032
                // Check if this series is currently visible
UNCOV
1033
                auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
1034
                if (config.has_value() && config.value()->is_visible) {
×
1035
                    total_visible_event_series++;
×
1036
                }
1037
            }
1038
        }
1039
    }
1040

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

UNCOV
1043
    if (total_visible_event_series <= 0) {
×
1044
        return;// No series to scale
×
1045
    }
1046

1047
    // Calculate optimal vertical spacing
1048
    // Leave some margin at top and bottom (10% each = 20% total)
1049
    float const effective_height = static_cast<float>(canvas_height) * 0.8f;
×
UNCOV
1050
    float const optimal_spacing = effective_height / static_cast<float>(total_visible_event_series);
×
1051

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

1056
    // Clamp to reasonable bounds
UNCOV
1057
    float const min_spacing = 0.01f;
×
UNCOV
1058
    float const max_spacing = 1.0f;
×
UNCOV
1059
    float const final_spacing = std::clamp(normalized_spacing, min_spacing, max_spacing);
×
1060

1061
    // Calculate optimal event height (keep events clearly within their lane)
1062
    // Use a conservative fraction of spacing so multiple stacked series remain visually distinct
UNCOV
1063
    float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
×
UNCOV
1064
    float const min_height = 0.01f;
×
UNCOV
1065
    float const max_height = 0.5f;
×
UNCOV
1066
    float const final_height = std::clamp(optimal_event_height, min_height, max_height);
×
1067

UNCOV
1068
    std::cout << "Calculated spacing: " << optimal_spacing << " pixels -> "
×
UNCOV
1069
              << final_spacing << " normalized units" << std::endl;
×
UNCOV
1070
    std::cout << "Calculated event height: " << final_height << " normalized units" << std::endl;
×
1071

1072
    // Apply the calculated settings to all event series in the group
UNCOV
1073
    for (auto const & key: group_keys) {
×
UNCOV
1074
        auto config = ui->openGLWidget->getDigitalEventConfig(key);
×
1075
        if (config.has_value()) {
×
UNCOV
1076
            config.value()->vertical_spacing = final_spacing;
×
UNCOV
1077
            config.value()->event_height = final_height;
×
UNCOV
1078
            config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
×
1079
        }
1080
    }
1081

UNCOV
1082
    std::cout << "Applied auto-calculated event spacing: spacing = " << final_spacing
×
UNCOV
1083
              << ", height = " << final_height << std::endl;
×
UNCOV
1084
}
×
1085

1086
void DataViewer_Widget::autoArrangeVerticalSpacing() {
36✔
1087
    std::cout << "DataViewer_Widget: Auto-arranging with plotting manager..." << std::endl;
36✔
1088

1089
    // Update dimensions first
1090
    _updatePlottingManagerDimensions();
36✔
1091

1092
    // Apply new allocations to all registered series
1093
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
36✔
1094
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
36✔
1095
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
36✔
1096

1097
    for (auto const & key: analog_keys) {
100✔
1098
        _applyPlottingManagerAllocation(key);
64✔
1099
    }
1100
    for (auto const & key: event_keys) {
60✔
1101
        _applyPlottingManagerAllocation(key);
24✔
1102
    }
1103
    for (auto const & key: interval_keys) {
36✔
1104
        _applyPlottingManagerAllocation(key);
×
1105
    }
1106

1107
    // Calculate and apply optimal scaling to fill the canvas
1108
    _autoFillCanvas();
36✔
1109

1110
    // Update OpenGL widget view bounds based on content height
1111
    _updateViewBounds();
36✔
1112

1113
    // Trigger canvas update to show new positions
1114
    ui->openGLWidget->updateCanvas();
36✔
1115

1116
    auto total_keys = analog_keys.size() + event_keys.size() + interval_keys.size();
36✔
1117
    std::cout << "DataViewer_Widget: Auto-arrange completed for " << total_keys << " series" << std::endl;
36✔
1118
}
72✔
1119

1120
void DataViewer_Widget::_updateViewBounds() {
36✔
1121
    if (!_plotting_manager) {
36✔
UNCOV
1122
        return;
×
1123
    }
1124

1125
    // PlottingManager uses normalized coordinates, so view bounds are typically -1 to +1
1126
    // For now, use standard bounds but this enables future enhancement
1127
    std::cout << "DataViewer_Widget: Using standard view bounds with PlottingManager" << std::endl;
36✔
1128
}
1129

UNCOV
1130
std::string DataViewer_Widget::_convertDataType(DM_DataType dm_type) const {
×
UNCOV
1131
    switch (dm_type) {
×
UNCOV
1132
        case DM_DataType::Analog:
×
UNCOV
1133
            return "Analog";
×
UNCOV
1134
        case DM_DataType::DigitalEvent:
×
1135
            return "DigitalEvent";
×
UNCOV
1136
        case DM_DataType::DigitalInterval:
×
UNCOV
1137
            return "DigitalInterval";
×
UNCOV
1138
        default:
×
1139
            // For unsupported types, default to Analog
1140
            // This should be rare in practice given our type filters
UNCOV
1141
            std::cerr << "Warning: Unsupported data type " << convert_data_type_to_string(dm_type)
×
UNCOV
1142
                      << " defaulting to Analog for plotting manager" << std::endl;
×
UNCOV
1143
            return "Analog";
×
1144
    }
1145
}
1146

1147
void DataViewer_Widget::_updatePlottingManagerDimensions() {
57✔
1148
    if (!_plotting_manager) {
57✔
UNCOV
1149
        return;
×
1150
    }
1151

1152
    // Get current canvas dimensions from OpenGL widget
1153
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
57✔
1154

1155
    // PlottingManager works in normalized device coordinates, so no specific dimension update needed
1156
    // But we could update viewport bounds if needed in the future
1157

1158
    std::cout << "DataViewer_Widget: Updated plotting manager dimensions: "
57✔
1159
              << canvas_width << "x" << canvas_height << " pixels" << std::endl;
57✔
1160
}
1161

1162
void DataViewer_Widget::_applyPlottingManagerAllocation(std::string const & series_key) {
286✔
1163
    if (!_plotting_manager) {
286✔
1164
        return;
×
1165
    }
1166

1167
    auto data_type = _data_manager->getType(series_key);
286✔
1168

1169
    std::cout << "DataViewer_Widget: Applying plotting manager allocation for '" << series_key << "'" << std::endl;
286✔
1170

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

1174
    // Apply positioning based on data type
1175
    if (data_type == DM_DataType::Analog) {
286✔
1176
        auto config = ui->openGLWidget->getAnalogConfig(series_key);
219✔
1177
        if (config.has_value()) {
219✔
1178
            float yc, yh;
219✔
1179
            if (_plotting_manager->getAnalogSeriesAllocationForKey(series_key, yc, yh)) {
219✔
1180
                config.value()->allocated_y_center = yc;
219✔
1181
                config.value()->allocated_height = yh;
219✔
1182
            }
1183
        }
1184

1185
    } else if (data_type == DM_DataType::DigitalEvent) {
67✔
1186
        auto config = ui->openGLWidget->getDigitalEventConfig(series_key);
65✔
1187
        if (config.has_value()) {
65✔
1188
            // Basic allocation - will be properly implemented when OpenGL widget is updated
1189
            std::cout << "  Applied basic allocation to event '" << series_key << "'" << std::endl;
65✔
1190
        }
1191

1192
    } else if (data_type == DM_DataType::DigitalInterval) {
2✔
1193
        auto config = ui->openGLWidget->getDigitalIntervalConfig(series_key);
2✔
1194
        if (config.has_value()) {
2✔
1195
            // Basic allocation - will be properly implemented when OpenGL widget is updated
1196
            std::cout << "  Applied basic allocation to interval '" << series_key << "'" << std::endl;
2✔
1197
        }
1198
    }
1199
}
1200

1201
// ===== Context menu and configuration handling =====
1202
void DataViewer_Widget::_showGroupContextMenu(std::string const & group_name, QPoint const & global_pos) {
×
1203
    QMenu menu;
×
UNCOV
1204
    QMenu * loadMenu = menu.addMenu("Load configuration");
×
1205
    QAction * loadSpikeSorter = loadMenu->addAction("spikesorter configuration");
×
1206
    QAction * clearConfig = menu.addAction("Clear configuration");
×
1207

1208
    connect(loadSpikeSorter, &QAction::triggered, this, [this, group_name]() {
×
1209
        _loadSpikeSorterConfigurationForGroup(QString::fromStdString(group_name));
×
1210
    });
×
1211
    connect(clearConfig, &QAction::triggered, this, [this, group_name]() {
×
1212
        _clearConfigurationForGroup(QString::fromStdString(group_name));
×
UNCOV
1213
    });
×
1214

1215
    menu.exec(global_pos);
×
UNCOV
1216
}
×
1217

UNCOV
1218
void DataViewer_Widget::_loadSpikeSorterConfigurationForGroup(QString const & group_name) {
×
1219
    // For now, use a test constant string or file dialog; here we open a file dialog
UNCOV
1220
    QString path = QFileDialog::getOpenFileName(this, QString("Load spikesorter configuration for %1").arg(group_name), QString(), "Text Files (*.txt *.cfg *.conf);;All Files (*)");
×
UNCOV
1221
    if (path.isEmpty()) return;
×
UNCOV
1222
    QFile file(path);
×
UNCOV
1223
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return;
×
UNCOV
1224
    QByteArray data = file.readAll();
×
UNCOV
1225
    auto positions = _parseSpikeSorterConfig(data.toStdString());
×
UNCOV
1226
    if (positions.empty()) return;
×
UNCOV
1227
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
×
1228

1229
    // Re-apply allocation to visible analog keys and update
UNCOV
1230
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
UNCOV
1231
    for (auto const & key : analog_keys) {
×
UNCOV
1232
        _applyPlottingManagerAllocation(key);
×
1233
    }
UNCOV
1234
    ui->openGLWidget->updateCanvas();
×
UNCOV
1235
}
×
1236

UNCOV
1237
void DataViewer_Widget::_clearConfigurationForGroup(QString const & group_name) {
×
UNCOV
1238
    _plotting_manager->clearAnalogGroupConfiguration(group_name.toStdString());
×
1239
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
×
1240
    for (auto const & key : analog_keys) {
×
UNCOV
1241
        _applyPlottingManagerAllocation(key);
×
1242
    }
UNCOV
1243
    ui->openGLWidget->updateCanvas();
×
UNCOV
1244
}
×
1245

1246
std::vector<PlottingManager::AnalogGroupChannelPosition> DataViewer_Widget::_parseSpikeSorterConfig(std::string const & text) {
1✔
1247
    std::vector<PlottingManager::AnalogGroupChannelPosition> out;
1✔
1248
    std::istringstream ss(text);
1✔
1249
    std::string line;
1✔
1250
    bool first = true;
1✔
1251
    while (std::getline(ss, line)) {
6✔
1252
        if (line.empty()) continue;
5✔
1253
        if (first) { first = false; continue; } // skip header row (electrode name)
5✔
1254
        std::istringstream ls(line);
4✔
1255
        int row = 0; int ch = 0; float x = 0.0f; float y = 0.0f;
4✔
1256
        if (!(ls >> row >> ch >> x >> y)) continue;
4✔
1257
        // SpikeSorter is 1-based; convert to 0-based for our program
1258
        if (ch > 0) ch -= 1;
4✔
1259
        PlottingManager::AnalogGroupChannelPosition p; p.channel_id = ch; p.x = x; p.y = y;
4✔
1260
        out.push_back(p);
4✔
1261
    }
4✔
1262
    return out;
2✔
1263
}
1✔
1264

1265
void DataViewer_Widget::_loadSpikeSorterConfigurationFromText(QString const & group_name, QString const & text) {
1✔
1266
    auto positions = _parseSpikeSorterConfig(text.toStdString());
1✔
1267
    if (positions.empty()) {
1✔
UNCOV
1268
        std::cout << "No positions found in spike sorter configuration" << std::endl;
×
UNCOV
1269
        return;
×
1270
    }
1271
    _plotting_manager->loadAnalogSpikeSorterConfiguration(group_name.toStdString(), positions);
1✔
1272
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
1✔
1273
    for (auto const & key : analog_keys) {
5✔
1274
        _applyPlottingManagerAllocation(key);
4✔
1275
    }
1276
    ui->openGLWidget->updateCanvas();
1✔
1277
}
1✔
1278

1279
void DataViewer_Widget::_autoFillCanvas() {
36✔
1280
    std::cout << "DataViewer_Widget: Auto-filling canvas with PlottingManager..." << std::endl;
36✔
1281

1282
    if (!_plotting_manager) {
36✔
UNCOV
1283
        std::cout << "No plotting manager available" << std::endl;
×
UNCOV
1284
        return;
×
1285
    }
1286

1287
    // Get current canvas dimensions
1288
    auto [canvas_width, canvas_height] = ui->openGLWidget->getCanvasSize();
36✔
1289
    std::cout << "Canvas size: " << canvas_width << "x" << canvas_height << " pixels" << std::endl;
36✔
1290

1291
    // Count visible series using PlottingManager
1292
    auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
36✔
1293
    auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
36✔
1294
    auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
36✔
1295

1296
    int visible_analog_count = static_cast<int>(analog_keys.size());
36✔
1297
    int visible_event_count = static_cast<int>(event_keys.size());
36✔
1298
    int visible_interval_count = static_cast<int>(interval_keys.size());
36✔
1299

1300
    int total_visible = visible_analog_count + visible_event_count + visible_interval_count;
36✔
1301
    std::cout << "Visible series: " << visible_analog_count << " analog, "
36✔
1302
              << visible_event_count << " events, " << visible_interval_count
36✔
1303
              << " intervals (total: " << total_visible << ")" << std::endl;
36✔
1304

1305
    if (total_visible == 0) {
36✔
1306
        std::cout << "No visible series to auto-scale" << std::endl;
1✔
1307
        return;
1✔
1308
    }
1309

1310
    // Calculate optimal vertical spacing to fill canvas
1311
    // Use 90% of canvas height, leaving 5% margin at top and bottom
1312
    float const usable_height = static_cast<float>(canvas_height) * 0.9f;
35✔
1313
    float const optimal_spacing_pixels = usable_height / static_cast<float>(total_visible);
35✔
1314

1315
    // Convert to normalized coordinates (assuming 2.0 total normalized height)
1316
    float const optimal_spacing_normalized = (optimal_spacing_pixels / static_cast<float>(canvas_height)) * 2.0f;
35✔
1317

1318
    // Clamp to reasonable bounds
1319
    float const min_spacing = 0.02f;
35✔
1320
    float const max_spacing = 1.5f;
35✔
1321
    float const final_spacing = std::clamp(optimal_spacing_normalized, min_spacing, max_spacing);
35✔
1322

1323
    std::cout << "Calculated optimal spacing: " << optimal_spacing_pixels << " pixels -> "
35✔
1324
              << final_spacing << " normalized units" << std::endl;
35✔
1325

1326
    // Apply the calculated vertical spacing
1327
    ui->vertical_spacing->setValue(static_cast<double>(final_spacing));
35✔
1328

1329
    // Calculate and apply optimal event heights for digital event series
1330
    if (visible_event_count > 0) {
35✔
1331
        // Calculate event height conservatively to avoid near full-lane rendering
1332
        // Use 30% of the spacing and cap at 0.2 to keep consistent scale across zooms
1333
        float const optimal_event_height = std::min(final_spacing * 0.3f, 0.2f);
11✔
1334

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

1337
        // Apply optimal height to all visible digital event series
1338
        for (auto const & key: event_keys) {
35✔
1339
            auto config = ui->openGLWidget->getDigitalEventConfig(key);
24✔
1340
            if (config.has_value() && config.value()->is_visible) {
24✔
1341
                config.value()->event_height = optimal_event_height;
24✔
1342
                config.value()->display_mode = EventDisplayMode::Stacked;// Ensure stacked mode
24✔
1343
                std::cout << "  Applied event height " << optimal_event_height
24✔
1344
                          << " to series '" << key << "'" << std::endl;
24✔
1345
            }
1346
        }
1347
    }
1348

1349
    // Calculate and apply optimal interval heights for digital interval series
1350
    if (visible_interval_count > 0) {
35✔
1351
        // Calculate optimal interval height to fill most of the allocated space
1352
        // Use 80% of the spacing to leave some visual separation between intervals
UNCOV
1353
        float const optimal_interval_height = final_spacing * 0.8f;
×
1354

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

1357
        // Apply optimal height to all visible digital interval series
UNCOV
1358
        for (auto const & key: interval_keys) {
×
UNCOV
1359
            auto config = ui->openGLWidget->getDigitalIntervalConfig(key);
×
UNCOV
1360
            if (config.has_value() && config.value()->is_visible) {
×
UNCOV
1361
                config.value()->interval_height = optimal_interval_height;
×
UNCOV
1362
                std::cout << "  Applied interval height " << optimal_interval_height
×
UNCOV
1363
                          << " to series '" << key << "'" << std::endl;
×
1364
            }
1365
        }
1366
    }
1367

1368
    // Calculate optimal global scale for analog series to use their allocated space effectively
1369
    if (visible_analog_count > 0) {
35✔
1370
        // Sample a few analog series to estimate appropriate scaling
1371
        std::vector<float> sample_std_devs;
28✔
1372
        sample_std_devs.reserve(std::min(5, visible_analog_count));// Sample up to 5 series
28✔
1373

1374
        int sampled = 0;
28✔
1375
        for (auto const & key: analog_keys) {
92✔
1376
            if (sampled >= 5) break;
64✔
1377

1378
            auto config = ui->openGLWidget->getAnalogConfig(key);
64✔
1379
            if (config.has_value() && config.value()->is_visible) {
64✔
1380
                auto series = _data_manager->getData<AnalogTimeSeries>(key);
64✔
1381
                if (series) {
64✔
1382
                    float std_dev = calculate_std_dev_approximate(*series);
64✔
1383
                    if (std_dev > 0.0f) {
64✔
1384
                        sample_std_devs.push_back(std_dev);
64✔
1385
                        sampled++;
64✔
1386
                    }
1387
                }
1388
            }
64✔
1389
        }
1390

1391
        if (!sample_std_devs.empty()) {
28✔
1392
            // Use median standard deviation for scaling calculation
1393
            std::sort(sample_std_devs.begin(), sample_std_devs.end());
28✔
1394
            float median_std_dev = sample_std_devs[sample_std_devs.size() / 2];
28✔
1395

1396
            // Calculate scale so that ±3 standard deviations use ~60% of allocated space
1397
            float const target_amplitude_fraction = 0.6f;
28✔
1398
            float const target_amplitude_pixels = optimal_spacing_pixels * target_amplitude_fraction;
28✔
1399
            float const target_amplitude_normalized = (target_amplitude_pixels / static_cast<float>(canvas_height)) * 2.0f;
28✔
1400

1401
            // For ±3σ coverage
1402
            float const three_sigma_coverage = target_amplitude_normalized;
28✔
1403
            float const optimal_global_scale = three_sigma_coverage / (6.0f * median_std_dev);
28✔
1404

1405
            // Clamp to reasonable bounds
1406
            float const min_scale = 0.01f;
28✔
1407
            float const max_scale = 100.0f;
28✔
1408
            float const final_scale = std::clamp(optimal_global_scale, min_scale, max_scale);
28✔
1409

1410
            std::cout << "Calculated optimal global scale: median_std_dev=" << median_std_dev
28✔
1411
                      << ", target_amplitude=" << target_amplitude_pixels << " pixels"
28✔
1412
                      << ", final_scale=" << final_scale << std::endl;
28✔
1413

1414
            // Apply the calculated global scale
1415
            ui->global_zoom->setValue(static_cast<double>(final_scale));
28✔
1416
        }
1417
    }
28✔
1418

1419
    std::cout << "Auto-fill canvas completed" << std::endl;
35✔
1420
}
38✔
1421

1422
void DataViewer_Widget::cleanupDeletedData() {
17✔
1423
    if (!_data_manager) {
17✔
UNCOV
1424
        return;
×
1425
    }
1426

1427
    // Collect keys that no longer exist in DataManager
1428
    std::vector<std::string> keys_to_cleanup;
17✔
1429

1430
    if (_plotting_manager) {
17✔
1431
        auto analog_keys = _plotting_manager->getVisibleAnalogSeriesKeys();
17✔
1432
        for (auto const & key: analog_keys) {
24✔
1433
            if (!_data_manager->getData<AnalogTimeSeries>(key)) {
7✔
1434
                keys_to_cleanup.push_back(key);
×
1435
            }
1436
        }
1437
        auto event_keys = _plotting_manager->getVisibleDigitalEventSeriesKeys();
17✔
1438
        for (auto const & key: event_keys) {
17✔
UNCOV
1439
            if (!_data_manager->getData<DigitalEventSeries>(key)) {
×
UNCOV
1440
                keys_to_cleanup.push_back(key);
×
1441
            }
1442
        }
1443
        auto interval_keys = _plotting_manager->getVisibleDigitalIntervalSeriesKeys();
17✔
1444
        for (auto const & key: interval_keys) {
17✔
1445
            if (!_data_manager->getData<DigitalIntervalSeries>(key)) {
×
1446
                keys_to_cleanup.push_back(key);
×
1447
            }
1448
        }
1449
    }
17✔
1450

1451
    if (keys_to_cleanup.empty()) {
17✔
1452
        return;
17✔
1453
    }
1454

1455
    // De-duplicate keys in case the same key appears in multiple lists
UNCOV
1456
    std::sort(keys_to_cleanup.begin(), keys_to_cleanup.end());
×
1457
    keys_to_cleanup.erase(std::unique(keys_to_cleanup.begin(), keys_to_cleanup.end()), keys_to_cleanup.end());
×
1458

1459
    // Post cleanup to OpenGLWidget's thread safely
1460
    QPointer<OpenGLWidget> glw = ui ? ui->openGLWidget : nullptr;
×
UNCOV
1461
    if (glw) {
×
UNCOV
1462
        QMetaObject::invokeMethod(glw, [glw, keys = keys_to_cleanup]() {
×
1463
            if (!glw) return;
×
1464
            for (auto const & key : keys) {
×
UNCOV
1465
                glw->removeAnalogTimeSeries(key);
×
1466
                glw->removeDigitalEventSeries(key);
×
UNCOV
1467
                glw->removeDigitalIntervalSeries(key);
×
1468
            } }, Qt::QueuedConnection);
1469
    }
1470

1471
    // Remove from PlottingManager defensively (all types) on our thread
1472
    if (_plotting_manager) {
×
UNCOV
1473
        for (auto const & key: keys_to_cleanup) {
×
1474
            (void) _plotting_manager->removeAnalogSeries(key);
×
UNCOV
1475
            (void) _plotting_manager->removeDigitalEventSeries(key);
×
1476
            (void) _plotting_manager->removeDigitalIntervalSeries(key);
×
1477
        }
1478
    }
1479

1480
    // Re-arrange remaining data
UNCOV
1481
    autoArrangeVerticalSpacing();
×
1482
}
17✔
1483

UNCOV
1484
void DataViewer_Widget::_hidePropertiesPanel() {
×
1485
    // Save current splitter sizes before hiding
UNCOV
1486
    _saved_splitter_sizes = ui->main_splitter->sizes();
×
1487
    
1488
    // Collapse the properties panel to 0 width
1489
    ui->main_splitter->setSizes({0, ui->main_splitter->sizes()[1]});
×
1490
    
1491
    // Hide the properties panel and show the reveal button
UNCOV
1492
    ui->properties_container->hide();
×
UNCOV
1493
    ui->show_properties_button->show();
×
1494
    
1495
    _properties_panel_collapsed = true;
×
1496
    
UNCOV
1497
    std::cout << "Properties panel hidden" << std::endl;
×
1498
    
1499
    // Trigger a canvas update to adjust to new size
UNCOV
1500
    ui->openGLWidget->update();
×
UNCOV
1501
}
×
1502

UNCOV
1503
void DataViewer_Widget::_showPropertiesPanel() {
×
1504
    // Show the properties panel
UNCOV
1505
    ui->properties_container->show();
×
1506
    
1507
    // Restore saved splitter sizes
UNCOV
1508
    if (!_saved_splitter_sizes.isEmpty()) {
×
UNCOV
1509
        ui->main_splitter->setSizes(_saved_splitter_sizes);
×
1510
    } else {
1511
        // Default sizes if no saved sizes (320px for properties, rest for plot)
UNCOV
1512
        ui->main_splitter->setSizes({320, 1000});
×
1513
    }
1514
    
1515
    // Hide the reveal button
UNCOV
1516
    ui->show_properties_button->hide();
×
1517
    
UNCOV
1518
    _properties_panel_collapsed = false;
×
1519
    
UNCOV
1520
    std::cout << "Properties panel shown" << std::endl;
×
1521
    
1522
    // Trigger a canvas update to adjust to new size
UNCOV
1523
    ui->openGLWidget->update();
×
UNCOV
1524
}
×
1525

UNCOV
1526
void DataViewer_Widget::_exportToSVG() {
×
UNCOV
1527
    std::cout << "SVG Export initiated" << std::endl;
×
1528

1529
    // Get save file path from user
UNCOV
1530
    QString const fileName = QFileDialog::getSaveFileName(
×
1531
        this,
UNCOV
1532
        tr("Export Plot to SVG"),
×
UNCOV
1533
        QString(),
×
UNCOV
1534
        tr("SVG Files (*.svg);;All Files (*)"));
×
1535

UNCOV
1536
    if (fileName.isEmpty()) {
×
UNCOV
1537
        std::cout << "SVG Export cancelled by user" << std::endl;
×
UNCOV
1538
        return;
×
1539
    }
1540

1541
    try {
1542
        // Create SVG exporter with current plot state
UNCOV
1543
        SVGExporter exporter(ui->openGLWidget, _plotting_manager.get());
×
1544

1545
        // Configure scalebar if requested
NEW
1546
        if (ui->svg_scalebar_checkbox->isChecked()) {
×
NEW
1547
            int const scalebar_length = ui->scalebar_length_spinbox->value();
×
NEW
1548
            exporter.enableScalebar(true, scalebar_length);
×
1549
        }
1550

1551
        // Generate SVG document
UNCOV
1552
        QString const svg_content = exporter.exportToSVG();
×
1553

1554
        // Write to file
UNCOV
1555
        QFile file(fileName);
×
UNCOV
1556
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
UNCOV
1557
            QMessageBox::critical(
×
1558
                this,
UNCOV
1559
                tr("Export Failed"),
×
UNCOV
1560
                tr("Could not open file for writing:\n%1").arg(fileName));
×
UNCOV
1561
            std::cerr << "Failed to open file: " << fileName.toStdString() << std::endl;
×
UNCOV
1562
            return;
×
1563
        }
1564

UNCOV
1565
        QTextStream out(&file);
×
UNCOV
1566
        out << svg_content;
×
UNCOV
1567
        file.close();
×
1568

UNCOV
1569
        std::cout << "SVG Export successful: " << fileName.toStdString() << std::endl;
×
1570

1571
        // Show success message
UNCOV
1572
        QMessageBox::information(
×
1573
            this,
UNCOV
1574
            tr("Export Successful"),
×
UNCOV
1575
            tr("Plot exported to:\n%1\n\nCanvas size: %2x%3")
×
UNCOV
1576
                .arg(fileName)
×
UNCOV
1577
                .arg(exporter.getCanvasWidth())
×
UNCOV
1578
                .arg(exporter.getCanvasHeight()));
×
1579

UNCOV
1580
    } catch (std::exception const & e) {
×
UNCOV
1581
        QMessageBox::critical(
×
1582
            this,
UNCOV
1583
            tr("Export Failed"),
×
UNCOV
1584
            tr("An error occurred during export:\n%1").arg(e.what()));
×
UNCOV
1585
        std::cerr << "SVG Export failed: " << e.what() << std::endl;
×
UNCOV
1586
    }
×
UNCOV
1587
}
×
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