Skip to content

Styling

H.P. Gansevoort edited this page Oct 22, 2026 · 18 revisions

Styling


Themes

MatPlotLibNet ships with built-in themes that mirror matplotlib style sheets.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

Plt.Create().WithTheme(Theme.Default).Plot(x, y).Save("default.svg");
Plt.Create().WithTheme(Theme.Dark).Plot(x, y).Save("dark.svg");
Plt.Create().WithTheme(Theme.Solarize).Plot(x, y).Save("solarized.svg");

Theme.ColorBlindSafe

This theme uses the Okabe-Ito 8-color palette, which is colorblind-safe for deuteranopia, protanopia, and tritanopia. The background is white and the text is black.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [1, 2, 3, 4, 5];
double[] y = [12, 15, 14, 18, 21];

Plt.Create()
    .WithTheme(Theme.ColorBlindSafe)
    .Plot(x, y, s => s.Label = "Revenue")
    .Save("accessible.svg");

The same colors are also available as a colormap ("okabe_ito" / "okabe_ito_r"):

using MatPlotLibNet.Styling.ColorMaps;

var map = ColorMaps.Get("okabe_ito");

Theme.MatplotlibClassic and Theme.MatplotlibV2

These two themes reproduce the matplotlib look in pure .NET; no Python runtime is required. Theme.MatplotlibClassic mimics matplotlib's pre-2.0 look (white background, bgrcmyk 7-color cycle, DejaVu Sans 12pt). Theme.MatplotlibV2 mimics the modern default (white background, soft-black #262626 text, tab10 10-color cycle, DejaVu Sans 10pt). Both ship with the grid hidden by default, matching matplotlib's plt.grid(False).

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

Plt.Create()
    .WithTheme(Theme.MatplotlibV2)
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y, s => s.Label = "Series 1")
        .Plot(x, y2, s => s.Label = "Series 2")
        .WithLegend())
    .Save("matplotlib_v2.svg");

See MatplotlibThemes for the full color cycles, comparison table, and implementation notes.

Theme.HighContrast

This theme targets WCAG AAA: white and black give a 21:1 contrast ratio. It uses a bold 13pt font, a thick 1.5px dark grid, and an 8-color high-chroma cycle.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

Plt.Create()
    .WithTheme(Theme.HighContrast)
    .Plot(x, y)
    .Save("high-contrast.svg");

Custom Theme

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

var theme = Theme.CreateFrom(Theme.Dark)
    .WithBackground(Color.FromHex("#1a1a2e"))
    .WithFont(f => f with { Family = "Consolas", Size = 14 })
    .Build();

Plt.Create().WithTheme(theme).Plot(x, y).Save("custom.svg");

A figure's theme travels with it: ToJson writes it as data, property by property, and FromJson, the MCP server and every transport read it back, so a theme made with ThemeBuilder comes back whole too. A figure on the default theme writes no theme at all.


Operator themes and the alarm palette

Four themes have backgrounds designed for a wall display rather than a report: Theme.OpsNight, OpsPanel, OpsWarm, OpsContrast.

Every theme also defines its own alarm colours, so a state mark never takes a colour from the series cycle:

using MatPlotLibNet.Styling;

var theme = Theme.OpsNight;
Color resting  = theme.Alarm.Resting;    // nothing wrong — the neutral shade
Color warning  = theme.Alarm.Warning;    // out of band
Color critical = theme.Alarm.Critical;   // acted on now
Color unknown  = theme.Alarm.Unknown;    // no information (a hatch, never a colour)

var custom = Theme.CreateFrom(Theme.Dark)
    .WithAlarmPalette(new AlarmPalette(Colors.Gray, Colors.Orange, Colors.Crimson, Colors.DarkGray))
    .Build();

In these themes, colour is reserved for states that need attention. See Chart Types#control-room for the tiles that use the alarm palette.


PropCycler

PropCycler — four series cycling color and line style

A PropCycler cycles Color, LineStyle, MarkerStyle, and LineWidth at the same time across series. Each property cycles at its own length, and the series index wraps around at the least common multiple (LCM) of those lengths. When PropCycler is null, the theme's CycleColors[] is used instead.

