• 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

0.0
/src/WhiskerToolbox/DataViewer_Widget/SVGExporter.cpp
1
#include "SVGExporter.hpp"
2

3
#include "OpenGLWidget.hpp"
4
#include "DataViewer/PlottingManager/PlottingManager.hpp"
5
#include "DataViewer/AnalogTimeSeries/AnalogTimeSeriesDisplayOptions.hpp"
6
#include "DataViewer/AnalogTimeSeries/MVP_AnalogTimeSeries.hpp"
7
#include "DataViewer/DigitalEvent/DigitalEventSeriesDisplayOptions.hpp"
8
#include "DataViewer/DigitalEvent/MVP_DigitalEvent.hpp"
9
#include "DataViewer/DigitalInterval/DigitalIntervalSeriesDisplayOptions.hpp"
10
#include "DataViewer/DigitalInterval/MVP_DigitalInterval.hpp"
11
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
12
#include "DigitalTimeSeries/Digital_Event_Series.hpp"
13
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
14
#include "TimeFrame/TimeFrame.hpp"
15
#include "DataManager/utils/color.hpp"
16

17
#include <glm/glm.hpp>
18
#include <glm/gtc/matrix_transform.hpp>
19

20
#include <iostream>
21

22
SVGExporter::SVGExporter(OpenGLWidget * gl_widget, PlottingManager * plotting_manager)
×
23
    : gl_widget_(gl_widget),
×
24
      plotting_manager_(plotting_manager) {
×
25
}
×
26

27
QString SVGExporter::exportToSVG() {
×
28
    svg_elements_.clear();
×
29

30
    // Get current visible time range from OpenGL widget
31
    auto const x_axis = gl_widget_->getXAxis();
×
32
    auto const start_time = x_axis.getStart();
×
33
    auto const end_time = x_axis.getEnd();
×
34
    auto const y_min = gl_widget_->getYMin();
×
35
    auto const y_max = gl_widget_->getYMax();
×
36

37
    std::cout << "SVG Export - Time range: " << start_time << " to " << end_time << std::endl;
×
38
    std::cout << "SVG Export - Y range: " << y_min << " to " << y_max << std::endl;
×
39

40
    // Export in same order as OpenGL rendering: intervals first (background), then analog, then events
41
    
42
    // 1. Export digital interval series (rendered as background)
43
    auto const & interval_series_map = gl_widget_->getDigitalIntervalSeriesMap();
×
44
    for (auto const & [key, interval_data] : interval_series_map) {
×
45
        if (interval_data.display_options->is_visible) {
×
46
            addDigitalIntervalSeries(
×
47
                key,
48
                interval_data.series,
×
49
                interval_data.time_frame,
×
50
                *interval_data.display_options,
×
51
                static_cast<float>(start_time),
52
                static_cast<float>(end_time));
53
        }
54
    }
55

56
    // 2. Export analog time series
57
    auto const & analog_series_map = gl_widget_->getAnalogSeriesMap();
×
58
    for (auto const & [key, analog_data] : analog_series_map) {
×
59
        if (analog_data.display_options->is_visible) {
×
60
            addAnalogSeries(
×
61
                key,
62
                analog_data.series,
×
63
                analog_data.time_frame,
×
64
                *analog_data.display_options,
×
65
                start_time,
66
                end_time);
67
        }
68
    }
69

70
    // 3. Export digital event series
71
    auto const & event_series_map = gl_widget_->getDigitalEventSeriesMap();
×
72
    for (auto const & [key, event_data] : event_series_map) {
×
73
        if (event_data.display_options->is_visible) {
×
74
            addDigitalEventSeries(
×
75
                key,
76
                event_data.series,
×
77
                event_data.time_frame,
×
78
                *event_data.display_options,
×
79
                start_time,
80
                end_time);
81
        }
82
    }
83

84
    // 4. Add scalebar if enabled (drawn last so it's on top)
NEW
85
    if (scalebar_enabled_) {
×
NEW
86
        addScalebar(static_cast<float>(start_time), static_cast<float>(end_time));
×
87
    }
88

89
    return buildSVGDocument();
×
90
}
91

