Skip to content

Streaming

H.P. Gansevoort edited this page Sep 12, 2026 · 9 revisions

Streaming & Realtime

v1.4.0 — append data points to a live figure without rebuilding it. Typical uses are dashboards, telemetry and oscilloscopes.

Quick Start

using MatPlotLibNet;
using MatPlotLibNet.Models.Streaming;

// 1. Build a streaming figure
StreamingLineSeries? series = null;
var sf = Plt.Create()
    .WithTitle("Live Telemetry")
    .AddSubPlot(1, 1, 1, ax => { series = ax.StreamingPlot(capacity: 5000); })
    .BuildStreaming(TimeSpan.FromMilliseconds(33)); // 30fps throttle

// 2. Subscribe to render events (Avalonia example)
// sf.RenderRequested += () => Dispatcher.UIThread.Post(InvalidateVisual);

// 3. Append data from any thread
for (int i = 0; i < 1000; i++)
{
    series!.AppendPoint(i, Math.Sin(i * 0.1));
    await Task.Delay(10);
}

sf.Dispose();

Architecture

Data source (any thread)     Render timer          UI thread
       │                          │                    │
  AppendPoint(x, y)               │                    │
  [write lock: ns]                │                    │
  _version++                      │                    │
       │                    check DataVersion          │
       │                    vs lastRendered            │
       │                          │                    │
       │                    fire RenderRequested ─────►│
       │                          │            marshal │
       │                          │                    ▼
       │                          │          ApplyAxisScaling()
       │                          │          CreateSnapshot()
       │                          │          ChartRenderer.Render()

Key types

Type Description
RingBuffer<T> A fixed-capacity circular sequence of any element type: a value, a record, a point in time. One writer, many readers (ReaderWriterLockSlim). An append never allocates; when the buffer is full, the append itself overwrites the oldest element. Measured at 64 M appends/s for double and 59 M/s for a four-field struct.
RingBuffer<T>.Aggregate() Folds every held element into a caller-supplied state in one pass, without creating an intermediate collection. Computing the range of a 10 000-point series costs 33 bytes this way and 320 kB as a copy.
RingBufferExtensions Min() and Max() for any INumber<T>, computed under one read lock without allocating. MinOrNaN() and MaxOrNaN() do the same for the double ring buffer that an axis reads.
DoubleRingBuffer A thin forwarder to RingBuffer<double>, kept under the name it has had since v1.7, so the wrap-around arithmetic is written only once.
StreamingSnapshot Immutable point-in-time copy (double[] X, double[] Y, long Version).
IStreamingSeries Contract: AppendPoint, AppendPoints, Clear, Version, Count, Capacity, CreateSnapshot.
StreamingSeries Abstract base class with one RingBuffer<StreamingPoint>, a version counter and ComputeDataRange. It stores whole points in a single buffer instead of separate x and y buffers, so a snapshot is a single read and an x value can never be paired with a y value from a different sample. Renamed from StreamingSeriesBase in v1.13.0, because the repository bans the *Base suffix.
StreamingFigure Wraps a Figure. Owns the render timer and the version tracking, and provides ApplyAxisScaling() and the RenderRequested event.
AxisScaleMode A sealed record hierarchy with the modes Fixed, AutoScale, SlidingWindow(size) and StickyRight(size).

Streaming Series Types

StreamingLineSeries

A line chart backed by a ring buffer. The default capacity is 10,000.

var series = ax.StreamingPlot(capacity: 10_000, configure: s =>
{
    s.Color = Colors.Blue;
    s.LineWidth = 2.0;
    s.Label = "Sensor A";
});

series.AppendPoint(timestamp, value);

StreamingScatterSeries

A scatter plot backed by a ring buffer. The default capacity is 10,000.

var series = ax.StreamingScatter(configure: s =>
{
    s.Color = Colors.Orange;
    s.MarkerSize = 4;
});

StreamingSignalSeries

Stores Y values only. X is computed from SampleRate plus an offset. Optimized for oscilloscope, audio and telemetry data. The default capacity is 100,000.

var signal = ax.StreamingSignal(capacity: 100_000, sampleRate: 44100.0, configure: s =>
{
    s.Color = Colors.Green;
});

signal.AppendSample(amplitude);
signal.AppendSamples(buffer); // batch append

StreamingCandlestickSeries

Four parallel ring buffers, one each for open, high, low and close (O/H/L/C). Raises the BarAppended event, which streaming indicators use to attach themselves. The default capacity is 5,000.