All four are drawn. A series that leaves its line style, width or marker at what a new series holds (solid, 1.5 points, no marker; a scatter holds a circle) takes the cycler's entry for its place; a series that sets another value keeps its own, as a series with a colour of its own does. A value equal to the default cannot be told from one left alone, so under a cycler of dashes a series cannot be forced back to solid by setting solid. The entries reach lines, steps, scatters, the outline of an area, error-bar widths, density curves, ECDFs, regression, signal, polar and 3-D lines, both secondary axes and every legend swatch. A cycler that lists only colours changes nothing else, and no built-in theme has a cycler.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [0, 1, 2, 3, 4];
double[][] y =
[
    [1, 2, 3, 4, 5],
    [2, 3, 4, 5, 6],
    [3, 4, 5, 6, 7],
    [4, 5, 6, 7, 8],
];

var cycler = new PropCyclerBuilder()
    .WithColors(Colors.Blue, Colors.Orange, Colors.Green, Colors.Red)
    .WithLineStyles(LineStyle.Solid, LineStyle.Dashed)
    .WithMarkerStyles(MarkerStyle.Circle, MarkerStyle.Square)
    .WithLineWidths(2.0, 1.5)
    .Build();

// A cycler travels in a theme: embed it once and reuse the theme across figures
var theme = Theme.CreateFrom(Theme.Dark)
    .WithPropCycler(cycler)
    .Build();

Plt.Create()
    .WithTheme(theme)
    .AddSubPlot(1, 1, 1, ax =>
    {
        for (int i = 0; i < 4; i++)
            ax.Plot(x, y[i], s => s.Label = $"Series {i + 1}");
        ax.WithLegend();
    })
    .Save("cycler.svg");

Color Maps

MatPlotLibNet includes 74 base maps. Each one is registered automatically with a reversed _r variant, which makes 148 colormaps in six families:

Colormap comparison

Family Examples
Sequential viridis, plasma, inferno, magma, cividis, blues, greens, gray, hot, spring, summer, autumn, winter, cool, afmhot
Diverging rdbu, rdylgn, bwr, seismic, coolwarm, prgn, rdgy
Qualitative tab10, tab20, set1, set2, set3, paired, dark2, accent
Cyclic hsv, twilight, twilight_shifted
Accessible okabe_ito, petroff6, petroff8, petroff10
Misc rainbow, ocean, terrain, cmrmap, jet, turbo

The accessible family is for a chart that has to be read by everyone. okabe_ito is the eight-colour palette from Okabe and Ito; the three Petroff sequences come from Accessible Color Sequences for Data Visualization and pick colours so that every pair inside a sequence stays apart under the common forms of colour blindness, and in grayscale as well. Pick the one whose number matches how many things you are drawing.

using MatPlotLibNet;
using MatPlotLibNet.Styling;
using MatPlotLibNet.Styling.ColorMaps;

double[,] data = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };

// Apply to a heatmap
Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Heatmap(data)
        .WithColorMap("plasma")
        .WithColorBar(cb => cb with { Label = "Intensity", Extend = ColorBarExtend.Both }))
    .Save("heatmap.svg");

// Registry lookup — case-insensitive, append _r for reversed variant
var map      = ColorMapRegistry.Get("rdylgn");
var reversed = ColorMapRegistry.Get("viridis_r");

// Custom gradient with positioned stops
var custom = LinearColorMap.FromPositions("my_map",
[
    new ColorStop(0.0, Color.FromHex("#000080")),
    new ColorStop(0.3, Color.FromHex("#FFFFFF")),
    new ColorStop(1.0, Color.FromHex("#800000")),
]);

// Discrete categorical colormap (no interpolation)
var discrete = new ListedColorMap("my_cat", [Colors.Blue, Colors.Red, Colors.Green]);

A map that ships goes into JSON by its name. A map built in code, like the two above, goes in as what it is made of: its colours, their positions, and its under, over and bad colours, so it comes back from FromJson, the MCP server and every transport as itself. A map of a type you write yourself is code; it travels only by a name registered with ColorMapRegistry.Register, which the reader has to register too.

Colormappable Series

