NaN Data Handling¶
Sensor failures, uneven sampling intervals and anomalies in data processing pipelines can all produce NaN (Not a Number) values. If a plotting library cannot handle NaN correctly, axis ranges can end up wrong, curves can render badly, or the application can crash.
Qwt 7 handles NaN data correctly and optimizes rendering for it; the original Qwt 6 did not. In Qwt 6, NaN data pollutes boundingRect (returning invalid rectangles), produces incorrect pixel coordinates during mapping, breaks autoscaling and causes other bugs. Qwt 7 adds NaN handling at every stage, from the utility functions up to the rendering pipeline.
Note — Qwt 7 New Feature
Correct NaN data handling and performance optimization is one of the major improvements of Qwt 7 over the original Qwt 6.2.0. All core improvements were added in December 2025 by the project maintainer.
NaN Handling Architecture Overview¶
Qwt 7's NaN handling spans the entire 2D plotting pipeline, from the data layer to the rendering layer:
flowchart TD
A["Data Layer<br/>QwtSeriesData"] -->|"boundingRect() skips NaN"| B["Coordinate Calculation<br/>qwtBoundingRectT()"]
B -->|"Returns valid bounding rect"| C["Autoscale Layer<br/>QwtPlot"]
C -->|"Checks if boundingRect is finite"| D["Coordinate Mapping<br/>QwtPointMapper"]
D -->|"Every algorithm path skips NaN"| E["Rendering Layer<br/>QPainter"]
F["qwt_is_nan_or_inf()<br/>Unified NaN/Inf detection"] -.->|"Used by all layers"| A
F -.-> B
F -.-> D
style A fill:#4a90d9,color:#fff
style B fill:#4a90d9,color:#fff
style C fill:#e8833a,color:#fff
style D fill:#5cb85c,color:#fff
style E fill:#f0ad4e,color:#fff
style F fill:#9b59b6,color:#fff
Core Utility: qwt_is_nan_or_inf()¶
Source file: src/core/qwt_math.h
Qwt 7 introduces a unified NaN/Inf detection function family as the foundation for all NaN handling. The original Qwt 6 does not have such a unified detection utility.
Function Overloads¶
Three overloads are provided via SFINAE to cover different data types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Helper Templates¶
In addition to the base detection functions, the following utilities are provided:
| Function | Description |
|---|---|
qwtContainsNanOrInf(first, last) |
Check whether an iterator range contains NaN/Inf |
qwtRemoveNanOrInf(container) |
In-place removal of all NaN/Inf values from a container |
qwtRemoveNanOrInfCopy(container) |
Return a new container with NaN/Inf values removed |
NaN Handling in boundingRect¶
Source file: src/core/qwt_series_data.cpp
The Qwt 6 Problem¶
The original Qwt 6.2.0 qwtBoundingRectT() template does not skip NaN samples when computing the bounding rectangle of a data series. Due to IEEE 754 floating-point comparison semantics (NaN < x and NaN > x both evaluate to false), NaN values cause:
- Bounding rectangle boundaries to be polluted with invalid values
- Autoscaling to receive incorrect ranges, resulting in abnormal axis display
- Subsequent coordinate mapping to produce erroneous results
Qwt 7 Improvement¶
Qwt 7 adds NaN-skipping logic to both loops in qwtBoundingRectT<T>():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
isSampleNanOrInf() Type Overloads¶
isSampleNanOrInf() provides dedicated overloads for each sample type, ensuring correct NaN detection across all data types:
| Sample Type | Fields Checked |
|---|---|
QPointF |
x, y (delegates to qwt_is_nan_or_inf()) |
QwtPoint3D |
x, y, z |
QwtPointPolar |
azimuth, radius |
QwtIntervalSample |
value, interval.minValue, interval.maxValue |
QwtOHLCSample |
close, high, low, open, time |
QwtBoxSample |
position, whiskerLower, q1, median, q3, whiskerUpper |
QwtVectorFieldSample |
x, y, vx, vy |
QwtSetSample |
Handled separately within qwtBoundingRect(QwtSetSample) |
NaN Handling in Coordinate Mapping¶
Source file: src/plot/qwt_point_mapper.cpp
QwtPointMapper is the core class for coordinate mapping, where all downsampling and coordinate transformation paths are implemented. Qwt 7 ensures that every mapping path skips NaN points.
NaN Handling per Downsampling Algorithm¶
| Algorithm | Function | NaN Handling |
|---|---|---|
| Consecutive Duplicate Filtering | qwtToPolylineFiltered() |
Find first non-NaN point, continue on NaN in loop |
| Quad Reduce | qwtMapPointsQuad() |
Find first non-NaN point, continue on NaN in loop |
| Pixel-Column Reduce | qwtPixelColumnReduce() |
continue on NaN in loop |
| MinMax Bucket Reduce | qwtMinMaxBucketReduce() |
NaN pre-scan → select SIMD/scalar path |
| Scatter Mapping | qwtToPoints() / qwtToPointsFiltered() |
continue on NaN in loop |
| Image Rendering | qwtRenderDots() |
continue on NaN in loop |
Typical Processing Pattern¶
Most algorithms follow a unified pattern: find the first valid point as the starting point, then skip NaN in the main loop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
MinMax Bucket Reduce NaN Pre-scan (LTTB Path)¶
qwtMinMaxBucketReduce() is the LTTB downsampling algorithm new in Qwt 7. Its NaN handling is the most involved: it uses a pre-scan to decide whether to use the SIMD acceleration path:
flowchart TD
A["Extract raw Y pointer"] --> B{"Extraction\nsucceeded?"}
B -->|No| G["Generic path\nvirtual sample() access\nper-point NaN skip"]
B -->|Yes| C["NaN pre-scan\nscan Y values in visible range"]
C --> D{"Any NaN found?"}
D -->|No| E["SIMD fast path\nqwtSimdArgMinMax()"]
D -->|Yes| F["Scalar fast path\nraw pointer + per-point NaN check"]
style B fill:#4a90d9,color:#fff
style D fill:#4a90d9,color:#fff
style E fill:#5cb85c,color:#fff
style F fill:#f0ad4e,color:#fff
style G fill:#999,color:#fff
The pre-scan exists because a NaN sample can distort the SIMD path's argmin/argmax results. It requires a single O(n) pass; once NaN is detected, the code switches to the scalar path, which skips NaN per element. When the data contains no NaN (the common case), the SIMD path provides 3-4x acceleration.
NaN Semantics in the SIMD Module¶
Source files: src/core/qwt_simd_argminmax.h, src/core/qwt_simd_argminmax.cpp
qwtSimdArgMinMax() uses IEEE 754 comparison semantics to naturally ignore NaN values:
IEEE 754 Ordered Comparison¶
The SIMD implementation uses ordered comparison predicates _CMP_LT_OQ and _CMP_GT_OQ, which return false when either operand is NaN. Therefore, NaN values never become the new min/max and are "naturally ignored":
1 2 3 4 | |
All-NaN Input Fallback¶
When all elements are NaN, the initialized DBL_MAX/-DBL_MAX values are never updated. The public API adds a post-check:
1 2 3 4 5 6 7 8 9 10 11 | |
Curve Drawing NaN Handling Coverage¶
Source file: src/plot/qwt_plot_curve.cpp
All drawing styles now cover NaN handling:
| Drawing Style | NaN Handling | Implementation |
|---|---|---|
| Lines (all downsampling modes) | Handled | Delegates to QwtPointMapper |
| Dots (default path) | Handled | Delegates to QwtPointMapper |
| Symbols | Handled | Delegates to QwtPointMapper |
| Dots (MinimizeMemory path) | Handled | Direct iteration, skipped via isSampleNanOrInf() |
| Sticks | Handled | Direct iteration, skipped via isSampleNanOrInf() |
| Steps | Handled | Direct iteration, skipped via isSampleNanOrInf() |
Warning — Skipping is not breaking
In earlier versions the
Sticks,StepsandDotsMinimizeMemorypaths did not check for NaN; since commit17d0d13these paths (andclosestPoint()) have gained theisSampleNanOrInf()skip check, so every style is safe. Note, however, what the skip semantics mean: the Lines family and Steps "skip and bridge" NaN, so a gap is crossed by a line segment (a bridging chord) that does not exist in the data, and the polyline is not broken open at the gap; only per-sample styles likeDots/Sticksleave gaps naturally blank. If you need true "gap equals blank" break semantics, enable theQwtPlotCurve::BreakAtNancurve attribute (see Best Practices below), or split the series at the NaNs into multiple finite segments on the data side, one curve per segment. For the quantitative semantics matrix and pixel-level visual scans see the technical report.
NaN Protection in Autoscaling¶
Source files: src/plot/qwt_plot.cpp, src/plot/qwt_plot_axis.cpp
Autoscaling has two independent paths, each with its own form of NaN protection. QwtPlot::rescaleAxes() checks the validity of the item boundingRect and the finiteness of all four edges before merging ranges (the predicate is qwt_is_nan_or_inf(), which also catches Inf):
1 2 3 4 5 6 | |
QwtPlot::updateAxes(), the path taken during replot, judges only by width() >= 0 / height() >= 0: the boundingRect of an all-NaN series is the invalid rectangle QRectF(1, 1, -2, -2) (width and height both -2), which is skipped wholesale, leaving the axis interval invalid and ultimately falling back to the AxisData default range 0–1000; this is where "an all-NaN curve drops the axes back to 0–1000 and leaves the canvas blank" comes from:
1 2 3 4 5 6 | |
The dual protection across both paths ensures that even if boundingRect is accidentally polluted (e.g., by a future regression bug), autoscaling will not receive invalid range values.
NaN Handling in Other Modules¶
Contour Algorithm¶
The QwtRasterData CONREC contour algorithm detects NaN through accumulation sums: a NaN participating in addition makes the result NaN (the current source uses qwt_is_nan_or_inf(), which also catches Inf):
1 2 3 4 | |
Spectrogram Rendering¶
Spectrogram treats NaN values as data gaps, rendering them as transparent pixels. Earlier versions implemented a local bit-level NaN detector (qwtIsNaN) because qIsNaN() was not inlined and qt_is_nan lived in a Qt private header; the current source has removed it and uniformly uses qwt_is_nan_or_inf() from the core module. During rendering, NaN values are rendered as transparent pixels (0u):
1 2 3 | |
WithoutGaps Performance Flag¶
The QwtRasterData::WithoutGaps attribute flag tells the renderer that the data has no gaps, allowing NaN checks to be skipped for better performance:
1 2 | |
nanperf Performance Benchmark¶
Note — Qwt 7 New Example
examples/2D/nanperf/is a performance benchmark example added in Qwt 7 to compare rendering performance under different NaN distributions and downsampling modes. The original Qwt 6 does not have this example.
Overview¶
The nanperf example provides a GUI tool for interactively testing the impact of NaN data on rendering performance:
- 6 NaN distribution scenarios: Leading NaN, Leading & Trailing NaN, Middle NaN, Trailing NaN, X+Y NaN, X/Y Interleaved NaN
- 5 downsampling modes: ClipPolygons, FilterPoints, FilterPointsAggressive, FilterPointsPixel, FilterPointsLTTB
- Configurable parameters: Data point count (1,000 ~ 10,000,000), NaN ratio (0% ~ 99%), repeat count
- Bottleneck analysis: computes the difference between the SIMD cliff effect and the data reduction effect
UI Layout¶
The main window contains:
- Control bar: Mode selector, point count, NaN ratio, repeat count, action buttons
- 6 plot panels: Arranged in a 2×3 grid, each panel corresponding to one NaN distribution scenario
- Metrics table: Shows boundingRect time, replot time, FPS, NaN count for each (scenario × mode) combination
- Bottleneck analysis: Auto-generated analysis text isolating SIMD cliff effect from data reduction effect
NaN Distribution Scenarios¶
| Scenario | Description | Test Purpose |
|---|---|---|
| No NaN Baseline | No NaN reference | Performance comparison baseline |
| Leading NaN | NaN at beginning, signal follows | Tests impact of leading NaN on first-valid-point search |
| Leading & Trailing NaN | NaN at both ends, signal in middle | Tests impact of NaN at both boundaries |
| Middle NaN | Signal at both ends, NaN in middle | Tests impact of mid-sequence gaps on curve continuity |
| X+Y Middle NaN | Both X and Y are NaN (middle position) | Tests impact of non-monotonic X (breaks binary search optimization) |
| X/Y Interleaved NaN | Alternating X-only and Y-only NaN (middle position) | Tests impact of partial-coordinate NaN |
Bottleneck Analysis Logic¶
The BenchmarkRunner::analyze() method decomposes the performance difference into two factors:
- SIMD cliff effect (LTTB only): When any NaN exists in the data,
qwtMinMaxBucketReduce's pre-scan disables SIMD globally and falls back to the scalar path. Measured by comparing LTTB performance ratios with and without NaN. - Data reduction effect (all modes): After NaN points are skipped, fewer finite points need processing, so rendering is faster. Measured by comparing non-LTTB performance ratios with and without NaN.
The differential (delta) isolates the SIMD cliff effect:
- delta < 0: LTTB benefits more from data reduction (SIMD penalty negligible at low NaN ratios)
- 0 ≤ delta ≤ 0.1: SIMD penalty marginal (scalar overhead negligible at high NaN ratios)
- delta > 0.1: SIMD cliff clearly visible (LTTB penalized by scalar fallback)
Benchmark Results¶
Below are two sets of typical test results. Test conditions: 100,000 data points, 20 repetitions averaged. The FilterPointsLTTB figures were measured with the v7.4.2 implementation ( MinMax buckets, since v7.5.0 the attribute maps to the true LTTB ).
Low NaN Ratio (1%)¶
| Case | Mode | boundingRect (ms) | replot (ms) | FPS | NaN Points |
|---|---|---|---|---|---|
| No NaN Baseline | ClipPolygons | 8.615 | 116.850 | 8.6 | 0 |
| No NaN Baseline | FilterPointsLTTB | 8.777 | 12.440 | 80.4 | 0 |
| Leading NaN | FilterPointsLTTB | 8.089 | 9.726 | 102.8 | 1000 |
| Middle NaN | FilterPointsLTTB | 8.351 | 10.444 | 95.7 | 1000 |
| X+Y Middle NaN | FilterPointsLTTB | 8.257 | 9.601 | 104.2 | 1000 |
Bottleneck analysis:
- boundingRect: baseline 8.634 ms vs NaN 8.396 ms; cold-cache O(n) scan (cached in practice, re-computed only on data change)
- FilterPointsLTTB: NaN 9.849 ms vs baseline 12.440 ms (0.79x); LTTB benefits more from data reduction, SIMD penalty negligible at 1% NaN ratio
- Other modes: NaN 54.634 ms vs baseline 55.644 ms (0.98x); no SIMD path, ratio reflects pure data reduction
- delta = -0.19 → LTTB benefits more from data reduction
High NaN Ratio (99%)¶
| Case | Mode | boundingRect (ms) | replot (ms) | FPS | NaN Points |
|---|---|---|---|---|---|
| No NaN Baseline | ClipPolygons | 8.730 | 115.466 | 8.7 | 0 |
| No NaN Baseline | FilterPointsLTTB | 8.362 | 13.244 | 75.5 | 0 |
| Leading NaN | FilterPointsLTTB | 3.388 | 1.614 | 619.6 | 99000 |
| Middle NaN | FilterPointsLTTB | 3.468 | 1.762 | 567.6 | 99000 |
| X+Y Middle NaN | FilterPointsLTTB | 2.720 | 1.896 | 527.4 | 99000 |
Bottleneck analysis:
- boundingRect: baseline 8.540 ms vs NaN 3.164 ms; scanning is faster when NaN points are skipped
- FilterPointsLTTB: NaN 1.700 ms vs baseline 13.244 ms (0.13x); 99% of data skipped, rendering is much faster
- Other modes: NaN 5.524 ms vs baseline 54.860 ms (0.10x); pure data reduction effect
- delta = +0.03 → SIMD penalty marginal, scalar overhead negligible at high NaN ratio
Results Summary¶
The flowchart below summarizes both benchmark sweeps: at a 1% NaN ratio the LTTB path still beats the no-NaN baseline (0.79x replot time), and at a 99% ratio every mode speeds up dramatically (0.10–0.13x). The SIMD fallback penalty is negligible in both cases:
flowchart LR
subgraph LowNaN["1% NaN Ratio"]
A1["LTTB ratio: 0.79x<br/>(faster than baseline)"]
A2["Other ratio: 0.98x"]
A3["delta: -0.19<br/>LTTB benefits more"]
end
subgraph HighNaN["99% NaN Ratio"]
B1["LTTB ratio: 0.13x<br/>(7.7x faster)"]
B2["Other ratio: 0.10x"]
B3["delta: +0.03<br/>SIMD penalty negligible"]
end
LowNaN --> C["Conclusion: NaN skipping<br/>yields pure performance gain"]
HighNaN --> C
style C fill:#5cb85c,color:#fff
Key findings:
- NaN skipping never causes performance degradation: In all test scenarios, rendering with NaN data is faster than or equal to the no-NaN baseline, because skipping NaN reduces the number of finite points to process.
- SIMD cliff effect is negligible: Even at a 1% NaN ratio, where LTTB falls back to the scalar path because the pre-scan disables SIMD, performance still exceeds the no-NaN baseline (0.79x); the data reduction benefit far outweighs the SIMD loss.
- High NaN ratios speed up rendering: At 99% NaN, LTTB's replot time drops from 13.2ms to 1.7ms (7.7x speedup), and FPS rises from 75.5 to 619.6.
- NaN distribution does not affect performance: Different NaN distributions (leading, middle, trailing, interleaved) produce nearly identical performance, so Qwt 7 handles NaN samples at any position.
Running the nanperf Example¶
Note — Build & Run
The nanperf example is included in the build by default. After building with
build.ps1, findnanperf.exein thebuild/bin/directory.
1 2 3 4 5# Build (including examples) .\build.ps1 build # Run .\build\bin\nanperf.exeYou can also ensure examples are built via
-DQWT_CONFIG_BUILD_EXAMPLE=ON(default) during CMake configuration.
UI Operation Guide¶
- Adjust parameters: Set data point count, NaN ratio, and repeat count in the top control bar
- Apply & Redraw: Immediately apply current parameters to all 6 panels and redraw
- Run Benchmark Sweep: Run the full 5-mode × 7-scenario (including baseline) benchmark test
- Export Markdown: Export test results as a Markdown table file
Best Practices¶
Data Preparation¶
Mark missing samples with a quiet NaN (Qt's qQNaN() helper produces one) and append them to the sample vectors like any regular value; boundingRect() and the renderers skip them automatically, so the axis scaling stays correct without any extra work. Note that "skip" means the polyline is bridged across the gap rather than broken open; when a gap must appear blank, see the warning under "Curve Drawing NaN Handling Coverage" above, the BreakAtNan attribute below, and the technical report:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Choosing a Drawing Style¶
All drawing styles now skip NaN samples (see the coverage table above), so for data with NaNs the QwtPlotCurve::Lines style remains a good first choice: it has complete NaN handling, and the default-enabled FilterPointsLTTB paint attribute gives the best performance at the same time. Note that the Lines/Steps skip semantics are "bridging": a gap is crossed by a line segment instead of being left blank; when gaps must appear blank, enable the BreakAtNan curve attribute (next section), split the data into finite segments across multiple curves, or switch to the per-sample Dots/Sticks styles:
1 2 3 4 5 6 7 | |
Gap Semantics: the BreakAtNan Attribute¶
By default a curve renders one continuous polyline: NaN/Inf samples are skipped and the neighboring finite samples are connected by a bridging chord that has no correspondence in the data. The QwtPlotCurve::BreakAtNan curve attribute switches Lines and Steps to gap semantics instead: every run of consecutive finite samples is rendered as an independent polyline and gaps stay blank, like in matplotlib:
1 2 3 4 | |
Behavior details:
- Per-segment processing: each run is smoothed, fitted (
Fitted), clipped and filled on its own; neither the smoothing kernel nor the fill area crosses a gap, and the fill of each run closes to the baseline independently. - Per-segment downsampling: the downsampling budget of the paint attributes (
FilterPointsLTTBetc.) applies per run; short runs fall back to simpler filtering, which is the defining cost of per-segment rendering. - No effect on Dots/Sticks: these styles draw per sample and skip NaN/Inf samples anyway, so gaps are naturally blank.
- Cached run scan: the finite-sample runs are scanned once per data change (same cache lifecycle as the series boundingRect) and reused by every repaint, so interactive zoom/pan does not pay the O(n) scan again. Custom
QwtSeriesDataimplementations that mutate their content in place must triggerdataChanged()(e.g. by callingsetData()again) for the cache to refresh. - Default off: without the attribute nothing changes; gaps are bridged exactly as before.
Data Gaps in Spectrogram¶
In a spectrogram, NaN cells are rendered as transparent holes by default, usually exactly the desired effect for a QwtPlotSpectrogram. Only when the raster truly has no gaps should you set the QwtRasterData::WithoutGaps attribute via setAttribute() to skip the per-cell NaN checks:
1 2 3 4 5 | |
API Reference¶
- Curve Downsampling Algorithms: Detailed explanation of the four downsampling algorithms
- Curve: QwtPlotCurve rendering attribute configuration