Skip to content

Advanced

H.P. Gansevoort edited this page Nov 1, 2026 · 21 revisions

Advanced


Date Axes

Date axis with auto ticks

Plot time-series data with automatic tick placement. The DateTime[] overload sets the X axis to AxisScale.Date automatically.

using MatPlotLibNet;

DateTime[] dates = Enumerable.Range(0, 365)
    .Select(i => new DateTime(2025, 1, 1).AddDays(i))
    .ToArray();
double[] values = dates.Select((_, i) => Math.Sin(i / 30.0)).ToArray();

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(dates, values)
        .SetXLabel("Date"))
    .Save("dates.svg");   // ticks: "Jan 2025", "Feb 2025", …

The same call works for sub-day data:

using MatPlotLibNet;

DateTime[] hours = Enumerable.Range(0, 48)
    .Select(i => DateTime.Today.AddHours(i))
    .ToArray();
double[] measurements = hours.Select((_, i) => 20 + 5 * Math.Sin(i / 4.0)).ToArray();

Plt.Create().Plot(hours, measurements).Save("hourly.svg");
// ticks: "00:00", "06:00", "12:00", …

AutoDateLocator chooses the tick interval from years, months, weeks, days, hours, minutes, and seconds. To set the locator and formatter yourself:

using MatPlotLibNet;
using MatPlotLibNet.Rendering.TickFormatters;
using MatPlotLibNet.Rendering.TickLocators;

DateTime[] dates = Enumerable.Range(0, 30)
    .Select(i => new DateTime(2025, 1, 1).AddDays(i))
    .ToArray();
double[] values = dates.Select((_, i) => Math.Cos(i / 5.0)).ToArray();

var locator = new AutoDateLocator();

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(dates, values)
        .SetXTickLocator(locator)
        .SetXTickFormatter(new AutoDateFormatter(locator)))   // the formatter reads the interval the locator chose
    .Save("dates_custom.svg");

Math Text Labels

Math text labels with Greek letters and super/subscript

You can use a small subset of LaTeX syntax in any title, axis label, annotation, or legend entry. Wrap the math in $...$.

using MatPlotLibNet;

double[] x = Enumerable.Range(0, 50).Select(i => i * 0.2).ToArray();
double[] y = x.Select(v => 2.0 * v + Math.Sin(v)).ToArray();

// Verbatim strings (@"...") keep the backslashes: in a plain string "\a", "\b" and "\t" are escape characters.
Plt.Create()
    .WithTitle(@"$\alpha$ vs $\beta$ correlation")
    .AddSubPlot(1, 1, 1, ax =>
    {
        ax.WithTitle("R$^{2}$ = 0.97");
        ax.SetYLabel(@"$\sigma$ (Pa)");
        ax.SetXLabel(@"Time $\Delta t$ (ms)");
        ax.Plot(x, y);
    })
    .Save("math.svg");

Quick reference:

Syntax Renders as
$\alpha$ α
$\sigma^{2}$ σ²
$x_{i}$ xᵢ
$\pm$ ±
$\infty$ ∞
$\leq$ ≤
$\degree$C °C
$\frac{a}{b}$ fraction a/b (stacked)
$\sqrt{x}$ √x with overline bar
$\sqrt[3]{x}$ cube root of x
$\hat{x}$ x̂ (circumflex accent)
$\bar{y}$ ȳ (overline accent)
$\vec{v}$ v⃗ (arrow accent)
$\mathrm{sin}$ sin (roman/upright font)
$\mathbf{F}$ F (bold font)
$\mathcal{L}$ ℒ (calligraphic font)
$\mathbb{R}$ ℝ (blackboard bold)
$\text{ if }$ " if " (roman text inside math)
$a\quad b$ a b (em space)
$\left(...\right)$ scaling delimiters

Text outside $...$ renders as-is. SVG backends emit <tspan baseline-shift="super/sub"> for super/subscripts and stacked <tspan dy="..."> for fractions. Non-SVG backends (Skia, MAUI) fall back to plain-text Unicode substitution.