These are all the series that support IColorMap (via IColormappable): HeatmapSeries, ImageSeries, Histogram2DSeries, ContourSeries, ContourfSeries, SurfaceSeries, ScatterSeries, PcolormeshSeries, HexbinSeries, SpectrogramSeries, TripcolorSeries, TricontourSeries, HierarchicalSeries.

ScatterSeries supports per-point colormap coloring via the C scalar array:

using MatPlotLibNet;
using MatPlotLibNet.Styling.ColorMaps;

double[] x = [1, 2, 3, 4, 5];
double[] y = [2, 4, 3, 5, 4];
double[] values = [5, 30, 55, 80, 100];

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax.Scatter(x, y, s =>
    {
        s.C        = values;          // scalar per point
        s.ColorMap = ColorMaps.Viridis;
        s.VMin     = 0;
        s.VMax     = 100;
        s.Normalizer = new PowerNormNormalizer(gamma: 0.5);
    }))
    .Save("scatter_colormap.svg");

Normalizers

using MatPlotLibNet.Styling.ColorMaps;

INormalizer[] normalizers =
[
    LinearNormalizer.Instance,                         // (v - min) / (max - min) — default
    new LogNormalizer(),                               // log scale mapping
    new TwoSlopeNormalizer(center: 0.0),               // diverging, center at value
    new BoundaryNormalizer([0.0, 10.0, 50.0, 100.0]),  // discrete boundaries
    new SymLogNormalizer(linthresh: 1.0),              // symmetric log — linear near zero, log beyond
    new PowerNormNormalizer(gamma: 0.5),               // power-law: gamma < 1 expands low values
    new CenteredNormNormalizer(vcenter: 0),            // maps chosen center to 0.5
    NoNormNormalizer.Instance,                         // pass-through — data already in [0, 1]
];

Extreme Values (Under / Over / Bad)

using MatPlotLibNet;
using MatPlotLibNet.Styling;
using MatPlotLibNet.Styling.ColorMaps;

double[,] data = { { -5, 2, 3 }, { 4, double.NaN, 6 }, { 7, 8, 15 } };
Color[] stops = [Color.FromHex("#000080"), Color.FromHex("#FFFFFF"), Color.FromHex("#800000")];

var cm = new LinearColorMap("custom", stops)
{
    UnderColor = Color.FromHex("#222222"),  // values below VMin
    OverColor  = Color.FromHex("#FFFFFF"),  // values above VMax
    BadColor   = Color.FromHex("#FF00FF"),  // NaN / masked values
};

// ColorBar extension slots show under/over colors
Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Heatmap(data)
        .WithColorMap(cm)
        .WithColorBar(cb => cb with { Extend = ColorBarExtend.Both }))
    .Save("extremes.svg");

The missing cell (NaN) is drawn in the bad colour, as in matplotlib; a map without a BadColor leaves it transparent.


Axis Formatting

Log Scale

using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Rendering.TickFormatters;

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

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .SetYScale(AxisScale.Log)
        .SetYTickFormatter(new LogTickFormatter())
        .Plot(x, y))
    .Save("logscale.svg");

A log axis has no place for 0, so a mark that stands on 0 (a bar, a histogram bin, a filled area, a stacked area, a box or a violin) runs from the bottom edge of the plot, as matplotlib's log scale clips it (nonpositive='clip'), and the axis starts a margin below the lowest value such marks reach, so the shortest one shows. A point at 0 or below is left out, and a line breaks there.

Date Axis (manual override)

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

DateTime[] dates = [.. Enumerable.Range(0, 30).Select(day => DateTime.Today.AddDays(day))];
double[] y = [.. Enumerable.Range(0, 30).Select(day => (double)(day % 7))];

var locator = new AutoDateLocator();

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(dates, y)
        .SetXTickLocator(locator)
        .SetXTickFormatter(new AutoDateFormatter(locator)))
    .Save("date_axis.svg");

See Advanced#date-axes for the automatic date axis workflow.


Spines

using MatPlotLibNet;
using MatPlotLibNet.Models;

double[] x = [-2, -1, 0, 1, 2];
double[] y = [-4, -1, 0, 1, 4];

