Skip to content

Examples

H.P. Gansevoort edited this page Oct 21, 2026 · 1 revision

Examples

What each release brought, shown in code: a section to each release, the newest first. Every block on this page is written against the library as it is now and compiled by a test together with every other block of this wiki, so what you copy from here builds. Each example says what you should see when you run it. The page starts at 1.19.0; the changelog says what every release changed and why, including the fixes that ask nothing new of your code.

Release What it brought
1.19.0 A line named where it ends instead of in the legend, an axis coloured to match its line in one call, and secondary axes that take their tick settings, keep their colours through JSON, answer a legend click and report their own values.

1.19.0

New

Name a line where it ends. WithDirectLabel() writes the name of the line added just before it at the right end of that line, in the line's colour, and leaves it out of the legend. A reader no longer has to match a swatch to a line.

using MatPlotLibNet;
using MatPlotLibNet.Models;

double[] month = [1, 2, 3, 4, 5, 6, 7, 8];
double[] oslo = [-4.3, -4.0, -0.2, 4.5, 10.8, 15.2, 16.4, 15.2];
double[] madrid = [6.3, 7.9, 11.2, 12.9, 16.7, 22.2, 25.6, 25.1];
double[] rome = [7.5, 8.5, 10.9, 13.6, 17.6, 21.6, 24.4, 24.5];
double[] average = [3.2, 4.1, 7.3, 10.3, 15.0, 19.7, 22.1, 21.6];

Plt.Create()
    .WithSize(700, 400)
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(month, oslo, s => s.Label = "Oslo").WithDirectLabel()
        .Plot(month, madrid, s => s.Label = "Madrid").WithDirectLabel()
        .Plot(month, rome, s => s.Label = "Rome").WithDirectLabel()
        .Plot(month, average, s => s.Label = "Average of the three")   // no WithDirectLabel: stays in the legend
        .WithLegend(LegendPosition.UpperLeft)
        .SetXLabel("Month")
        .SetYLabel("Mean temperature (°C)"))
    .Save("direct_labels.svg");

Three lines named at their ends, one in the legend

Oslo, Madrid and Rome stand in a column right of the plot, each in its line's colour, at the height where its line ends. Madrid and Rome end 0.6 °C apart, yet their names stand one above the other without touching, in the order the lines end. The legend holds a single entry: Average of the three. The figure keeps its size; the plot narrows to make room for the names.

Show the last value beside the name, or set the placement on the series. WithDirectLabel() is a shorthand for LabelPlacement = SeriesLabelPlacement.LineEnd. With WithLegendValues() the label carries the value it stands next to, as the legend entry would.

using MatPlotLibNet;
using MatPlotLibNet.Models.Series;

double[] day = [1, 2, 3, 4, 5];
double[] visitors = [120, 135, 128, 150, 162];
double[] orders = [31, 38, 35, 44, 47];

Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(day, visitors, s => { s.Label = "Visitors"; s.LabelPlacement = SeriesLabelPlacement.LineEnd; })
        .Plot(day, orders, s => { s.Label = "Orders"; s.LabelPlacement = SeriesLabelPlacement.LineEnd; })
        .WithLegendValues())
    .Save("last_values.svg");

The two labels read Visitors = 162.00 and Orders = 47.00, with a decimal point whatever the culture of the machine. A live line from StreamingPlot takes the same setting. Polar and 3-D axes draw no end labels, so there a line keeps its legend entry.

Colour an axis to match its line. SetYAxisColor and SetXAxisColor colour an axis' spine, tick marks, tick labels and title in one call. They exist on the axes builder (left and bottom axis) and on the secondary-axis builders (right and top axis). A line on the right-hand axis is named at its end as well.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] hour = [0, 4, 8, 12, 16, 20, 24];
double[] temperature = [12, 10, 14, 21, 23, 17, 13];
double[] humidity = [88, 92, 75, 52, 47, 63, 81];

var figure = Plt.Create()
    .WithSize(700, 400)
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(hour, temperature, s => { s.Label = "Temperature"; s.Color = Colors.Crimson; }).WithDirectLabel()
        .SetYLabel("Temperature (°C)")
        .SetYAxisColor(Colors.Crimson)
        .SetXLabel("Hour of the day")
        .WithSecondaryYAxis(right => right
            .Plot(hour, humidity, s => { s.Label = "Humidity"; s.Color = Colors.SteelBlue; })
            .WithDirectLabel()
            .SetYLabel("Relative humidity (%)")
            .SetYAxisColor(Colors.SteelBlue)))
    .TightLayout()
    .Build();

