This document describes the requirements for a custom plotting library to replace Qwt in PlotJuggler. The requirements are derived from analyzing the actual usage of Qwt functionality in the codebase.
| Reason | Details |
|---|---|
| WASM Support | Qwt is tightly coupled to Qt's widget system, making WebAssembly deployment difficult. A custom library can target WebGL/Canvas directly. |
| Qt Independence | Decoupling from Qt allows alternative backends (native, web) and reduces dependency footprint. Qt remains an option but not a requirement. |
| Performance | Need to visualize millions of points at 50 Hz refresh rate. Requires GPU acceleration and aggressive optimization strategies that Qwt doesn't provide out of the box. |
| Pattern | Description |
|---|---|
| Scale | Millions of data points per curve |
| Refresh Rate | 50 Hz continuous updates |
| Streaming | Live data ingestion while plotting |
| Synchronized Plots | Multiple plots with linked X-axis (time), zoom, and pan |
The library should provide an API at the same abstraction level as Qwt - a general-purpose 2D plotting library. It is NOT PlotJuggler-specific. PlotJuggler will integrate with it the same way it currently integrates with Qwt.
| Component | Choice |
|---|---|
| Language | C++ (modern C++17/20) |
| Build System | CMake |
| Package Manager | Conan |
- Qt Independence: Core rendering logic should be decoupled from Qt, with Qt as one possible backend
- Hardware Acceleration: Support GPU-accelerated rendering (OpenGL, WebGL, Metal, Vulkan)
- Cross-Platform: Linux, macOS, Windows, and WebAssembly
- High Performance: Handle datasets with millions of points at 50 Hz
- Rectangular canvas area for rendering curves and annotations
- Support multiple rendering backends:
- Software rendering (CPU-based, fallback)
- OpenGL (desktop)
- WebGL (WASM)
- Metal (macOS - optional)
- Configurable background color
- Mouse tracking on canvas
- Axes positioned around the canvas:
- X-axis at bottom
- Y-axis at left
- (Optional) Secondary axes at top/right
- Axis widgets with labels and tick marks
- Canvas alignment to scale tick marks
- Margins and spacing configuration
- Data space: Floating-point coordinates (double precision)
- Pixel space: Integer coordinates for rendering
- Bi-directional transformation between spaces
- Support for inverted/flipped axes
| Style | Description |
|---|---|
Lines |
Connected line segments between points |
Dots |
Individual points rendered as filled circles |
LinesAndDots |
Lines with point markers at each data point |
Sticks |
Vertical lines from each point to the X-axis |
Steps |
Staircase pattern (horizontal then vertical) |
StepsInverted |
Staircase pattern (vertical then horizontal) |
- Line color (RGB/RGBA)
- Line width (1.0, 1.5, 2.0, 3.0 typical values)
- Dot/point size (derived from line width)
- Visibility toggle
- Antialiasing enable/disable
- Curve title/name for legend
- Point Filtering: Reduce rendered points when zoomed out (e.g.,
FilterPointsAggressive) - Polygon Clipping: Clip curve segments to visible canvas area
- Dirty Rect Updates: Only repaint changed regions when possible
- Level-of-Detail: Adapt rendering detail based on zoom level
RenderAntialiased: Smooth line rendering- Clip to canvas bounds
class PlotSeriesData {
virtual size_t size() const = 0;
virtual PointF sample(size_t index) const = 0;
virtual RectF boundingRect() const = 0;
};- X values represent time, Y values represent data
- Time offset support (shift all X values by offset)
- Index lookup from X value (for cursor tracking)
- Arbitrary X/Y data (not time-based)
- Merged from two separate time series (X from series A, Y from series B)
getVisualizationRangeX(): Return min/max X of datagetVisualizationRangeY(Range rangeX): Return min/max Y within X range- Used for auto-zoom calculations
- Transformed/processed data caching
- Reset cache on data update
- Incremental cache updates for streaming data
- Tick mark positions (major and minor)
- Tick labels with configurable formatting
- Custom label drawing (e.g., date/time formatting)
- Scale divisions
- Linear scale calculation
- Floating point attribute (axis doesn't include 0 unless data requires)
- Auto-scale from data range
- Manual scale bounds
class ScaleMap {
double transform(double dataValue) const; // data -> pixel
double invTransform(double pixelValue) const; // pixel -> data
double s1() const; // data min
double s2() const; // data max
};- Per-axis enable/disable
- Axis scale setter:
setAxisScale(axisId, min, max) - Auto-scaling toggle
- Inverted axis direction (min at top/right)
- Format timestamps as human-readable dates
- Multiple format options:
hh:mm:ss.z(time only)hh:mm:ss.z\nyyyy MMM dd(time and date)
- Click and drag to select zoom rectangle
- Rubber band visualization during selection
- Minimum zoom size threshold
- Accept/reject based on selection size
- Zoom centered on cursor position
- Configurable zoom factor per wheel step
- Per-axis zoom modes:
BOTH_AXES: Uniform zoomX_AXIS: Only zoom XY_AXIS: Only zoom Y
- Wheel on axis widget zooms only that axis
- Axis limits (min/max bounds)
- Aspect ratio preservation (for XY plots)
- Minimum/maximum zoom levels
- Zoom in key (Ctrl+Plus)
- Zoom out key (Ctrl+Minus)
- Reset zoom / zoom to fit
- Click and drag to pan the view
- Configurable mouse button:
- Middle mouse button (default)
- Ctrl + Left mouse button
- Left mouse button (alternative mode)
- Pan cursor feedback
- Optional axis limits (prevent panning past data bounds)
- Position at (x, y) coordinate
- Symbol types:
- Ellipse/Circle
- (Extensible to other shapes)
- Symbol attributes:
- Fill color
- Border pen
- Size
- Vertical line at X position
- Horizontal line at Y position
- Line style (solid, dashed, dotted)
- Line color and width
- Position at (x, y)
- Rich text support (HTML-like formatting)
- Background brush with configurable alpha
- Border pen (optional)
- Font configuration
- Text alignment (left, center, right)
- Label alignment relative to position
- Show/hide individual markers
- Visibility based on data availability
- Follows X position (time for timeseries)
- Shows values of all curves at that X
- Color-coded per curve
| Mode | Description |
|---|---|
LINE_ONLY |
Just the vertical line |
VALUE |
Line + numeric values |
VALUE_NAME |
Line + values + curve names |
- Show marker dot on each curve at tracker X position
- Color matches curve color
- Hide when curve point outside visible range
- Secondary tracker for delta comparison
- Show delta values (current - reference)
- Different color from primary tracker
- Text box positioned to avoid overlapping tracker line
- Flip position when reaching canvas edge
- Semi-transparent background
- Rendered inside the plot canvas
- Configurable position (corners, custom alignment)
- Draggable position (optional)
- Curve color indicator (line segment or symbol)
- Curve name/title
- Click to toggle curve visibility
- Visual feedback for hidden curves (grayed text)
- Background with border
- Border radius (rounded corners optional)
- Font size configuration
- Margins and spacing
- Collapse/expand toggle button
- Mouse wheel on legend adjusts font size
- Click legend item toggles visibility
- Click collapse button to minimize
- Major grid lines for X and Y
- Minor grid lines (optional)
- Grid line style (solid, dotted, dashed)
- Grid line color and width
- Enable/disable per axis
- Enable/disable minor lines independently
- Draw colored rectangles based on data series values
- Color mapped from data values via colormap
- Time-aligned regions
class PlotItem {
virtual int rtti() const = 0; // Runtime type identification
virtual void draw(Painter*, ScaleMap& xMap, ScaleMap& yMap, RectF& canvasRect) = 0;
virtual RectF boundingRect() const = 0;
void attach(Plot* plot);
void detach();
void setVisible(bool);
void setZ(double); // Stack order
};- PNG (raster)
- JPEG (raster)
- SVG (vector)
- Configurable resolution/dimensions
- Copy to clipboard
- Save to file dialog
class PlotRenderer {
void render(Plot* plot, Painter* painter, Rect targetRect);
};- Press, release, move, wheel
- Button identification (left, middle, right)
- Modifier keys (Ctrl, Shift, Alt)
- Configurable mouse patterns for actions
- Pattern matching for zoom, pan, etc.
- Drag and drop support (for curve dragging)
- Accept/reject drop based on data type
| Metric | Target |
|---|---|
| Points per curve | 1,000,000+ (millions) |
| Curves per plot | 20+ |
| Replot rate | 50 Hz (20ms budget) |
| Zoom/pan latency | <20ms |
| Streaming ingest | Continuous append without frame drops |
- Point decimation: Reduce to ~2-4x pixel width points when zoomed out
- GPU-based rendering: Offload line/point drawing to shaders
- Incremental updates: Only upload new data to GPU for streaming
- Spatial indexing: Fast range queries (e.g., for Y min/max in X range)
- Double buffering: Render next frame while displaying current
- Level-of-detail: Pre-computed min/max pyramids for fast zoom-out
Reference: KDAB - A Speed-Up for Charting on Embedded
Instead of rendering individual points, render one vertical line per pixel column spanning from the minimum to maximum Y value in that column's X range. This reduces millions of points to ~1000-2000 line segments (screen width).
Data Structure: Min-Max Tree
- Binary tree where each node stores (min, max) of its subtree
- Array-based layout (infix ordering) for cache efficiency
- Bit operations for parent/child navigation (no pointers)
- Query: O(log n) for min/max in any X range
- Update: O(log n) append, only bubbles up if min/max changes
Performance Results (from KDAB):
| Points | Naive Rendering | Min-Max Tree |
|---|---|---|
| 300,000 | >2000 ms | ~3 ms |
Why this matters for PlotJuggler:
- 1M points → ~1000 vertical lines (one per pixel column)
- Streaming: O(log n) update per new point
- Zoom: Query any range in O(log n)
- Memory: 2x overhead (min + max per node) but worth it
- Zero-copy data interface where possible (point to user's data)
- Lazy bounding rect calculation with caching
- GPU buffer reuse (avoid re-allocation per frame)
- Memory-mapped file support for very large datasets (optional)
- Append-only data path optimized for minimal overhead
- Ring buffer support for fixed-window streaming
- Async data upload to GPU (don't block main thread)
+-------------------+
| Application |
+-------------------+
|
+-------------------+
| PlotLib Core | <- Qt-independent
| (Data, Math, |
| Rendering API) |
+-------------------+
|
+-------------------+ +-------------------+ +-------------------+
| Qt Backend | | Web/Canvas Backend| | Other Backend |
| (QWidget, | | (WebGL, Canvas2D) | | (SDL, etc.) |
| QPainter, | | | | |
| QOpenGLWidget) | | | | |
+-------------------+ +-------------------+ +-------------------+
- Data series interfaces
- Coordinate transformation math
- Curve algorithms (line simplification, point filtering)
- Scale calculations
- Color and style definitions
- Event abstraction
- Abstract Painter interface
- Abstract Widget/Canvas interface
- Input event translation
- Timer/animation support
- Native OpenGL support
- High-DPI display handling
- System color scheme integration
- WebGL 2.0 rendering
- Canvas 2D fallback
- Touch event handling
- Browser-compatible event loop
- Memory constraints awareness
| Qwt Class | Purpose | Custom Equivalent Needed |
|---|---|---|
QwtPlot |
Main plot widget | Plot |
QwtPlotCanvas |
Software canvas | PlotCanvas |
QwtPlotOpenGLCanvas |
OpenGL canvas | GLPlotCanvas |
QwtPlotCurve |
Curve rendering | PlotCurve |
QwtSeriesData<QPointF> |
Data interface | PlotSeriesData |
QwtScaleMap |
Coordinate transform | ScaleMap |
QwtScaleDraw |
Axis drawing | ScaleDraw |
QwtScaleEngine |
Scale calculation | ScaleEngine |
QwtPlotZoomer |
Rectangle zoom | PlotZoomer |
QwtPlotMagnifier |
Wheel zoom | PlotMagnifier |
QwtPlotPanner |
Pan/drag | PlotPanner |
QwtPlotMarker |
Point/line markers | PlotMarker |
QwtSymbol |
Marker symbols | MarkerSymbol |
QwtPlotLegendItem |
In-canvas legend | PlotLegend |
QwtPlotGrid |
Grid lines | PlotGrid |
QwtPlotItem |
Base plot item | PlotItem |
QwtPlotRenderer |
Export rendering | PlotRenderer |
QwtText |
Rich text labels | PlotText |
QwtPainter |
Optimized painting | Painter |
QwtEventPattern |
Input patterns | EventPattern |
The following Qwt features are not used by PlotJuggler and do not need to be implemented:
- Polar plots (
QwtPolarPlot) - Bar charts (
QwtPlotBarChart) - Spectrograms / heatmaps (
QwtPlotSpectrogram) - Histogram (
QwtPlotHistogram) - Vector fields (
QwtPlotVectorField) - Trading curves (OHLC)
- Dials, knobs, sliders
- Compass, analog clock widgets
- Multi-bar charts
- Shape items
- SVG items
- Interval curves
- Curve fitting (Bezier, spline)
- Raster items
- Plot canvas with software rendering
- Basic curve rendering (lines, dots)
- Axis system with linear scales
- Coordinate transformation
- Mouse wheel zoom
- Rectangle zoom selection
- Panning
- Event handling abstraction
- Point and line markers
- Text labels
- Grid lines
- Curve tracker
- Legend
- Export (PNG, SVG)
- Custom plot items
- Date/time axis
- OpenGL backend
- WebGL backend for WASM
- Performance optimization
- Render known datasets and compare to reference images
- Test all curve styles
- Test zoom/pan states
- Benchmark with 100k, 1M, 10M points
- Measure replot time at 50 Hz target
- Profile memory usage and GPU memory
- Streaming throughput tests (points/second)
- Test on all target platforms (Linux, macOS, Windows)
- Verify WASM build and runtime in major browsers
- Test touch input on mobile browsers
- Multiple plots with linked axes
- Verify frame timing consistency
- Test zoom/pan propagation latency