Plt.Create()
    .AddSubPlot(1, 2, 1, ax => ax
        .Plot(x, y)
        // Hide top and right (publication style)
        .HideTopSpine().HideRightSpine())
    .AddSubPlot(1, 2, 2, ax => ax
        .Plot(x, y)
        // Move x-axis to y = 0
        .WithSpines(s => s with
        {
            Bottom = s.Bottom with { Position = SpinePosition.Data, PositionValue = 0 }
        }))
    .Save("spines.svg");

Grid Lines

Grid lines are on by default with all built-in themes, matching matplotlib's default: solid #B0B0B0 lines at width 0.8.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

Plt.Create()
    // Default — major grid on both axes
    .AddSubPlot(2, 3, 1, ax => ax.Plot(x, y))

    // Customize color and style
    .AddSubPlot(2, 3, 2, ax => ax.Plot(x, y).WithGrid(g => g with
    {
        Color     = Color.FromHex("#444444"),
        LineStyle = LineStyle.Dashed,
        LineWidth = 0.5
    }))

    // Only X-axis grid lines
    .AddSubPlot(2, 3, 3, ax => ax.Plot(x, y).WithGrid(g => g with { Axis = GridAxis.X }))

    // Add minor grid lines
    .AddSubPlot(2, 3, 4, ax => ax.Plot(x, y).WithGrid(g => g with { Which = GridWhich.Both }))

    // Turn off entirely
    .AddSubPlot(2, 3, 5, ax => ax.Plot(x, y).ShowGrid(false))
    .Save("grids.svg");

GridWhich controls tick level: Major (default), Minor, Both. GridAxis controls which axis: X, Y, Both (default).


Chrome Configuration

TextStyle

TextStyle is a nullable partial font override. Apply it to a theme Font via ApplyTo(Font):

using MatPlotLibNet.Styling;

var style = new TextStyle { FontSize = 18, FontWeight = FontWeight.Bold, Color = Colors.Navy };
var font = style.ApplyTo(Theme.Default.DefaultFont);

Title and Axis Label Styling

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [1, 2, 3, 4];
double[] y = [12, 15, 14, 18];

var figure = Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y)
        .WithTitle("Revenue 2026", ts => ts with { FontSize = 22, Color = Css4Colors.DarkBlue })
        .SetXLabel("Quarter", ts => ts with { FontSize = 14 })
        .SetYLabel("USD (M)", ts => ts with { FontSize = 14, FontSlant = FontSlant.Italic }))
    .Build();

// Title alignment, set on the built axes
figure.SubPlots[0].TitleLoc = TitleLocation.Left;   // Left / Center (default) / Right
figure.Save("titles.svg");

Legend Configuration

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

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

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y, s => s.Label = "North")
        .Plot(x, y2, s => s.Label = "South")
        .WithLegend(l => l with
        {
            NCols       = 2,
            Title       = "Series",
            FontSize    = 11,
            FrameOn     = true,
            FrameAlpha  = 0.9,
            FancyBox    = true,
            Shadow      = true,
            EdgeColor   = Colors.Gray,
            MarkerScale = 1.2,
            Position    = LegendPosition.LowerRight
        }))
    .Save("legend.svg");

Tick Customization

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

var figure = Plt.Create().Plot(x, y).Build();
var ax = figure.SubPlots[0];

ax.XAxis.MajorTicks = ax.XAxis.MajorTicks with
{
    Direction  = TickDirection.In,   // In / Out (default) / InOut
    Length     = 8,
    Width      = 1.2,
    Color      = Colors.DarkGray,
    LabelSize  = 11,
    LabelColor = Css4Colors.DimGray,
    Pad        = 5
};

figure.Save("ticks.svg");

A right-hand or top axis takes the same settings through figure.SubPlots[0].SecondaryYAxis!.MajorTicks and SecondaryXAxis!.MajorTicks; the examples page shows one with its ticks turned inward.

Spine Styling

using MatPlotLibNet;
using MatPlotLibNet.Styling;

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

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y)
        .WithSpines(s => s with
        {
            Top   = s.Top   with { Visible = false },
            Right = s.Right with { Visible = false },
            Left  = s.Left  with { Color = Colors.Navy, LineStyle = LineStyle.Solid },
            Bottom = s.Bottom with { LineWidth = 1.5 }
        }))
    .Save("spine_styling.svg");