var candles = ax.StreamingCandlestick(capacity: 5000);
candles.AppendBar(open: 100, high: 110, low: 95, close: 105);
candles.AppendBar(new OhlcBar(105, 115, 100, 110));

Axis Scale Modes

Control how axes auto-scale when streaming data arrives.

sf.DefaultConfig = new StreamingAxesConfig(
    new AxisScaleMode.SlidingWindow(100.0),  // X: show last 100 units
    new AxisScaleMode.AutoScale());           // Y: fit all visible data
Mode Behavior
Fixed User-set limits, never auto-adjusted
AutoScale Fit all data every render
SlidingWindow(size) Show last N X-units, scroll as data arrives
StickyRight(size) Like SlidingWindow but only scroll when at the right edge

Streaming Indicators

There are 11 incremental technical indicators, all O(1) per append. They attach to a StreamingCandlestickSeries through its BarAppended event. Each indicator writes its output to its own StreamingLineSeries, so the renderer needs no changes. The abstract base class is StreamingIndicator (renamed from StreamingIndicatorBase in v1.13.0).

var candles = ax.StreamingCandlestick(5000);
candles.WithStreamingSma(axes, 20);            // auto-appends SMA line
candles.WithStreamingBollinger(axes, 20, 2.0); // auto-appends 3 band lines
candles.WithStreamingRsi(axes, 14);            // auto-appends RSI line
Indicator Output Complexity
StreamingSma Rolling average O(1)
StreamingEma Exponential average O(1)
StreamingRsi Wilder's RSI (0–100) O(1)
StreamingBollinger 3 series: mid, upper, lower O(1)
StreamingMacd 3 series: MACD, signal, histogram O(1)
StreamingObv Cumulative volume O(1)
StreamingAtr Wilder's true range O(1)
StreamingStochastic 2 series: %K, %D O(1) amortized
StreamingWilliamsR Rolling min/max O(1) amortized
StreamingCci Mean deviation O(n/period)
StreamingVwap Cumulative price*vol / vol O(1)

Platform Integration

All 5 UI hosts use StreamingFigure in the same way: subscribe to RenderRequested, marshal the call to the UI thread, and invalidate the view.

Host Control Marshal
Avalonia MplStreamingChartControl Dispatcher.UIThread.Post
Uno MplStreamingChartElement DispatcherQueue.TryEnqueue
MAUI MplStreamingChartView MainThread.BeginInvokeOnMainThread
Blazor MplStreamingChart InvokeAsync + StateHasChanged
ASP.NET Core StreamingChartSession IChartPublisher.PublishSvgAsync

Avalonia example

<avalonia:MplStreamingChartControl StreamingFigure="{Binding LiveChart}" />

Blazor example

<MplStreamingChart StreamingFigure="@_streamingFigure" />

Server-push (ASP.NET Core)

services.AddMatPlotLibNetSignalR();

// In a background service:
var sf = new StreamingFigure(figure);
registry.RegisterStreaming("dashboard-1", sf);
// sf.RenderRequested auto-publishes SVG to all connected clients

Rx Integration

Connect IObservable<T> sources to a streaming series. No dependency on System.Reactive is needed:

using MatPlotLibNet.Data;

IObservable<StreamingPoint> sensorStream = ...;  // StreamingPoint(double X, double Y)
using var subscription = series.SubscribeTo(sensorStream);

IObservable<OhlcBar> priceStream = ...;
using var ohlcSub = candles.SubscribeTo(priceStream);

SVG Diff Engine

For Blazor and other web scenarios, SvgDiffEngine computes a minimal patch between two renders. The patch is typically 10x smaller than the full SVG:

var patch = SvgDiffEngine.Compute(previousSvg, currentSvg);
if (patch.IsFullReplace)
    SendFullSvg(currentSvg);
else
    SendPatches(patch.Patches); // only changed series groups

Performance Tuning

Scenario Buffer Throttle Memory
Dashboard (1/sec) 1,000 1000ms 16 KB
Telemetry (100/sec) 10,000 33ms 640 KB
Oscilloscope (10K/sec) 100,000 16ms 800 KB
Trading (OHLC, 10/sec) 5,000 100ms 160 KB

Tips:

  • Set MinRenderInterval to match your target FPS
  • Use StreamingSignalSeries (Y-only) for uniform sample rates; it saves 50% memory compared with an XY series
  • The ring buffer never allocates on append; CreateSnapshot() allocates once per render
  • For more than 100K points, consider LTTB downsampling on the snapshot; the existing MaxDisplayPoints setting is respected

Clone this wiki locally