Math text also supports accents (\hat, \bar, \tilde, \dot, \vec), font variants (\mathrm, \mathbf, \mathit, \mathcal, \mathbb), \text{}, spacing (\,, \:, \;, \quad, \qquad), and 96 symbol mappings that cover Greek letters, math operators, arrows, set and logic symbols, and blackboard bold, for example ℝ, ℂ, ℤ, ℕ, ℚ, →, ⇒, ←, ↔, ∀, ∃, ∈, ∉, ∪, ∩.

MathText — fractions, sqrt, accents, font variants


Constrained Layout

Constrained layout computes the figure margins from the measured text extents instead of using hardcoded defaults.

using MatPlotLibNet;

double[] x = [2000, 2005, 2010, 2015, 2020];
double[] y1 = [282, 295, 309, 321, 331];
double[] y2 = [10.3, 13.0, 15.0, 18.2, 21.1];

// TightLayout() and ConstrainedLayout() are equivalent
Plt.Create()
    .TightLayout()
    .AddSubPlot(2, 2, 1, ax => { ax.SetYLabel("Population (millions)"); ax.Plot(x, y1); })
    .AddSubPlot(2, 2, 2, ax => { ax.SetYLabel(@"GDP ($\times 10^{9}$)");  ax.Plot(x, y2); })
    .Save("constrained.svg");

It measures the Y-tick label widths, the axis label sizes, and the subplot title heights, then computes exact SubPlotSpacing margins and clamps them to sensible ranges. Without either flag, the fixed defaults apply (MarginLeft=60, MarginBottom=50, …).

Outside legends

Outside legend — right margin reserved

Four LegendPosition values place the legend box outside the plot area: OutsideRight, OutsideLeft, OutsideTop, OutsideBottom. The constrained-layout engine measures the legend box with LegendMeasurer, which uses the same formulas as the renderer at draw time, and widens the matching figure margin by the box width plus a 16 px gap. The [10, 140] right-margin clamp is dynamic: for OutsideRight it rises to at least legendBoxWidth + 40, so a wide legend never clips.

using MatPlotLibNet;
using MatPlotLibNet.Models;

double[] x = Enumerable.Range(0, 100).Select(i => i * 0.1).ToArray();
double[] sin = x.Select(Math.Sin).ToArray();
double[] cos = x.Select(Math.Cos).ToArray();
double[] decay = x.Select(v => Math.Exp(-v / 5) * Math.Cos(v)).ToArray();

Plt.Create()
    .WithSize(900, 500)
    .TightLayout()
    .AddSubPlot(1, 1, 1, ax =>
    {
        ax.Plot(x, sin, s => s.Label = "sin(x)");
        ax.Plot(x, cos, s => s.Label = "cos(x)");
        ax.Plot(x, decay, s => s.Label = "exp(-x/5)·cos(x)");
        ax.WithLegend(l => l with { Position = LegendPosition.OutsideRight, Title = "Series" });
    })
    .Save("legend_outside.svg");

Without TightLayout() or ConstrainedLayout() the legend clips at the figure edge, because the margin reservation pass only runs when one of those flags is set.

Label collision avoidance

Dense pies, sunbursts, Sankeys, and bar charts can stack their labels on top of each other. LabelLayoutEngine is an iterative pair-wise repulsion solver that spreads them apart:

  • It computes each label's bounding rectangle with FontMetricsDefault.Current. That is the same provider the renderer uses, so layout and drawing agree byte for byte.
  • For each overlapping pair, it applies a minimum-translation vector (MTV) shift along the axis with the smaller overlap, weighted by each label's priority.
  • It clamps every label rectangle back inside the plot bounds on each pass.
  • It stops at convergence or after 20 iterations.
  • When a label has moved more than leaderThreshold pixels from its original anchor, it records a leader-line anchor (LeaderLineStart) so the caller can draw a connector from that anchor to the label's final position.

PieSeriesRenderer, SunburstSeriesRenderer, SankeySeriesRenderer, and BarSeriesRenderer use the engine automatically. TreemapSeriesRenderer uses a per-cell measured-fit check instead: each label is constrained to its own cell rectangle, so labels in different cells cannot collide.

You can also call the engine directly:

using MatPlotLibNet;
using MatPlotLibNet.Rendering;
using MatPlotLibNet.Rendering.Layout;
using MatPlotLibNet.Rendering.Svg;
using MatPlotLibNet.Styling;

var font = new Font { Size = 12 };