Axis Colour

SetYAxisColor and SetXAxisColor colour an axis' spine, tick marks, tick labels and title in one call, so an axis reads as belonging to the line drawn in that colour. They exist on the axes builder (left and bottom axis) and on WithSecondaryYAxis / WithSecondaryXAxis (right and top axis); everything else on the axis is kept, and the colour survives ToJson / FromJson. Keep an axis title that names the series and its unit, so the colour is never the only thing that tells a reader which axis a line belongs to.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] hour = [0, 6, 12, 18, 24];
double[] temperature = [12, 11, 21, 17, 13];
double[] humidity = [88, 90, 52, 63, 81];

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(hour, temperature, s => s.Color = Colors.Crimson)
        .SetYLabel("Temperature (°C)")
        .SetYAxisColor(Colors.Crimson)
        .WithSecondaryYAxis(right => right
            .Plot(hour, humidity, s => s.Color = Colors.SteelBlue)
            .SetYLabel("Relative humidity (%)")
            .SetYAxisColor(Colors.SteelBlue)))
    .Save("axis_color.svg");

The result, with both lines named at their ends, is pictured on the examples page.

ColorBar Orientation and Shrink

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[,] data = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Heatmap(data)
        .WithColorBar(cb => cb with
        {
            Orientation = ColorBarOrientation.Horizontal,
            Shrink      = 0.8,
            DrawEdges   = true,
            Label       = "Intensity"
        }))
    .Save("colorbar_horizontal.svg");

Hatch Patterns

Fill a bar, area, or pie slice with a hatch pattern instead of (or on top of) a solid fill.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

string[] categories = ["A", "B", "C", "D"];
double[] values = [4, 7, 3, 5];
double[] x = [0, 1, 2, 3, 4];
double[] y1 = [1, 3, 2, 4, 3];
double[] y2 = [0, 1, 0.5, 1.5, 1];

Plt.Create()
    // Bar series with cross hatch
    .AddSubPlot(1, 3, 1, ax => ax.Bar(categories, values, s =>
    {
        s.Color      = Colors.SteelBlue;
        s.Hatch      = HatchPattern.Cross;
        s.HatchColor = Colors.White;
    }))

    // Area series with forward-diagonal lines
    .AddSubPlot(1, 3, 2, ax => ax.FillBetween(x, y1, y2, s =>
    {
        s.Hatch      = HatchPattern.ForwardDiagonal;
        s.HatchColor = Colors.DarkGray;
    }))

    // Pie with per-slice hatches
    .AddSubPlot(1, 3, 3, ax => ax.Pie(values, categories, s =>
    {
        s.Hatches = [HatchPattern.Horizontal, HatchPattern.Vertical,
                     HatchPattern.DiagonalCross, HatchPattern.Dots];
    }))
    .Save("hatches.svg");

Available patterns: None, ForwardDiagonal (/), BackDiagonal (\), Horizontal (-), Vertical (|), Cross (+), DiagonalCross (x), Dots (.), Stars (*).

Series with hatch support: BarSeries, HistogramSeries, AreaSeries, StackedAreaSeries, PieSeries, ContourfSeries.


StackedBaseline

Control how the zero baseline is positioned in a stacked area chart.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [0, 1, 2, 3, 4];
double[][] ySets =
[
    [1, 2, 3, 2, 1],
    [2, 1, 2, 3, 2],
    [1, 3, 1, 2, 3],
];

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax.StackPlot(x, ySets, s =>
    {
        s.Baseline = StackedBaseline.Wiggle;   // matplotlib's baseline='wiggle'
    }))
    .Save("streamgraph.svg");
Value Description
Zero (default) Stack grows upward from y=0
Symmetric Stack centered at y=0 (ThemeRiver)
Wiggle matplotlib's wiggle: the middles of the layers stay as near to zero as the stack allows, the lower layers weighing more
WeightedWiggle matplotlib's weighted_wiggle, the streamgraph layout: also follows each layer's slope, weighted by its size

