|
QtIndustrialWidgets 2.3.0
Modern, High-Performance Industrial Instruments & SCADA Widgets for Qt
|
QtIndustrialWidgets::StripChart is a real-time multi-channel scrolling oscilloscope and strip chart recorder designed for industrial test benches, data acquisition (DAQ) systems, SCADA telemetry, and embedded instrumentation.
In QtIndustrialWidgets 2.1.0, StripChart features a true nanosecond time axis, high-throughput bulk insertion APIs, and pixel-column M4 decimation for data streams of up to millions of points per channel. Measured paint and insertion times, with machine and method, are in the Benchmark Report.
In earlier versions, StripChart used sample buffer index as its horizontal axis. In version 2.0.0+, StripChart supports dual horizontal axis modes:
XAxisMode::SampleIndex: Legacy mode where points are positioned by index in the circular buffer.XAxisMode::Time: Nanosecond-precision real-time axis based on std::chrono::nanoseconds.All timestamps in the public API use std::chrono::nanoseconds (64-bit integer), eliminating floating-point jitter and ambiguities between seconds, milliseconds, or microseconds:
TimeLabelFormat::Relative)**: Displays time relative to the leading edge (e.g., -10 s ... 0 s). Labels automatically scale units (ns, µs, ms, s, min) and live in the cached background reticle.TimeLabelFormat::Absolute)**: Maps timestamps to wall-clock time via setTimeOrigin(const QDateTime &origin). Dynamic labels update in real time (HH:mm:ss.zzz).
Vertical time grid lines adapt dynamically to the visible span using the standard 1-2-5 decade progression (1, 2, 5, 10, 20, 50, etc.), ensuring intuitive intervals.
Switching xAxisMode() between SampleIndex and Time clears all buffered sample data across all channels (ring contents, sample counts, latest values, timestamps, and decimation states). This prevents mixing incompatible timebases (sample index vs nanoseconds). Channel configuration metadata (name, color, pen width, visibility) and the diagnostic rejectedSampleCount() are preserved.
When using explicit-timestamp APIs (addSample, addSamples, addUniformSamples, addSynchronousSamples) while in SampleIndex mode, timestamps are ignored and samples are stored using the channel's running sample index (totalSamples), preventing channel poisoning. A warning is logged on the first occurrence per channel.
Rendering raw waveforms with millions of data points every frame is CPU-bound and does not scale. StripChart integrates an internal M4 decimation engine based on the Jugel et al. M4 algorithm. In the measured streaming cases the number of heap allocations per frame does not depend on the number of samples for continuous signals; with NaN gaps it grows with the number of visible gaps (see the Benchmark Report).
The plot is W_dev = round(W_logical * devicePixelRatioF()) device pixels wide, and every decimation bucket corresponds to exactly one device column (see "Bucket-to-column rule" below). For each bucket:
(t_first, y_first)(t_min, y_min)(t_max, y_max)(t_last, y_last)≤ 4 · W_dev points, rendered as contiguous QPolygonF polylines.
k = floor(t · W_dev / span)) or to the absolute sample index (k = floor(i · W_dev / capacity)), and the visible window always starts and ends on bucket boundaries. Advancing the window by less than one bucket moves no sample to another column; advancing it by one bucket shifts the decimated trace by exactly one device column.
W_dev buckets, and bucket k occupies device column k − kStart of the plot, in both axis modes and at any device pixel ratio, including fractional ones such as 1.25 and 1.5.k = floor(x · W_dev / den) with den = time span in ns or buffer capacity, computed with a portable 128-bit multiply/divide). Bucket widths are therefore floor or ceil of den / W_dev and need not be equal; in Time mode the window covers exactly the configured time span for spans of at least W_dev ns; shorter spans use 1 ns buckets and the window is W_dev ns wide (time labels describe that window).DecimationMode::Auto switch a sample moves by at most half a device pixel.W_dev newest buckets. When the buffer has wrapped, the oldest, partial bucket (fewer than capacity / W_dev samples) can fall out on the left, and with a full buffer the newest sample lies less than one device pixel from the right edge.
DecimationMode::Auto: Automatically activates M4 decimation when the visible sample count exceeds W_dev (plot width in device pixels); otherwise renders raw samples (default).DecimationMode::Always: Forces M4 decimation regardless of sample count.DecimationMode::Off: Renders raw samples directly.
To guarantee strictly bounded frame times ($O(W)$) even when decimating dense high-frequency noise:
Always or triggered by Auto), waveforms are drawn with a cosmetic 1-device-pixel pen without antialiasing (Qt::FlatCap, Qt::MiterJoin; the end pixels of each segment are drawn explicitly, see "Bucket-to-column rule"). In dense M4 min/max zig-zags (up to ~7,680 vertices on a 1920-px plot), antialiased wide pens force QPainter's rasterizer (QStroker) to compute outline polygons for every zig-zag segment, which is expensive for dense data. Drawing with a cosmetic 1-device-pixel unaliased pen avoids QStroker outline synthesis entirely. Measured paint times are in the Benchmark Report.Off or when sample count $\le W_{dev}$), user-configured channel pen widths and antialiasing remain active for smooth visual appearance.Visible thickness switch in Auto mode (expected behaviour): Because of the rule above, a channel in DecimationMode::Auto changes appearance at runtime. While its visible sample count is ≤ W_dev it is drawn raw, with its penWidth (default 1.8 px, set via addChannel()) and antialiasing; as soon as the visible sample count exceeds W_dev (typically while the buffer fills the time window) it switches to the 1-device-pixel unaliased pen, so the trace becomes visibly thinner. The decision is taken per channel on every frame, so the trace becomes thick again if the visible sample count drops back to ≤ W_dev (for example after widening the plot or shortening the visible window). To avoid the switch, set DecimationMode::Always for a consistent look at all fill levels, or keep the channel penWidth close to 1 so both paths look alike.
All insertion functions must be called from the GUI thread:
t >= lastTimestamp). Out-of-order samples are rejected, counted in rejectedSampleCount(channelId), and logged with a rate-limited warning.addDataPoint(channelId, value) while in Time mode stamps samples with the internal monotonic clock (QElapsedTimer). This represents GUI arrival time (subject to event loop latency) rather than sensor acquisition time.paintEvent (or on programmatic calls to yMinimum() / yMaximum()) by scanning only the visible window ($O(W)$). Dynamic hysteresis expands range immediately on spikes and delays range shrinking until 30 consecutive frames with data span < 50% of the range, preventing cached grid redraw churn.
Polylines break automatically into separate segments when:
Δt > gapThreshold.gapThreshold = 0 ns): the threshold is set dynamically to 4 * EMA(Δt) (Exponential Moving Average of inter-sample intervals).
QPainter polylines.--- when the latest telemetry value is non-finite.
A non-finite sample is how a sensor dropout reaches the chart, and a dropout must never be hidden: a trace that looks continuous across a failed sensor is misleading.
Each channel stores circular Structure-of-Arrays (SOA) buffers:
std::vector<qint64> timestamps (8 bytes/sample)std::vector<double> values (8 bytes/sample)Capacity can be configured up to 2^24 (16,777,216 samples per channel = 256 MB per channel):
If the circular buffer is full and cannot cover the configured timeSpan(), isTimeWindowFullyCovered() returns false and a warning suggests increasing capacity.
In version 2.0.0, the public channel(int) method and ChannelInfo struct were removed to encapsulate internal storage.
Replace previous calls as follows:
| Version 1.x Call | Version 2.0.0 Replacement |
|---|---|
chart->channel(id)->name | chart->channelName(id) |
chart->channel(id)->color | chart->channelColor(id) |
chart->channel(id)->visible | chart->isChannelVisible(id) |
chart->channel(id)->penWidth | chart->channelPenWidth(id) |
chart->channel(id)->count | chart->channelSampleCount(id) |
chart->channel(id)->latestValue | chart->channelLatestValue(id) |
In version 2.0.1, switching xAxisMode() explicitly clears all channel sample buffers to prevent timestamp poisoning. If your application dynamically toggles modes, ensure that acquisition buffers are re-streamed or restarted after changing modes.