-
Notifications
You must be signed in to change notification settings - Fork 2
Streaming
v1.4.0 — First-class live data support. Dashboards and telemetry feeds can append data points without rebuilding the figure.
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 |
|---|---|
DoubleRingBuffer |
Fixed-capacity circular buffer. ReaderWriterLockSlim. Never allocates on append. |
StreamingSnapshot |
Immutable point-in-time copy (double[] X, double[] Y, long Version). |
IStreamingSeries |
Contract: AppendPoint, AppendPoints, Clear, Version, Count, Capacity, CreateSnapshot. |
StreamingSeriesBase |
Abstract base with twin ring buffers, version counter, ComputeDataRange. |
StreamingFigure |
Wraps Figure. Render timer, version tracking, ApplyAxisScaling(), RenderRequested event. |
AxisScaleMode |
Sealed record hierarchy: Fixed, AutoScale, SlidingWindow(size), StickyRight(size). |
Ring-buffer-backed line chart. Default capacity 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);Ring-buffer-backed scatter plot. Default capacity 10,000.
var series = ax.StreamingScatter(configure: s =>
{
s.Color = Colors.Orange;
s.MarkerSize = 4;
});Y-only storage. X computed from SampleRate + offset. Optimized for oscilloscope / audio / telemetry. Default capacity 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 (O/H/L/C). BarAppended event for indicator auto-attach. Default capacity 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 |
11 incremental technical indicators, all O(1) per append. Auto-attach to StreamingCandlestickSeries via the BarAppended event. Each indicator owns its own StreamingLineSeries output — zero renderer changes.
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 consume StreamingFigure through the same pattern: subscribe to RenderRequested, marshal to UI thread, invalidate.
| 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 streaming series without System.Reactive dependency:
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/web scenarios, the SvgDiffEngine computes minimal patches between renders — typically 10x smaller than 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 — saves 50% memory vs XY - Ring buffer never allocates on append;
CreateSnapshot()allocates once per render - For >100K points, consider LTTB downsampling on the snapshot (existing
MaxDisplayPointsrespected)