Both wiggles are matplotlib's own formulas. A missing value (NaN) leaves a gap at its own point only. StreamGraph(x, layers, baseline) on the axes builder draws each layer as an area of its own, with its own legend entry; a StackPlot layer's name does not reach the legend.


AreaSeries Enhancements

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [0, 1, 2, 3, 4];
double[] y1 = [1, 3, 2, 4, 3];
double[] y2 = [0, 1, 0.5, 1.5, 1];

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax.FillBetween(x, y1, y2, s =>
    {
        s.EdgeColor = Css4Colors.DarkBlue;      // separate stroke for boundary lines
        s.StepMode  = DrawStyle.StepsPre;       // step interpolation (Pre/Mid/Post)
    }))
    .Save("area_steps.svg");

StepMode uses the same DrawStyle enum as LineSeries: Default (smooth), StepsPre, StepsMid, StepsPost.


SurfaceSeries Enhancements

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [.. Enumerable.Range(0, 40).Select(i => i * 0.25)];
double[] y = [.. Enumerable.Range(0, 40).Select(i => i * 0.25)];
var z = new double[y.Length, x.Length];
for (int row = 0; row < y.Length; row++)
    for (int col = 0; col < x.Length; col++)
        z[row, col] = Math.Sin(x[col]) * Math.Cos(y[row]);

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax.Surface(x, y, z, s =>
    {
        s.EdgeColor = Colors.Black.WithAlpha(40);  // wireframe stroke (null = default)
        s.RowStride = 2;   // render every 2nd row — faster for large grids
        s.ColStride = 2;   // render every 2nd column
    }))
    .Save("surface.svg");

Figure-Level ColorBar

Render a single colorbar shared across all subplots, positioned outside the plot area.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[,] dataA = { { 1, 2, 3 }, { 4, 5, 6 } };
double[,] dataB = { { 6, 5, 4 }, { 3, 2, 1 } };

Plt.Create()
    .WithColorBar(cb => cb with
    {
        Label       = "Signal Intensity",
        Orientation = ColorBarOrientation.Vertical,   // or Horizontal
        Shrink      = 0.9,
        Padding     = 10
    })
    .AddSubPlot(1, 2, 1, ax => ax.Heatmap(dataA))
    .AddSubPlot(1, 2, 2, ax => ax.Heatmap(dataB))
    .Save("shared_colorbar.svg");

The colorbar picks up its colormap and range from the first IColorBarDataProvider series found across all subplots.


TwinY — Secondary X-Axis

Add a second X-axis at the top of a subplot, independent of the primary X-axis.

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

double[] x = [1, 2, 3, 4];
double[] y = [10, 20, 15, 25];
double[] x2 = [100, 200, 300, 400];
double[] celsius = [-10, 5, 20, 35, 50];
double[] power = [1.8, 1.4, 1.0, 1.3, 1.9];

// On the Axes model
var figure = new Figure();
var axes = figure.AddSubPlot();
axes.Plot(x, y);
axes.TwinY();
var secondary = axes.PlotXSecondary(x2, y);
secondary.Color = Colors.Red;
secondary.Label = "Secondary";
figure.Save("twin_y.svg");

// Or via AxesBuilder
Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y)
        .WithSecondaryXAxis(b =>
        {
            b.SetXLabel("Temperature (°C)");
            b.SetXLim(-10, 50);
            b.PlotXSecondary(celsius, power, s => s.Color = Css4Colors.OrangeRed);
        }))
    .Save("secondary_x.svg");

Top-edge ticks and label are rendered automatically. The secondary X range is computed from XSecondarySeries data.


SaveOptions

Save takes a SaveOptions record, and each format uses what it can:

Setting What it does
Dpi sizes a PNG: a figure is laid out in pixels at 96 dpi, and the PNG is drawn with dpi / 96 pixels for each, so 500 × 350 at 150 dpi is 781 × 547. Left at 96 it leaves the figure's own WithDpi in force; any other value overrides it for that save. An SVG, a PDF page and JSON have no pixels and ignore it.
PrettifySvg writes the SVG indented, one element to a line
SvgDecimalPrecision rounds positions and sizes in the SVG to 0 to 15 decimals, half away from zero; stroke widths, opacities and text keep their digits
Title, Author the title and creator in the SVG's metadata, the PDF's document information and the PNG's text chunks