// Three labels that all want to sit at nearly the same spot.
LabelCandidate[] candidates =
[
    new(new Point(200, 150), "North", font),
    new(new Point(204, 152), "North-east", font),
    new(new Point(198, 155), "East", font, Priority: LabelPriority.High),
];
var plotBounds = new Rect(0, 0, 400, 300);

var placements = LabelLayoutEngine.Place(
    candidates,
    plotBounds,
    FontMetricsDefault.Current,
    leaderThreshold: 6.0);

var ctx = new SvgRenderContext();
foreach (var p in placements)
{
    if (p.LeaderLineStart is { } anchor)
        ctx.DrawLine(anchor, p.FinalPoint, new StrokeStyle(Colors.Black, 0.5, LineStyle.Solid));
    ctx.DrawText(p.Text, p.FinalPoint, p.Font, p.Alignment);
}

Console.WriteLine(ctx.GetOutput());

Layouts

GridSpec layout

GridSpec — Unequal Subplot Sizes

using MatPlotLibNet;
using MatPlotLibNet.Models;

double[] x = Enumerable.Range(0, 40).Select(i => i * 0.25).ToArray();
double[] y = x.Select(Math.Sin).ToArray();
string[] cats = ["Q1", "Q2", "Q3", "Q4"];
double[] vals = [12, 18, 9, 15];

Plt.Create()
    .WithGridSpec(2, 2, heightRatios: [2.0, 1.0], widthRatios: [3.0, 1.0])
    .AddSubPlot(GridPosition.Single(0, 0), ax => ax.Plot(x, y).WithTitle("Main"))
    .AddSubPlot(GridPosition.Single(0, 1), ax => ax.Scatter(x, y))
    .AddSubPlot(new GridPosition(RowStart: 1, RowEnd: 2, ColStart: 0, ColEnd: 2), ax => ax.Bar(cats, vals))   // the whole bottom row
    .Save("gridspec.svg");

Shared Axes

using MatPlotLibNet;

double[] x = Enumerable.Range(0, 40).Select(i => i * 0.25).ToArray();
double[] y1 = x.Select(Math.Sin).ToArray();
double[] y2 = x.Select(Math.Cos).ToArray();

Plt.Create()
    .AddSubPlot(2, 1, 1, ax => ax.ShareX("group1").Plot(x, y1))
    .AddSubPlot(2, 1, 2, ax => ax.ShareX("group1").Plot(x, y2))
    .Save("shared_x.svg");

Inset Axes

using MatPlotLibNet;

double[] x = Enumerable.Range(0, 200).Select(i => i * 0.05).ToArray();
double[] y = x.Select(v => Math.Sin(3 * v) * Math.Exp(-v / 4)).ToArray();
double[] xZoom = x.Where(v => v <= 1.0).ToArray();
double[] yZoom = y.Take(xZoom.Length).ToArray();

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y)
        .AddInset(0.6, 0.6, 0.35, 0.35, inset => inset
            .Plot(xZoom, yZoom).WithTitle("Detail")))
    .Save("inset.svg");

An inset is built where it stands: the callback gets a builder over the inset itself, so its legend, tick locators and formatters (an ITickFormatter of your own among them), scales, overlays and colour bar are the ones you set, and a chart drawn in an inset by one of the one-call verbs keeps the names it writes on its axes. Its grid is the theme's grid of the figure it is drawn under, and an inset can hold an inset. The y of an inset is its top edge counted down from the top of its parent (matplotlib's inset_axes counts up from the bottom).

The zoom of an inset

IndicateZoom() on the builder of an inset has the parent draw a rectangle over the region the inset shows, and the two lines from it to the inset's frame that matplotlib's rule keeps, so a reader sees where the detail was taken from. The rectangle spans the limits the inset draws, those you set on it or the ones it scales to its data:

using MatPlotLibNet;

double[] x = [.. Enumerable.Range(0, 200).Select(i => i / 20.0)];
double[] y = [.. x.Select(v => Math.Sin(v) + 0.3 * Math.Sin(5 * v))];

Plt.Create()
    .WithTitle("A closer look at the wiggle")
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y)
        .SetYLim(-1.5, 3.5)
        .AddInset(0.5, 0.03, 0.45, 0.35, inset => inset
            .Plot(x, y)
            .SetXLim(6, 7.5)
            .SetYLim(-0.7, 1)
            .IndicateZoom()))
    .Save("inset_zoom.svg");