figure.Save("axis_colors.svg");

Left axis in crimson, right axis in steel blue, both lines named at their ends

The left spine, its ticks, its numbers and its title are crimson; the right ones are steel blue. Both names stand right of the plot, past the right-hand numbers and title, each at the height where its own line ends. Everything else on the axes (tick length, font size, spine width) stays as it was. 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.

Keep it through JSON. REST, SignalR, Blazor and the MCP server hand a figure on as JSON. The axis colours and the label placement now travel with it; JSON written by an earlier version reads as no colour and a legend entry.

using MatPlotLibNet.Models.Series;

var copy = ChartServices.Serializer.FromJson(figure.ToJson());
var axes = copy.SubPlots[0];

Console.WriteLine(axes.Spines.Right.Color?.ToHex());                         // #4682B4
Console.WriteLine(axes.SecondaryYAxis!.MajorTicks.LabelColor?.ToHex());      // #4682B4
Console.WriteLine(((LineSeries)axes.SecondarySeries[0]).LabelPlacement);     // LineEnd

It prints #4682B4 twice and LineEnd. A top axis made with WithSecondaryXAxis, with its label, limits and series, survives the trip as well; before this release it was lost on the way.

Colour the top axis, and the title makes room for it. A second X axis along the top reads the same measurements in another unit. Its numbers and title now get room of their own, and the axes title stands above them.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] celsius = [-10, 0, 10, 20, 30, 40];
double[] fahrenheit = [14, 32, 50, 68, 86, 104];
double[] efficiency = [71, 80, 88, 93, 90, 82];

Plt.Create()
    .WithSize(700, 450)
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(celsius, efficiency, s => s.Color = Colors.Teal)
        .SetXLabel("Outdoor temperature (°C)")
        .SetXAxisColor(Colors.Teal)
        .SetYLabel("Efficiency (%)")
        .WithTitle("Heat pump efficiency")
        .WithSecondaryXAxis(top => top
            .ScatterXSecondary(fahrenheit, efficiency, s => s.Color = Colors.Chocolate)
            .SetXLabel("Outdoor temperature (°F)")
            .SetXAxisColor(Colors.Chocolate)))
    .Save("top_axis.svg");

A teal bottom axis in °C, a chocolate top axis in °F, the title above both

The teal line is read in °C on the bottom axis; the chocolate dots, the same measurements read in °F on the top axis, sit on the line. The title stands above the top numbers and the top axis title, and nothing overlaps.

Improved

A secondary axis takes its tick settings. The ticks of the right-hand and the top axis follow that axis' own MajorTicks: direction, length, width, colour, label size, label colour, padding, rotation and Visible = false. Before this release every one of those settings was ignored there.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] hour = [0, 4, 8, 12, 16, 20, 24];
double[] load = [310, 280, 420, 560, 610, 480, 330];
double[] price = [0.21, 0.18, 0.27, 0.34, 0.38, 0.30, 0.22];

var figure = Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(hour, load)
        .SetYLabel("Load (MW)")
        .WithSecondaryYAxis(right => right
            .Plot(hour, price, s => s.Color = Colors.Tab10Orange)
            .SetYLabel("Price (EUR/kWh)")))
    .Build();

var right = figure.SubPlots[0].SecondaryYAxis!;
right.MajorTicks = right.MajorTicks with { Direction = TickDirection.In, Length = 8, LabelSize = 12, LabelColor = Colors.Navy };

figure.Save("right_ticks.svg");

Right-hand ticks pointing into the plot, with larger navy numbers

The right-hand marks point into the plot and are 8 px long, and their numbers are larger and navy. Left untouched, a secondary axis now looks like the primary one: marks 4.86 px long and 1.11 px wide, numbers 9.72 px from the spine.

Mirrored ticks are drawn, and the layout makes room for them. WithXTicksMirrored() repeats the X ticks along the top edge, where before it set a flag nothing read. Mirrored Y ticks point away from the plot on the right edge, as they do on the left.

using MatPlotLibNet;

double[] depth = [0, 50, 100, 150, 200, 250];
double[] pressure = [101325, 605000, 1110000, 1615000, 2120000, 2625000];