A GIF follows the dpi of each frame's figure. A format that cannot use a setting ignores it, so the same options can be handed to any save. WithDpi on the figure builder sets the resolution once for every PNG of the figure.

using MatPlotLibNet;

double[] x = [1, 2, 3, 4];
double[] y = [12, 15, 14, 18];

var fig = Plt.Create().Plot(x, y).Build();

fig.Save("output.svg", new SaveOptions
{
    PrettifySvg         = true,
    SvgDecimalPrecision = 1,
    Title               = "Annual Revenue",
    Author              = "Finance Team"
});

fig.Save("output.png", new SaveOptions { Dpi = 150, Title = "Annual Revenue" });   // needs MatPlotLibNet.Skia

Annotations

Reference Line Labels

Labels on AxHLine / AxVLine are now rendered automatically. A line beyond the data widens the automatic range, as matplotlib's axhline does, so it is drawn inside the plot; beyond a limit you set (or a zoom), neither the line nor its label is drawn.

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

double[] x = [0, 10, 20, 30, 40, 50];
double[] y = [20, 60, 110, 150, 90, 70];

var figure = new Figure();
var ax = figure.AddSubPlot();
ax.Plot(x, y);

var line = ax.AxHLine(50.0);
line.Label = "Mean";
line.Color = Colors.Red;
// Horizontal: label appears right-aligned at the right edge, above the line.
// Vertical:   label appears near the top of the line.

Connection Styles

Control how the arrow connects the annotation text to its target.

var ann = ax.Annotate("Peak", 42.0, 150.0);
ann.ArrowTargetX = 45.0;
ann.ArrowTargetY = 90.0;
ann.ConnectionStyle = ConnectionStyle.Arc3;   // or Straight / Angle / Angle3
ann.ConnectionRad   = 0.4;                    // curvature for Arc3/Angle3
ann.ArrowStyle      = ArrowStyle.FancyArrow;
ann.ArrowHeadSize   = 10;
ConnectionStyle Shape
Straight Direct line (default)
Arc3 Bezier arc, offset by ConnectionRad × distance
Angle Horizontal then vertical (elbow)
Angle3 Smoothed elbow with bezier rounding

Arrow Head Styles

ann.ArrowStyle = ArrowStyle.Wedge;      // wider filled arrowhead
ann.ArrowStyle = ArrowStyle.CurveB;     // open curved head at target
ann.ArrowStyle = ArrowStyle.BracketAB;  // perpendicular brackets at both ends
ann.ArrowStyle = ArrowStyle.None;       // no arrowhead (line only)

Callout Boxes

Wrap annotation text in a styled box.

var note = ax.Annotate("Note", 1.5, 1.5);
note.BoxStyle        = BoxStyle.Round;
note.BoxPadding      = 6;
note.BoxCornerRadius = 8;
note.BoxFaceColor    = Colors.White;
note.BoxEdgeColor    = Colors.Gray;
note.BoxLineWidth    = 1.5;
BoxStyle Shape
None No box (default)
Square Plain rectangle
Round Rounded corners (bezier arcs)
RoundTooth Rounded rect with zigzag bottom edge
Sawtooth Sawtooth edges on all sides

Fluent API — Annotation with Arrow

using MatPlotLibNet;
using MatPlotLibNet.Models;

double[] x = [0, 10, 20, 30, 40, 50];
double[] y = [20, 60, 110, 150, 90, 70];

// Single call sets text, position, and arrow target:
Plt.Create()
   .Plot(x, y)
   .Annotate("High", 42.0, 150.0, arrowX: 45.0, arrowY: 90.0, ann =>
   {
       ann.ConnectionStyle = ConnectionStyle.Angle;
       ann.ArrowStyle = ArrowStyle.FancyArrow;
   })
   .ToSvg();

Span Region Border and Label

var span = ax.AxHSpan(1.0, 2.0);
span.Label     = "Support zone";
span.LineStyle = LineStyle.Dashed;
span.LineWidth = 1.5;
span.EdgeColor = Colors.DarkGray;

figure.Save("annotations.svg");

Clone this wiki locally