A wavy blue line across a plot, with an inset above its middle that shows the stretch from 6 to 7.5 larger, a grey rectangle around that stretch on the line and two grey lines from the rectangle's top corners to the inset's bottom corners

The callback sets the colour, the opacity (half by default), the width and the dash pattern of the rectangle and the lines, and Connectors = false draws the rectangle alone. An inset that lies wholly within its region, or a region wholly within its inset, draws no lines. Only a Cartesian parent draws the indicator, and it is drawn over the inset, where matplotlib draws it just under.

Custom Spacing

using MatPlotLibNet;

double[] x = [1, 2, 3, 4];
double[] y = [3, 1, 4, 1];

Plt.Create()
    .WithSubPlotSpacing(s => s with { MarginLeft = 80, HorizontalGap = 20 })
    .AddSubPlot(1, 2, 1, ax => ax.Plot(x, y))
    .AddSubPlot(1, 2, 2, ax => ax.Bar(["A"], [10]))
    .Save("spacing.svg");

Text, legend and image on the figure

A text, a legend and an image can belong to the figure itself instead of a subplot, as matplotlib's figtext, figlegend and figimage do. FigText(text, x, y) places a text at a fraction of the figure's width and height counted from its bottom-left corner, FigLegend() lists the labelled series of every subplot once in a legend of the figure (each subplot's labelled patches after its series), and FigImage(data, xOffset, yOffset) places an image of which each value is one pixel of the figure, offset in pixels from the bottom-left corner:

using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Rendering;
using MatPlotLibNet.Styling;

double[] month = [1, 2, 3, 4, 5, 6];
double[] north = [12, 15, 14, 18, 21, 24];
double[] south = [9, 11, 15, 14, 17, 19];
var strip = new double[10, 140];
for (int row = 0; row < 10; row++)
    for (int column = 0; column < 140; column++)
        strip[row, column] = column;

Plt.Create()
    .WithTitle("Orders by region")
    .WithSize(640, 440)
    .AddSubPlot(1, 1, 1, ax => ax
        .WithLegend(visible: false)
        .Plot(month, north, s => s.Label = "North")
        .Plot(month, south, s => s.Label = "South")
        .SetXLabel("Month"))
    .FigLegend(legend => legend with { Position = LegendPosition.UpperRight })
    .FigText("Source: field survey", 0.01, 0.01)
    .FigText("Preliminary", 0.01, 0.97, text =>
    {
        text.VerticalAlignment = TextVerticalAlignment.Top;
        text.Color = Colors.Red;
    })
    .FigImage(strip, 480, 8)
    .Save("figure_overlays.svg");

The three are drawn through one pass that both the SVG and the raster renderer run, so every output shows them alike, and a figure with no subplot at all still draws them. The images stand under every subplot, then the subplots, then the figure's colour bar, texts and legend. A position outside 0 to 1, or one that is not a number, draws nothing, and an image that reaches past the figure is cut at its edge. The legend of a figure is static, reserves no room and ignores TightLayout, so switch off the legends of the subplots and stand it in a corner the charts leave free; a figure has one legend, and a second FigLegend replaces the first. Every example of the three is on the examples page, with its picture.


Animations (Browser / SignalR)

using MatPlotLibNet;
using MatPlotLibNet.Animation;
using MatPlotLibNet.Interactive;

double[] x = Enumerable.Range(0, 100).Select(i => i * 0.1).ToArray();

var animation = new AnimationBuilder(frameCount: 60, frame =>
    Plt.Create()
        .WithTitle($"t = {frame * 0.1:F1}")
        .Plot(x, x.Select(v => Math.Sin(v + frame * 0.1)).ToArray())
        .Build())
{
    Interval = TimeSpan.FromMilliseconds(50),
    Loop = true
};

var handle = await Plt.Create().Plot(x, x.Select(Math.Sin).ToArray()).Build().ShowAsync();

// A looping animation plays until it is cancelled: stop it after 10 seconds.
// Without Loop, AnimateAsync returns after the last frame.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await handle.AnimateAsync(animation, cts.Token);

GIF Export

Export frames directly to an animated GIF (requires MatPlotLibNet.Skia).

using MatPlotLibNet;
using MatPlotLibNet.Animation;
using MatPlotLibNet.Skia;

double[] x = Enumerable.Range(0, 100).Select(i => i * 0.1).ToArray();

var animation = new AnimationBuilder(60, frame =>
    Plt.Create()
        .WithTitle($"t = {frame * 0.1:F1} s")
        .Plot(x, x.Select(v => Math.Sin(v + frame * 0.2)).ToArray())
        .Build())
{
    Interval = TimeSpan.FromMilliseconds(80),
    Loop = true
};

animation.SaveGif("wave.gif");          // save to file
byte[] gif = animation.ToGif();         // or get bytes (e.g. for Blazor download)

Each frame is drawn at its figure's dpi, so WithDpi(192) on the frames doubles the GIF's width and height. The output is a GIF89a file with a NETSCAPE2.0 loop extension. Colors are quantized to a uniform 252-color palette (a 6×7×6 RGB cube), and each frame's data is LZW-compressed. There is no FFmpeg dependency.


3-D Charts — Camera, Lighting, and Rotation

3-D charts render as SVG using a perspective camera with depth-sorted polygons.

Cross-series depth composition

Every 3-D axes runs one shared depth queue (DepthQueue3D) for all 3-D series on that axes. Each series (Bar3D, PlanarBar3D, Surface, and the others) pushes its drawable primitives into the queue as closures, each with a centroid depth. After all series have rendered, the axes renderer sorts the queue back to front once and invokes the closures in that order. Matplotlib's ax.bar3d sorts each call's faces independently, so there the draw order can visibly break the compositing. Here the insertion order does not affect the output.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] xs = [1, 2, 3, 4, 5];
double[] ysRow0 = [0, 0, 0, 0, 0];
double[] ysRow1 = [1, 1, 1, 1, 1];
double[] ysRow4 = [4, 4, 4, 4, 4];
double[] zs0 = [3, 5, 2, 6, 4];
double[] zs1 = [2, 4, 6, 3, 5];
double[] zs4 = [5, 2, 4, 6, 3];