Plt.Create()
    .WithSize(600, 400)
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(depth, pressure)
        .WithTitle("Pressure under water")
        .SetXLabel("Depth (m)")
        .SetYLabel("Pressure (Pa)")
        .WithXTicksMirrored()
        .WithYTicksMirrored())
    .TightLayout()
    .Save("mirrored_ticks.svg");

Ticks and numbers on all four sides, the title above the top ones

The depth numbers appear along the top as well as the bottom, and the title stands above them instead of on them. The pressure numbers appear on both sides, and the right margin is as wide as the widest of them. A secondary axis with series takes its edge for its own ticks, so the mirrored ticks are not drawn on that edge.

A legend click hides a line on a secondary axis. A line on the right-hand or the top axis is drawn in a series group of its own, like a line on the main axes. The legend toggle, the hover highlight and a streaming update all find it there.

using MatPlotLibNet;

double[] hour = [0, 4, 8, 12, 16, 20, 24];
double[] temperature = [12, 10, 14, 21, 23, 17, 13];
double[] humidity = [88, 92, 75, 52, 47, 63, 81];

Plt.Create()
    .WithBrowserInteraction()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(hour, temperature, s => s.Label = "Temperature")
        .SetYLabel("Temperature (°C)")
        .WithSecondaryYAxis(right => right
            .Plot(hour, humidity, s => s.Label = "Humidity")
            .SetYLabel("Relative humidity (%)"))
        .WithLegend())
    .Save("legend_toggle.svg");

Open legend_toggle.svg in a browser and click Humidity in the legend: the right-hand line disappears, and a second click brings it back. Before this release the click hid nothing. A click on a plotted line no longer toggles it either; only a click on a legend entry does.

A theme's colour cycle reaches a secondary line. The colour a line is drawn in and the colour of its legend swatch are decided in one place, for the main axes and the secondary ones alike.

using MatPlotLibNet;
using MatPlotLibNet.Styling;

double[] x = [1, 2, 3, 4, 5];
double[] sales = [12, 15, 14, 18, 21];
double[] margin = [0.31, 0.29, 0.33, 0.35, 0.34];

var theme = Theme.CreateFrom(Theme.Default)
    .WithPropCycler(new PropCyclerBuilder().WithColors(Colors.Red, Colors.Green, Colors.Blue).Build())
    .Build();

Plt.Create()
    .WithTheme(theme)
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, sales, s => s.Label = "Sales")
        .WithSecondaryYAxis(right => right.Plot(x, margin, s => s.Label = "Margin"))
        .WithLegend())
    .Save("cycled_secondary.svg");

Sales is drawn red and Margin green (#008000), and the legend swatches show the same two colours. Before, the right-hand line took the plain colour cycle (#FF7F0E) while its swatch showed green. Tooltips (WithTooltips()) and theme settings reach series on a secondary axis too.

A click on a secondary line reports its own values. With the server data cursor, a click on a line on the right-hand axis reports Y in right-hand units, a line on the top axis reports X in top-axis units, and every click is measured against the plot area and the range the axes actually show. Hover, Shift-drag selection, wheel zoom and drag pan work in the units of the axes under the pointer too.

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

double[] hour = [0, 4, 8, 12, 16, 20, 24];
double[] temperature = [12, 10, 14, 21, 23, 17, 13];
double[] humidity = [88, 92, 75, 52, 47, 63, 81];

var figure = Plt.Create()
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(hour, temperature, s => s.Label = "Temperature")
        .WithSecondaryYAxis(right => right.Plot(hour, humidity, s => s.Label = "Humidity")))
    .WithServerInteraction("weather", i => i.EnableDataCursor())
    .Build();

app.Services.GetRequiredService<FigureRegistry>().Register("weather", figure, options => options
    .OnDataCursor(evt =>
    {
        Console.WriteLine($"{evt.Annotation.SeriesLabel}: {evt.Annotation.DataX:0.#} h, {evt.Annotation.DataY:0.#}");
        return ValueTask.CompletedTask;
    }));

app.Run();

A click on the humidity line at noon prints a value near Humidity: 12 h, 52, read on the right-hand axis in percent; a click on the temperature line reads the left axis in °C. Before this release every click was converted with the left axis' limits over the whole picture, so an autoscaled chart reported numbers that matched neither axis. See Bidirectional SignalR for the page this figure is shown on.

Clone this wiki locally