Skip to content

Advanced

xkqg edited this page Apr 17, 2026 · 10 revisions

Advanced


Date Axes

Date axis with auto ticks

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

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()
    .Plot(dates, values)
    .WithXLabel("Date")
    .Save("dates.svg");   // ticks: "Jan 2025", "Feb 2025", …

Sub-day data:

DateTime[] hours = Enumerable.Range(0, 48)
    .Select(i => DateTime.Today.AddHours(i))
    .ToArray();

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

AutoDateLocator selects tick intervals across: Years → Months → Weeks → Days → Hours → Minutes → Seconds. Manual override:

ax.SetXTickLocator(new AutoDateLocator())
  .SetXTickFormatter(new AutoDateFormatter());

Math Text Labels

Math text labels with Greek letters and super/subscript

Use mini-LaTeX syntax in any title, axis label, annotation, or legend entry. Wrap math in $...$.

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) use plain-text Unicode substitution as fallback.

MathText features: fractions (\frac), square roots (\sqrt), accents (\hat, \bar, \tilde, \dot, \vec), font variants (\mathrm, \mathbf, \mathit, \mathcal, \mathbb), \text{}, spacing (\,, \:, \;, \quad, \qquad), scaling delimiters (\left/\right), and 96 symbol mappings (Greek + math operators + arrows + set/logic + blackboard bold) including blackboard bold (ℝ, ℂ, ℤ, ℕ, ℚ), arrows (→, ⇒, ←, ↔), and set/logic operators (∀, ∃, ∈, ∉, ∪, ∩).

MathText — fractions, sqrt, accents, font variants


Constrained Layout

Automatically compute margins from actual text extents instead of hardcoded defaults.

// 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");

The engine measures Y-tick label widths, axis label sizes, and subplot title heights, then computes exact SubPlotSpacing margins clamped to sensible ranges. When neither flag is set, fixed defaults (MarginLeft=60, MarginBottom=50, …) are used.

Outside legends

Outside legend — right margin reserved

Four new LegendPosition values place the legend box outside the plot area: OutsideRight, OutsideLeft, OutsideTop, OutsideBottom. The constrained-layout engine measures the legend box via the new shared LegendMeasurer (the same formulas the renderer uses for draw-time positioning) and widens the corresponding figure margin by the full box width + 16 px gap. The hard [10, 140] right-margin clamp is dynamic: it raises to at least legendBoxWidth + 40 for any OutsideRight legend so a 200 px legend never gets clipped by the default ceiling.

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() / ConstrainedLayout() the legend would clip at the figure edge — the reservation pass only runs when one of those flags is set.

Label collision avoidance

Dense pies, sunbursts, Sankeys, and bar charts used to stack labels on top of each other for overlapping anchors. v1.1.4 introduces the LabelLayoutEngine — an iterative pair-wise repulsion solver that:

  • Computes the bounding rectangle of each label via ChartServices.FontMetrics (same provider the renderer uses, so layout and draw agree byte-identical).
  • For each overlapping pair, applies a minimum-translation vector (MTV) shift along the smaller-overlap axis, weighted by per-label priority.
  • Clamps every label rect back inside the plot bounds on each pass.
  • Terminates at convergence or 20 iterations (whichever comes first).
  • When a label moves more than leaderThreshold pixels from its original anchor, records a leader-line anchor so the caller can draw a connector back to the semantic position — CalloutBoxRenderer.DrawLeaderLine(ctx, anchor, finalPoint, color).

The engine is used automatically by PieSeriesRenderer, SunburstSeriesRenderer, SankeySeriesRenderer, and BarSeriesRenderer. TreemapSeriesRenderer uses a per-cell measured-fit check instead (each label is constrained to its own rect, so there's no cross-cell collision to resolve).

You can use the engine directly for custom label layouts:

var candidates = myAnchors.Select(a =>
    new LabelCandidate(a.Point, a.Text, font)).ToList();
var placements = LabelLayoutEngine.Place(
    candidates,
    plotBounds,
    ChartServices.FontMetrics,
    leaderThreshold: 6.0);
foreach (var p in placements)
{
    if (p.LeaderLineStart is { } anchor)
        CalloutBoxRenderer.DrawLeaderLine(ctx, anchor, p.FinalPoint, Colors.Black);
    ctx.DrawText(p.Text, p.FinalPoint, p.Font, p.Alignment);
}

Layouts

GridSpec layout

GridSpec — Unequal Subplot Sizes

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(GridPosition.Span(1, 2, 0, 2), ax => ax.Bar(cats, vals))
    .Save("gridspec.svg");

Shared Axes

.AddSubPlot(2, 1, 1, ax => ax.ShareX("group1").Plot(x, y1))
.AddSubPlot(2, 1, 2, ax => ax.ShareX("group1").Plot(x, y2))

Inset Axes

ax.Plot(x, y)
  .AddInset(0.6, 0.6, 0.35, 0.35, inset => inset
      .Plot(xZoom, yZoom).WithTitle("Detail"));

Custom Spacing

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");

Animations (Browser / SignalR)

using MatPlotLibNet.Animation;

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, y).Build().ShowAsync();
await handle.AnimateAsync(animation);

