┌─────────────────────────────────────────────────────────┐
│ Application (PlotJuggler, standalone, WASM) │
├─────────────────────────────────────────────────────────┤
│ High-Level API: Plot │
│ (owns axes, grid, renderers, interaction handlers) │
├─────────────────────────────────────────────────────────┤
│ Components │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │PlotCurve │ │PlotAxis │ │PlotGrid │ │PlotLegend │ │
│ │PlotMarker│ │PlotZoomer│ │PlotPanner│ │PlotMagnif.│ │
│ └──────────┘ └──────────┘ └──────────┘ └───────────┘ │
├─────────────────────────────────────────────────────────┤
│ Core Services │
│ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │LineRenderer│ │MarkerRender│ │TextRenderer │ │
│ │(AA quads) │ │(SDF shapes)│ │(fontstash) │ │
│ └────────────┘ └────────────┘ └──────────────────┘ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │PlotArea │ │ScaleMap │ │M4Decimation │ │
│ │(layout) │ │(transforms)│ │(MinMaxTree) │ │
│ └────────────┘ └────────────┘ └──────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Renderer (Sokol GFX wrapper) │
├─────────────────────────────────────────────────────────┤
│ Sokol: sokol_gfx.h | sokol_app.h | sokol_fontstash.h │
│ Backends: OpenGL 3.3 | Metal | D3D11 | WebGL2 │
└─────────────────────────────────────────────────────────┘
| Component | Choice | Rationale |
|---|---|---|
| GPU abstraction | Sokol | WASM-first, minimal footprint, single-header, zlib license |
| Text rendering | sokol_fontstash + stb_truetype | No external font deps, works in WASM |
| Math | cglm | Lightweight, C-compatible |
| Testing | GoogleTest | Standard, FetchContent integration |
| Rejected | NanoVG, Skia, ImPlot, bgfx | See docs/research.md for full analysis |
PIMPL: Public API classes (Plot, Renderer, PlotCurve) hide Sokol/GPU types from headers.
Attach/Detach: PlotCurves are externally owned; attached to Plot via non-owning pointers. Curves can move between plots.
Event Abstraction: Backend-agnostic POD event structs (ScrollEvent, MouseButtonEvent, etc.) with adapters for Sokol and Qt.
M4 Decimation: Per pixel column, stores 4 values (first/min/max/last Y) for visually-lossless rendering of millions of points as ~2000 segments.
Fragment Shader AA: Lines rendered as GPU-expanded quads; antialiasing via distance-function in fragment shader (no MSAA needed).
- Owns: PlotArea, LineRenderer, MarkerRenderer, TextRenderer, PlotAxis (left+bottom), PlotGrid, PlotMagnifier, PlotPanner, PlotZoomer
- Holds: non-owning pointers to attached PlotCurves
- Auto-scales to data bounds by default
- Owns: PlotSeriesData (data container with MinMaxTree)
- References: CurveAttributes (color, width, style)
- Renders via:
render(PlotArea&, LineRenderer&, MarkerRenderer&) - Supports: 6 styles (Lines, Dots, LinesAndDots, Steps, StepsInverted, Sticks)
- Owns: x/y ScaleMap instances
- Manages: margins, canvas-to-plot-area coordinate mapping
- Provides: data-to-pixel and pixel-to-NDC transforms
- LineRenderer: Batched AA line segments (GPU quads, max 100K segments/batch)
- MarkerRenderer: SDF-based point markers (instanced quads)
- TextRenderer: Font atlas text (fontstash integration)
User data (doubles) → PlotSeriesData → MinMaxTree (built on set/append)
↓
Render frame: M4Decimation queries MinMaxTree for visible range
↓
~2000 segments → LineRenderer batch → GPU draw call
| Directory | Contents |
|---|---|
include/splot/ |
Public headers (PIMPL forward-decl only) |
src/ |
Implementations + Sokol shaders |
examples/ |
Numbered demo apps (Sokol + Qt) |
tests/ |
Unit tests (tests/unit/) and visual regression (tests/visual/) |
benchmarks/ |
Performance benchmarks |
third_party/ |
Sokol headers, stb, fontstash |
docs/ |
Architecture, rendering, coordinates, testing docs |
| Technique | Where | Effect |
|---|---|---|
| M4 Decimation | PlotCurve::render | 1M points → ~2000 segments |
| MinMaxTree | PlotSeriesData | O(log n) range queries |
| Fragment shader AA | LineRenderer | 100x faster than MSAA |
| Batch rendering | LineRenderer | One draw call per curve |
| Dirty tracking | StreamingBuffer | Only upload changed bytes |
| Scissor clipping | Plot::render | GPU clips to PlotArea region |
Measured results (RelWithDebInfo, 10M points):
- Frame time: ~2ms (target <20ms)
- Streaming rate: 88.6M pts/sec (target 100K pts/sec)
- Query latency: <1ms