92
glm::vec2 SVGExporter::transformVertexToSVG(glm::vec4 const & vertex, glm::mat4 const & mvp) const {
×
93
    // Apply MVP transformation to get normalized device coordinates (NDC)
94
    glm::vec4 ndc = mvp * vertex;
×
95
    
96
    // Perform perspective divide if w != 0
97
    if (std::abs(ndc.w) > 1e-6f) {
×
98
        ndc /= ndc.w;
×
99
    }
100

101
    // Map NDC [-1, 1] to SVG coordinates [0, width] × [0, height]
102
    // Note: SVG Y-axis is inverted (top = 0, bottom = height)
103
    float const svg_x = static_cast<float>(svg_width_) * (ndc.x + 1.0f) / 2.0f;
×
104
    float const svg_y = static_cast<float>(svg_height_) * (1.0f - ndc.y) / 2.0f;
×
105

106
    return glm::vec2(svg_x, svg_y);
×
107
}
108

109
QString SVGExporter::colorToHex(int r, int g, int b) {
×
110
    return QString("#%1%2%3")
×
111
        .arg(r, 2, 16, QChar('0'))
×
112
        .arg(g, 2, 16, QChar('0'))
×
113
        .arg(b, 2, 16, QChar('0'));
×
114
}
115

116
void SVGExporter::addAnalogSeries(
×
117
        std::string const & key,
118
        std::shared_ptr<AnalogTimeSeries> const & series,
119
        std::shared_ptr<TimeFrame> const & time_frame,
120
        NewAnalogTimeSeriesDisplayOptions const & display_options,
121
        int start_time,
122
        int end_time) {
123

124
    std::cout << "SVG Export - Adding analog series: " << key << std::endl;
×
125

126
    // Get data in visible range
127
    auto const series_start_index = getTimeIndexForSeries(
×
128
        TimeFrameIndex(start_time),
129
        gl_widget_->getMasterTimeFrame().get(),
×
130
        time_frame.get());
×
131
    auto const series_end_index = getTimeIndexForSeries(
×
132
        TimeFrameIndex(end_time),
133
        gl_widget_->getMasterTimeFrame().get(),
×
134
        time_frame.get());
×
135

136
    auto analog_range = series->getTimeValueSpanInTimeFrameIndexRange(
×
137
        series_start_index, series_end_index);
138

139
    if (analog_range.values.empty()) {
×
140
        std::cout << "SVG Export - No data in range for " << key << std::endl;
×
141
        return;
×
142
    }
143

144
    // Build MVP matrices using same logic as OpenGL rendering
145
    auto const Model = new_getAnalogModelMat(
×
146
        display_options,
147
        display_options.cached_std_dev,
148
        display_options.cached_mean,
149
        *plotting_manager_);
×
150
    auto const View = new_getAnalogViewMat(*plotting_manager_);
×
151
    auto const Projection = new_getAnalogProjectionMat(
×
152
        TimeFrameIndex(start_time),
153
        TimeFrameIndex(end_time),
154
        gl_widget_->getYMin(),
×
155
        gl_widget_->getYMax(),
×
156
        *plotting_manager_);
×
157

158
    glm::mat4 const mvp = Projection * View * Model;
×
159

160
    // Convert color
161
    int r, g, b;
×
162
    hexToRGB(display_options.hex_color, r, g, b);
×
163
    QString const color = colorToHex(r, g, b);
×
164

165
    // Build polyline path
166
    // TODO: Implement gap handling (DetectGaps, ShowMarkers) similar to OpenGL rendering
167
    QStringList points;
×
168
    auto time_iter = analog_range.time_indices.begin();
×
169
    for (size_t i = 0; i < analog_range.values.size(); i++) {
×
170
        auto const x_data = time_frame->getTimeAtIndex(**time_iter);
×
171
        auto const y_data = analog_range.values[i];
×
172

173
        glm::vec4 const vertex(x_data, y_data, 0.0f, 1.0f);
×
174
        glm::vec2 const svg_pos = transformVertexToSVG(vertex, mvp);
×
175

176
        points.append(QString("%1,%2").arg(svg_pos.x).arg(svg_pos.y));
×
177
        ++(*time_iter);
×
178
    }
179

180
    // Create SVG polyline element
181
    QString const line_thickness = QString::number(display_options.line_thickness);
×
182
    QString const polyline = QString(
×
183
        R"(<polyline points="%1" fill="none" stroke="%2" stroke-width="%3" stroke-linejoin="round" stroke-linecap="round"/>)")
184
        .arg(points.join(" "))
×
185
        .arg(color)
×
186
        .arg(line_thickness);
×
187

188
    svg_elements_.append(polyline);
×
189
    std::cout << "SVG Export - Added " << points.size() << " points for " << key << std::endl;
×
190
}
×
191