// The output is the SAME whatever order these calls are made in —
// the shared depth queue handles cross-series sort.
Plt.Create()
    .AddSubPlot(1, 1, 1, ax =>
    {
        ax.PlanarBar3D(xs, ysRow0, zs0, s => s.Color = Colors.Red);     // front
        ax.PlanarBar3D(xs, ysRow1, zs1, s => s.Color = Colors.Green);
        ax.PlanarBar3D(xs, ysRow4, zs4, s => s.Color = Colors.Gold);    // back
    })
    .Save("planar_bars.svg");

Matplotlib face shading

Surfaces and 3-D bars lit with WithLighting(dx, dy, dz, ambient, diffuse) shade each face by k = ambient + diffuse · (dot(n̂, l̂) + 1) / 2, held to [0, 1], and multiply the face colour by k. At the defaults, 0.3 and 0.7, that is matplotlib's _shade_colors formula, k = 0.65 + 0.35 · dot(n̂, l̂), which maps the signed dot product from [−1, 1] to [0.3, 1.0]. This is not Lambertian shading, which clamps the dot product with max(0, dot): a face turned away from the light darkens to ambient times its base brightness instead of going black, and its hue is preserved. A light of your own (ILightSource) shades a 3-D bar as it shades a surface.

SVG vs PNG rendering parity

A 3-D axes rendered to SVG is byte-identical in layout to the same axes rendered to PNG:

  • Text measurement is unified: SvgRenderContext and SkiaRenderContext both delegate to FontMetricsDefault.Current, which delegates in turn to SkiaFontMetrics (bundled DejaVu Sans glyph widths), so there is no font-fallback drift.
  • Glyph rendering in SVG emits text as <path> elements with real glyph outlines from Skia, which matches matplotlib's svg.fonttype='path', so the text renders identically whatever fonts are installed.
  • Figure-level spacing (TightLayout / ConstrainedLayout) runs once via a shared ChartRenderer.PrepareSpacing helper used by both the SVG and PNG transform paths.

Camera

using MatPlotLibNet;

double[] x = Enumerable.Range(0, 25).Select(i => -3 + i * 0.25).ToArray();
double[] y = x;
var z = new double[y.Length, x.Length];
for (int r = 0; r < y.Length; r++)
    for (int c = 0; c < x.Length; c++)
        z[r, c] = Math.Exp(-(x[c] * x[c] + y[r] * y[r]) / 4);

