-
Notifications
You must be signed in to change notification settings - Fork 3
Streaming
v1.4.0 — append data points to a live figure without rebuilding it. Typical uses are dashboards, telemetry and oscilloscopes.
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();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()
| 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). |
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);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;
});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 appendFour 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));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 |
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) |
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:MplStreamingChartControl StreamingFigure="{Binding LiveChart}" /><MplStreamingChart StreamingFigure="@_streamingFigure" />services.AddMatPlotLibNetSignalR();
// In a background service:
var sf = new StreamingFigure(figure);
registry.RegisterStreaming("dashboard-1", sf);
// sf.RenderRequested auto-publishes SVG to all connected clientsConnect 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);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| 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
MinRenderIntervalto 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
MaxDisplayPointssetting is respected
MatPlotLibNet 1.18.0