192
void SVGExporter::addDigitalEventSeries(
×
193
        std::string const & key,
194
        std::shared_ptr<DigitalEventSeries> const & series,
195
        std::shared_ptr<TimeFrame> const & time_frame,
196
        NewDigitalEventSeriesDisplayOptions const & display_options,
197
        int start_time,
198
        int end_time) {
199

200
    std::cout << "SVG Export - Adding digital event series: " << key << std::endl;
×
201

202
    // Get events in visible range
203
    auto const series_start = getTimeIndexForSeries(
×
204
        TimeFrameIndex(start_time),
205
        gl_widget_->getMasterTimeFrame().get(),
×
206
        time_frame.get());
×
207
    auto const series_end = getTimeIndexForSeries(
×
208
        TimeFrameIndex(end_time),
209
        gl_widget_->getMasterTimeFrame().get(),
×
210
        time_frame.get());
×
211

212
    auto visible_events = series->getEventsInRange(series_start, series_end);
×
213

214
    // Build MVP matrices using same logic as OpenGL rendering
215
    auto const Model = new_getEventModelMat(display_options, *plotting_manager_);
×
216
    auto const View = new_getEventViewMat(display_options, *plotting_manager_);
×
217
    auto const Projection = new_getEventProjectionMat(
×
218
        start_time,
219
        end_time,
220
        gl_widget_->getYMin(),
×
221
        gl_widget_->getYMax(),
×
222
        *plotting_manager_);
×
223

224
    glm::mat4 const mvp = Projection * View * Model;
×
225

226
    // Convert color
227
    int r, g, b;
×
228
    hexToRGB(display_options.hex_color, r, g, b);
×
229
    QString const color = colorToHex(r, g, b);
×
230

231
    // Draw each event as a vertical line
232
    int event_count = 0;
×
233
    for (auto const & event_index : visible_events) {
×
234
        event_count++;
×
235
        auto const event_time = time_frame->getTimeAtIndex(TimeFrameIndex(event_index));
×
236

237
        // Create vertical line using same coordinates as OpenGL
238
        // Events extend based on display mode (Stacked or FullCanvas)
239
        float y_bottom, y_top;
240
        if (display_options.display_mode == EventDisplayMode::Stacked) {
×
241
            // Use allocated bounds with event height
242
            y_bottom = display_options.allocated_y_center - display_options.event_height / 2.0f;
×
243
            y_top = display_options.allocated_y_center + display_options.event_height / 2.0f;
×
244
        } else {
245
            // Full canvas mode
246
            y_bottom = gl_widget_->getYMin();
×
247
            y_top = gl_widget_->getYMax();
×
248
        }
249

250
        glm::vec4 const bottom_vertex(event_time, y_bottom, 0.0f, 1.0f);
×
251
        glm::vec4 const top_vertex(event_time, y_top, 0.0f, 1.0f);
×
252

253
        glm::vec2 const svg_bottom = transformVertexToSVG(bottom_vertex, mvp);
×
254
        glm::vec2 const svg_top = transformVertexToSVG(top_vertex, mvp);
×
255

256
        QString const line_thickness = QString::number(display_options.line_thickness);
×
257
        QString const line = QString(
×
258
            R"(<line x1="%1" y1="%2" x2="%3" y2="%4" stroke="%5" stroke-width="%6"/>)")
259
            .arg(svg_bottom.x)
×
260
            .arg(svg_bottom.y)
×
261
            .arg(svg_top.x)
×
262
            .arg(svg_top.y)
×
263
            .arg(color)
×
264
            .arg(line_thickness);
×
265

266
        svg_elements_.append(line);
×
267
    }
×
268

269
    std::cout << "SVG Export - Added " << event_count << " events for " << key << std::endl;
×
270
}
×
271