Plt.Create()
    .AddSubPlot(1, 1, 1, ax =>
    {
        ax.Surface(x, y, z);
        ax.WithCamera(elevation: 35, azimuth: -55, distance: 8);
    })
    .Save("surface3d.svg");
  • elevation is the camera tilt in degrees (default 30°).
  • azimuth is the camera rotation in degrees (default −60°).
  • distance is the perspective distance and must be at least 2.0. Omit it for an orthographic projection.

Lighting

// x, y and z as in the camera example above
Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Surface(x, y, z)
        .WithLighting(dx: 1.0, dy: 0.5, dz: -1.0, ambient: 0.4, diffuse: 0.6))   // light direction, then the two terms
    .Save("surface_lit.svg");

A face normal is computed per quad, and the face is shaded with the matplotlib formula above: the shaded color replaces the series fill color on each face. The ambient and diffuse terms are kept with the axes and survive a JSON round trip, but the shading of surfaces and bars reads only the light's direction, so today they do not change the picture.

Interactive SVG Rotation

// x, y and z as in the camera example above
Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax.Surface(x, y, z))
    .With3DRotation()
    .Save("interactive.svg");

With3DRotation() embeds an inline JavaScript block (Svg3DRotationScript). Dragging with the mouse recomputes the elevation and azimuth and re-sorts the polygons by depth. On the keyboard, the arrow keys rotate the view by ±5° and Home resets it to the initial view. There is no framework dependency: the chart works in any browser or HTML email viewer that allows SVG scripts.


Sankey hover emphasis

Sankey with hover emphasis

FigureBuilder.WithSankeyHover() embeds a script that dims every link not reachable upstream or downstream from the hovered node. This matches ECharts' focus: adjacency. The emphasis is keyboard-accessible through tabindex="0" and focus/blur events.

Every Sankey node rect carries a data-sankey-node-id attribute, and every link path carries data-sankey-link-source and data-sankey-link-target. The script uses those attributes to run a breadth-first search (BFS) in both directions from the hovered node and dims everything outside the reachable set.

using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Models.Series;

SankeyNode[] nodes = [new("Coal"), new("Gas"), new("Power"), new("Homes"), new("Industry")];
SankeyLink[] links =
[
    new(SourceIndex: 0, TargetIndex: 2, Value: 40),
    new(SourceIndex: 1, TargetIndex: 2, Value: 30),
    new(SourceIndex: 2, TargetIndex: 3, Value: 45),
    new(SourceIndex: 2, TargetIndex: 4, Value: 25),
];

Plt.Create()
    .WithSankeyHover()                           // embeds the script
    .AddSubPlot(1, 1, 1, ax => ax
        .HideAllAxes()                           // optional: a Sankey draws no axes of its own
        .Sankey(nodes, links, s =>
        {
            s.Iterations = 20;                   // minimise link crossings
            s.LinkColorMode = SankeyLinkColorMode.Gradient;
        }))
    .Save("sankey.svg");

Real-Time Charts

ASP.NET Core + SignalR

Server:

using MatPlotLibNet;
using MatPlotLibNet.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMatPlotLibNetSignalR();

var app = builder.Build();
app.MapChartHub();                               // the hub the clients below connect to: /charts-hub

// Push update anywhere in your app
var publisher = app.Services.GetRequiredService<IChartPublisher>();
var figure = Plt.Create().Plot([1.0, 2.0, 3.0], [21.5, 22.1, 21.8]).Build();
await publisher.PublishSvgAsync("sensor-1", figure);

app.Run();

Blazor client:

<MplLiveChart ChartId="sensor-1" HubUrl="/charts-hub" />

React client:

<MplLiveChart chartId="sensor-1" hubUrl="/charts-hub" />

Interactive Popup (no server)

using MatPlotLibNet;
using MatPlotLibNet.Interactive;

double[] x = [1, 2, 3, 4, 5];
double[] y = [2, 4, 3, 5, 4];

var handle = await Plt.Create().Plot(x, y).Build().ShowAsync();

// Recalculate and push update
handle.Figure.Title = "Recalculated";
await handle.UpdateAsync();

Clone this wiki locally