// Stop after 10 seconds
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.Animation;
using MatPlotLibNet.Skia;

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)

The encoder writes GIF89a with a NETSCAPE2.0 loop extension, 252-color uniform quantization (6×7×6 RGB cube), and LZW-compressed per-frame image data. No FFmpeg dependency.


3-D Charts — Camera, Lighting, and Rotation

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

Cross-series depth composition

Every 3-D axes runs a single shared depth queue (DepthQueue3D) across all 3-D series on that axes. Each series — Bar3D, PlanarBar3D, Surface3D, and the others — pushes its drawable primitives into the queue as closures with a centroid depth. After all series have rendered, the axes renderer sorts the queue back-to-front in one pass and invokes the closures in depth order.

Why this matters: matplotlib's ax.bar3d(...) has a well-known limitation — adding rows in the wrong order (front-to-back instead of back-to-front) visibly breaks compositing because each call sorts only its own faces and draws immediately. MatPlotLibNet lifts that restriction: you can call ax.Bar3D(...) or ax.PlanarBar3D(...) any number of times in any order and the shared depth queue interleaves the faces from every series correctly before painting. Insertion order doesn't affect the output.

// These two samples produce the SAME output regardless of loop order —
// 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

3-D series with WithLighting(...) use matplotlib's exact _shade_colors formula: k = 0.65 + 0.35 · dot(n̂, l̂), mapping the raw signed dot product from [−1, 1] to [0.3, 1.0]. No Lambertian max(0, dot) clamp — back-facing faces darken to 0.3× base brightness instead of going to zero, hue is preserved, and the full shade range matches matplotlib's art3d._shade_colors output pixel-for-pixel.

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: both SvgRenderContext and SkiaRenderContext delegate to ChartServices.FontMetricsSkiaFontMetrics (bundled DejaVu Sans glyph widths). No more font-fallback drift.
  • Glyph rendering in SVG: text elements are emitted as <path> with real glyph outlines from Skia (matches matplotlib's default svg.fonttype='path' behaviour). The SVG renders identically in any viewer regardless of installed fonts.
  • Figure-level spacing (TightLayout / ConstrainedLayout) runs once via a shared ChartRenderer.PrepareSpacing helper called by both the SVG and PNG transform paths. No state mutation on the figure.

Camera

Plt.Create()
    .AddSubPlot(1, 1, 1, ax =>
    {
        ax.Surface3D(x, y, z);
        ax.WithCamera(elevation: 35, azimuth: -55, distance: 8);
    })
    .Save("surface3d.svg");
  • elevation — camera tilt in degrees (default 30°)
  • azimuth — camera rotation in degrees (default −60°)
  • distance — perspective distance ≥ 2.0 (omit for orthographic)

Lighting

ax.WithLighting(ambient: 0.4, diffuse: 0.6, dirX: 1.0, dirY: 0.5, dirZ: -1.0);

Uses Lambertian (ambient + diffuse) shading. Face normals are computed per-quad; shaded color replaces the series fill color per face.

Interactive SVG Rotation

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax.Surface3D(x, y, z))
    .With3DRotation()
    .Save("interactive.svg");

Embeds a small inline JS block (Svg3DRotationScript). Mouse drag re-computes elevation + azimuth and re-sorts polygons by depth. Keyboard: arrow keys (±5°), Home resets to initial view. No framework dependency — works in any browser or HTML email viewer that allows SVG scripts.


Sankey hover emphasis

Sankey with hover emphasis

FigureBuilder.WithSankeyHover() embeds an interactive script that dims every link not reachable upstream or downstream from the hovered node. Matches ECharts' focus: adjacency behaviour; keyboard-accessible via tabindex="0" + focus/blur events.

Every Sankey node rect carries data-sankey-node-id, and every link path carries data-sankey-link-source / data-sankey-link-target — the script walks those attributes to do a BFS in both directions from the hovered node and applies a visual opacity reduction to every element NOT in the reachable set.

Plt.Create()
    .WithSankeyHover()                           // embeds the script
    .AddSubPlot(1, 1, 1, ax => ax
        .HideAllAxes()                           // bare canvas — no cartesian axes
        .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:

builder.Services.AddMatPlotLibNetSignalR();

// Push update anywhere in your app
await publisher.PublishSvgAsync("sensor-1", figure);

Blazor client:

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

React client:

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

Interactive Popup (no server)

using MatPlotLibNet.Interactive;

var handle = await Plt.Create().Plot(x, y).Build().ShowAsync();
// Recalculate and push update
await handle.UpdateAsync();

Clone this wiki locally