272
void SVGExporter::addDigitalIntervalSeries(
×
273
        std::string const & key,
274
        std::shared_ptr<DigitalIntervalSeries> const & series,
275
        std::shared_ptr<TimeFrame> const & time_frame,
276
        NewDigitalIntervalSeriesDisplayOptions const & display_options,
277
        float start_time,
278
        float end_time) {
279

280
    std::cout << "SVG Export - Adding digital interval series: " << key << std::endl;
×
281

282
    // Get intervals that overlap with visible range
283
    auto visible_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
284
        TimeFrameIndex(static_cast<int64_t>(start_time)),
285
        TimeFrameIndex(static_cast<int64_t>(end_time)),
286
        gl_widget_->getMasterTimeFrame().get(),
×
287
        time_frame.get());
×
288

289
    // Build MVP matrices using same logic as OpenGL rendering
290
    auto const Model = new_getIntervalModelMat(display_options, *plotting_manager_);
×
291
    auto const View = new_getIntervalViewMat(*plotting_manager_);
×
292
    auto const Projection = new_getIntervalProjectionMat(
×
293
        start_time,
294
        end_time,
295
        gl_widget_->getYMin(),
×
296
        gl_widget_->getYMax(),
×
297
        *plotting_manager_);
×
298

299
    glm::mat4 const mvp = Projection * View * Model;
×
300

301
    // Convert color
302
    int r, g, b;
×
303
    hexToRGB(display_options.hex_color, r, g, b);
×
304
    QString const color = colorToHex(r, g, b);
×
305

306
    // Draw each interval as a filled rectangle
307
    int interval_count = 0;
×
308
    for (auto const & interval : visible_intervals) {
×
309
        interval_count++;
×
310
        
311
        // IMPORTANT: Convert interval times from series time frame to master time frame
312
        // The interval.start and interval.end are in the series' time frame indices
313
        // We need to convert them to actual time values in the master time frame for rendering
314
        auto const interval_start = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
315
        auto const interval_end = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
316

317
        // Clip the interval to the visible range (same as OpenGL rendering)
318
        float const clipped_start = std::max(interval_start, start_time);
×
319
        float const clipped_end = std::min(interval_end, end_time);
×
320

321
        // Full canvas height for intervals
322
        float const y_bottom = gl_widget_->getYMin();
×
323
        float const y_top = gl_widget_->getYMax();
×
324

325
        // Define rectangle corners using clipped times
326
        glm::vec4 const bottom_left(clipped_start, y_bottom, 0.0f, 1.0f);
×
327
        glm::vec4 const top_right(clipped_end, y_top, 0.0f, 1.0f);
×
328

329
        glm::vec2 const svg_bottom_left = transformVertexToSVG(bottom_left, mvp);
×
330
        glm::vec2 const svg_top_right = transformVertexToSVG(top_right, mvp);
×
331

332
        // Calculate SVG rectangle parameters
333
        float const svg_x = std::min(svg_bottom_left.x, svg_top_right.x);
×
334
        float const svg_y = std::min(svg_bottom_left.y, svg_top_right.y);
×
335
        float const svg_width = std::abs(svg_top_right.x - svg_bottom_left.x);
×
336
        float const svg_height = std::abs(svg_top_right.y - svg_bottom_left.y);
×
337

338
        QString const rect = QString(
×
339
            R"(<rect x="%1" y="%2" width="%3" height="%4" fill="%5" fill-opacity="%6" stroke="none"/>)")
340
            .arg(svg_x)
×
341
            .arg(svg_y)
×
342
            .arg(svg_width)
×
343
            .arg(svg_height)
×
344
            .arg(color)
×
345
            .arg(display_options.alpha);
×
346

347
        svg_elements_.append(rect);
×
348
    }
×
349

350
    std::cout << "SVG Export - Added " << interval_count << " intervals for " << key << std::endl;
×
351
}
×
352

353
QString SVGExporter::buildSVGDocument() const {
×
354
    // Get background color from OpenGL widget
355
    auto const bg_color = gl_widget_->getBackgroundColor();
×
356
    
357
    // Build SVG document with fixed canvas size and viewBox for scaling
358
    QString svg_header = QString(
×
359
        R"(<?xml version="1.0" encoding="UTF-8" standalone="no"?>
360
<svg width="%1" height="%2" viewBox="0 0 %1 %2" xmlns="http://www.w3.org/2000/svg" version="1.1">
361
  <desc>WhiskerToolbox DataViewer Export</desc>
362
  <rect width="100%%" height="100%%" fill="%3"/>
363
)")
364
        .arg(svg_width_)
×
365
        .arg(svg_height_)
×
366
        .arg(QString::fromStdString(bg_color));
×
367

368
    QString const svg_footer = "</svg>";
×
369

370
    // Combine all elements
371
    QString document = svg_header;
×
372
    for (auto const & element : svg_elements_) {
×
373
        document += "  " + element + "\n";
×
374
    }
375
    document += svg_footer;
×
376

377
    return document;
×
378
}
×
379

NEW
380
void SVGExporter::addScalebar(float start_time, float end_time) {
×
NEW
381
    std::cout << "SVG Export - Adding scalebar (length: " << scalebar_length_ << " time units)" << std::endl;
×
382

383
    // Get the same MVP matrices used for rendering to ensure proper coordinate conversion
NEW
384
    auto const y_min = gl_widget_->getYMin();
×
NEW
385
    auto const y_max = gl_widget_->getYMax();
×
386

387
    // We'll use a simple orthographic projection for the scalebar
388
    // Place it in the bottom-right corner with some padding
NEW
389
    int const padding = 50;  // pixels from edges
×
NEW
390
    int const bar_height = 4; // pixels thick
×
391
    
392
    // Calculate the scalebar in data coordinates
393
    // The scalebar represents scalebar_length_ time units
NEW
394
    float const scalebar_start_time = end_time - static_cast<float>(scalebar_length_);
×
NEW
395
    float const scalebar_end_time = end_time;
×
396
    
397
    // Position the scalebar at the bottom of the canvas (in data Y coordinates)
NEW
398
    float const scalebar_y = y_min;
×
399
    
400
    // Create vertices for the scalebar endpoints in data space
NEW
401
    glm::vec4 const start_vertex(scalebar_start_time, scalebar_y, 0.0f, 1.0f);
×
NEW
402
    glm::vec4 const end_vertex(scalebar_end_time, scalebar_y, 0.0f, 1.0f);
×
403
    
404
    // Use identity matrices for simple coordinate transformation
405
    // We need to transform from data coordinates to SVG coordinates
NEW
406
    glm::mat4 const Model(1.0f);
×
NEW
407
    glm::mat4 const View(1.0f);
×
408
    
409
    // Create projection matrix that maps [start_time, end_time] x [y_min, y_max] to NDC
NEW
410
    float const x_range = end_time - start_time;
×
NEW
411
    float const y_range = y_max - y_min;
×
412
    
NEW
413
    glm::mat4 Projection(1.0f);
×
414
    // Scale from data range to [-1, 1]
NEW
415
    Projection[0][0] = 2.0f / x_range;
×
NEW
416
    Projection[1][1] = 2.0f / y_range;
×
417
    // Translate to center the range at origin
NEW
418
    Projection[3][0] = -1.0f - 2.0f * start_time / x_range;
×
NEW
419
    Projection[3][1] = -1.0f - 2.0f * y_min / y_range;
×
420
    
NEW
421
    glm::mat4 const mvp = Projection * View * Model;
×
422
    
423
    // Transform the endpoints to SVG coordinates
NEW
424
    glm::vec2 const svg_start = transformVertexToSVG(start_vertex, mvp);
×
NEW
425
    glm::vec2 const svg_end = transformVertexToSVG(end_vertex, mvp);
×
426
    
427
    // Adjust the scalebar to be in the bottom-right corner
428
    // Calculate the width of the scalebar in pixels
NEW
429
    float const bar_width_pixels = std::abs(svg_end.x - svg_start.x);
×
430
    
431
    // Position in bottom-right corner
NEW
432
    float const bar_x = static_cast<float>(svg_width_) - bar_width_pixels - static_cast<float>(padding);
×
NEW
433
    float const bar_y = static_cast<float>(svg_height_) - static_cast<float>(padding);
×
434
    
435
    // Draw the scalebar as a black horizontal line
NEW
436
    QString const scalebar_line = QString(
×
437
        R"(<line x1="%1" y1="%2" x2="%3" y2="%4" stroke="#000000" stroke-width="%5" stroke-linecap="butt"/>)")
NEW
438
        .arg(bar_x)
×
NEW
439
        .arg(bar_y)
×
NEW
440
        .arg(bar_x + bar_width_pixels)
×
NEW
441
        .arg(bar_y)
×
NEW
442
        .arg(bar_height);
×
443
    
NEW
444
    svg_elements_.append(scalebar_line);
×
445
    
446
    // Add small vertical ticks at the ends
NEW
447
    int const tick_height = 8; // pixels
×
NEW
448
    QString const left_tick = QString(
×
449
        R"(<line x1="%1" y1="%2" x2="%3" y2="%4" stroke="#000000" stroke-width="2"/>)")
NEW
450
        .arg(bar_x)
×
NEW
451
        .arg(bar_y - tick_height / 2)
×
NEW
452
        .arg(bar_x)
×
NEW
453
        .arg(bar_y + tick_height / 2);
×
454
    
NEW
455
    QString const right_tick = QString(
×
456
        R"(<line x1="%1" y1="%2" x2="%3" y2="%4" stroke="#000000" stroke-width="2"/>)")
NEW
457
        .arg(bar_x + bar_width_pixels)
×
NEW
458
        .arg(bar_y - tick_height / 2)
×
NEW
459
        .arg(bar_x + bar_width_pixels)
×
NEW
460
        .arg(bar_y + tick_height / 2);
×
461
    
NEW
462
    svg_elements_.append(left_tick);
×
NEW
463
    svg_elements_.append(right_tick);
×
464
    
465
    // Add text label showing the length
NEW
466
    QString const label_text = QString::number(scalebar_length_);
×
NEW
467
    float const label_x = bar_x + bar_width_pixels / 2.0f;
×
NEW
468
    float const label_y = bar_y - 10.0f; // Above the bar
×
469
    
NEW
470
    QString const label = QString(
×
471
        R"(<text x="%1" y="%2" font-family="Arial, sans-serif" font-size="14" fill="#000000" text-anchor="middle">%3</text>)")
NEW
472
        .arg(label_x)
×
NEW
473
        .arg(label_y)
×
NEW
474
        .arg(label_text);
×
475
    
NEW
476
    svg_elements_.append(label);
×
477
    
NEW
478
    std::cout << "SVG Export - Scalebar added at bottom-right corner" << std::endl;
×
NEW
479
}
×
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