Repository navigation
Chart Types
Try it live: Open the Playground to experiment with charts in your browser. Browse the Cookbook (39 pages) for copy-paste code examples with rendered output.
MatPlotLibNet has 91 series types across 16 categories — four of them streaming — plus 13 map projections and maps (a choropleth, bubbles, flows, a cartogram) via MatPlotLibNet.Geo. A test reads the assembly and checks that count (SeriesCountContractTests). Adding a series makes that test fail, so this number cannot go out of date unnoticed. See Geographic / Maps below for everything the geo package adds. Thirty-eight more charts, from a Pareto chart to a power spectrum, are built in one call from these series and are not series types of their own; see Charts in one call.
The fidelity suite renders these charts and compares them pixel for pixel against matplotlib. Click a tile for the full-resolution PNG.
Thirty-eight charts are one call each on the axes builder. Each verb builds its chart from the series on this page, one
series for each thing it draws, so they are not counted among the 91: a Pareto chart is a bar series and a line on a
right-hand axis, a ridgeline one area for each ridge, a power spectrum one line. The verbs are extension methods in the namespace
MatPlotLibNet, in six classes named for the data they take. Their last argument, configure, styles the series
after the verb has set them up. Each one is shown with its picture on the examples pages (1.20.0, 1.22.0, 1.25.0, 1.26.0 and 1.28.0).
| Data | Class | Verbs |
|---|---|---|
| values keyed by names | CategoricalAxesExtensions |
Lollipop, Dumbbell, Pareto, SlopeGraph, DotPlot, ParallelCoordinates, Marimekko
|
| a sample to estimate from | DistributionAxesExtensions |
Ridgeline, QQPlot, ProbabilityPlot, KaplanMeier, RiskTable, Raincloud, StemAndLeaf
|
| values over x positions | XYAxesExtensions |
BumpChart, ControlChart, ShewhartChart, StreamGraph, Horizon, ConnectedScatter, VLines, HLines, Stairs, Triplot
|
| the cells of a grid | GridAxesExtensions |
CalendarHeatmap, Spy, MatShow, CalendarEvents
|
| a series of prices in time order | PriceAxesExtensions |
Renko, Kagi
|
| a signal: samples taken at a steady rate, in the order they were taken | SignalAxesExtensions |
Psd, Csd, Cohere, MagnitudeSpectrum, PhaseSpectrum, AngleSpectrum, Acorr, Xcorr
|
ProbabilityPlot(sample, reference) is QQPlot against the distribution a QQReference names (exponential, uniform,
log-normal, Student t or your own quantile function), RiskTable(groups) writes the number at risk and the log-rank test under
a KaplanMeier chart, and ShewhartChart(data, rules, limits) draws the moving-range, X-bar, R, p and c charts and flags
Nelson's eight tests. StemAndLeaf(sample) writes every observation on a row, the stem and its leaves, each leaf in a
column of its own, and ConnectedScatter(x, y) joins the points of a scatter with arrows that show the way the path runs.
Stairs(values, edges) draws a value for each bin as a staircase, an outline or, with a baseline, a fill; VLines and HLines draw
a segment at each position between two limits; and Triplot(x, y) draws the edges of a triangulation, the mesh that Tripcolor and
Tricontour fill and contour.
A signal is neither x and y arrays nor a sample, because its order is its meaning, so the eight verbs of SignalAxesExtensions
take a double[] of samples. Psd, Csd and Cohere estimate a power, a cross spectrum and a coherence by Welch's method from
averaged segments, set by one SpectralOptions value (matplotlib's defaults for what it leaves out); MagnitudeSpectrum,
PhaseSpectrum and AngleSpectrum take one transform of the whole signal, set by one SpectrumOptions value; Acorr and Xcorr
correlate a signal with itself or with another, set by one CorrelationOptions value, and draw the correlation as stems or as
markers. A value that is missing is a gap: a sample that is NaN leaves every segment or lag that holds it out, and nothing is drawn
when none is left.
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
string[] kinds = ["Dents", "Scratches", "Stains", "Cracks"];
double[] counts = [98, 142, 45, 67];
double[] lastYear = [104, 120, 51, 70];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Pareto(kinds, counts)).Save("pareto.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Lollipop(kinds, counts)).Save("lollipop.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Dumbbell(kinds, new BarGroup("Last year", lastYear), new BarGroup("This year", counts))).Save("dumbbell.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.DotPlot(kinds, [new BarGroup("This year", counts), new BarGroup("Last year", lastYear)])).Save("dots.svg");The verbs of 1.28.0 take their data in the same way:
using MatPlotLibNet;
double[] edges = [0, 10, 20, 30, 40, 50];
double[] counts = [3, 8, 14, 9, 4];
double[] signal = [.. Enumerable.Range(0, 1000).Select(i => Math.Sin(2 * Math.PI * 50 * i / 1000.0))];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Stairs(counts, edges, 0)).Save("stairs.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Psd(signal, new SpectralOptions { SampleRate = 1000, SegmentLength = 200 })).Save("psd.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Acorr(signal, new CorrelationOptions { MaxLags = 60 })).Save("acorr.svg");Written to JSON, a composed chart is its parts, so the MCP server and every transport carry it and read it back as the same picture; it is not sorted, estimated or laid out again.

using MatPlotLibNet;
using MatPlotLibNet.Styling;
double[] x = [1, 2, 3, 4, 5];
double[] y = [2, 4, 3, 5, 4];
Plt.Create().Plot(x, y).Save("line.svg");
Plt.Create().Plot(x, y, s => { s.Color = Colors.Blue; s.LineWidth = 2; s.Label = "Series A"; }).Save("line.svg");A line can carry its name at its end instead of in the legend: WithDirectLabel() after Plot on the axes builder.
See the examples page.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
double[] x = [1, 2, 3, 4, 5];
double[] y = [2, 4, 3, 5, 4];
Plt.Create().Scatter(x, y).Save("scatter.svg");
Plt.Create().Scatter(x, y, s => { s.Marker = MarkerStyle.Circle; s.MarkerSize = 8; }).Save("scatter.svg");using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
Plt.Create().Bar(["Q1", "Q2", "Q3", "Q4"], [100, 200, 150, 250]).Save("bar.svg");
// Horizontal: the values run along X and the names stand on the Y axis, the first at the bottom as matplotlib's barh puts it.
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.Bar(["Q1", "Q2", "Q3", "Q4"], [100, 200, 150, 250], s => s.Orientation = BarOrientation.Horizontal))
.Save("barh.svg");A bar series and an area series take a GradientColor: the fill runs from the series' colour to that colour, along a bar from its
foot to its end and across an area from its baseline to the point of its top edge furthest from it. A gradient colour with an
alpha of its own fades to clear, and the SVG, PNG and PDF all paint it.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.Bar(["Q1", "Q2", "Q3", "Q4"], [100, 200, 150, 250], s => s.GradientColor = Color.FromHex("#C6DBEF")))
.Save("bar_gradient.svg");
using MatPlotLibNet;
var random = new Random(42);
double[] data = [.. Enumerable.Range(0, 500).Select(_ => random.NextDouble() + random.NextDouble())];
Plt.Create().Hist(data, bins: 20).Save("hist.svg");using MatPlotLibNet;
Plt.Create().Pie([40, 35, 25], ["A", "B", "C"]).Save("pie.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Donut([40, 35, 25], ["A", "B", "C"])).Save("donut.svg");using MatPlotLibNet;
double[] x = [1, 2, 3, 4, 5];
double[] y = [2, 4, 3, 5, 4];
double[] y1 = [1, 3, 2, 4, 3];
double[] y2 = [3, 5, 4, 6, 5];
double[] yErr = [0.3, 0.4, 0.2, 0.5, 0.3];
double[][] groups = [[1, 2, 2, 3, 4], [2, 3, 3, 4, 6], [1, 1, 2, 5, 5]];
Plt.Create().Step(x, y).Save("step.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.FillBetween(x, y1, y2)).Save("fill.svg"); // fill between two lines
Plt.Create().ErrorBar(x, y, yErr, yErr).Save("errbar.svg"); // error below, error above
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Stem(x, y)).Save("stem.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.BoxPlot(groups)).Save("box.svg"); // groups: double[][]
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Violin(groups)).Save("violin.svg");A violin's width at a height is the sample's Gaussian kernel density there, with Silverman's bandwidth (the estimator Kde
and Ridgeline use), scaled so that the widest point is the violin's width; a sample without spread is a spike.
Use these series for data of 100 k points or more. Viewport slicing is O(1) for a uniform sample rate, or an O(log n) binary search for non-uniform X, and LTTB downsampling follows. The render path makes no gen-2 GC allocations.
using MatPlotLibNet;
double[] samples = [.. Enumerable.Range(0, 200_000).Select(i => (i % 441) / 441.0 - 0.5)];
double[] x = [.. Enumerable.Range(0, 200_000).Select(i => i * 0.01 + (i % 7) * 0.001)];
double[] xNonUniform = x;
double[] y = [.. Enumerable.Range(0, 200_000).Select(i => (i % 1000) / 1000.0)];
// Uniform sample rate (e.g. audio, sensor data) — O(1) IndexRangeFor
Plt.Create().Signal(samples, sampleRate: 44100).Save("audio.svg");
// Non-uniform ascending X — O(log n) binary search
Plt.Create().SignalXY(xNonUniform, y).Save("signal_xy.svg");
// One-liner shortcuts
QuickPlot.Signal(samples, sampleRate: 44100, title: "Audio").Save("audio.svg");
QuickPlot.SignalXY(x, y).Save("xy.svg");using MatPlotLibNet;
double[] data = [1.2, 2.3, 2.1, 3.4, 2.8, 3.9, 4.1, 2.6, 3.3, 2.9];
double[][] groups = [[1, 2, 2, 3, 4], [2, 3, 3, 4, 6], [1, 1, 2, 5, 5]];
double[][] events = [[0.5, 1.2, 3.4], [0.8, 2.2, 2.9, 4.1]];
double[] x = [1, 2, 3, 4, 5];
double[] y = [2.1, 3.9, 6.2, 7.8, 10.1];
string[] categories = ["red", "blue", "red", "green", "blue", "red"];
Plt.Create().Kde(data).Save("kde.svg");
Plt.Create().Ecdf(data).Save("ecdf.svg");
Plt.Create().Rugplot(data).Save("rug.svg");
Plt.Create().Stripplot(groups).Save("strip.svg");
Plt.Create().Swarmplot(groups).Save("swarm.svg");
Plt.Create().Pointplot(groups).Save("point.svg");
Plt.Create().Eventplot(events).Save("event.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Regression(x, y)).Save("regression.svg"); // with LeastSquares fit line
Plt.Create().Residplot(x, y).Save("residual.svg"); // residuals of that fit
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Countplot(categories)).Save("count.svg");
FigureTemplates.FinancialDashboard produces a 3-panel layout (price 60 %, volume 15 %, oscillator 25 %) with grid lines, bar-center-aligned tick labels, and optional indicator overlays. Each indicator resolves against the most recently added price series — so .BollingerBands(20) followed by .Sma(5) computes the SMA over the raw close, not the Bollinger middle band.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
double[] open = [100, 102, 101, 104, 106, 105, 108, 110, 109, 112, 111, 114, 116, 115, 118, 117, 120, 122, 121, 124, 126, 125];
double[] close = [102, 101, 104, 106, 105, 108, 110, 109, 112, 111, 114, 116, 115, 118, 117, 120, 122, 121, 124, 126, 125, 128];
double[] high = [.. open.Zip(close, (o, c) => Math.Max(o, c) + 1.5)];
double[] low = [.. open.Zip(close, (o, c) => Math.Min(o, c) - 1.5)];
double[] volume = [.. Enumerable.Range(0, open.Length).Select(i => 1000.0 + 50 * (i % 5))];
FigureTemplates.FinancialDashboard(open, high, low, close, volume,
title: "ACME Corp",
configurePricePanel: ax => {
ax.BollingerBands(20); // BB(20, 2σ) with semi-transparent fill
ax.Sma(5);
},
configureOscillatorPanel: ax => {
ax.Rsi(close, 14);
ax.AxHLine(70, rl => { rl.Color = Color.FromHex("#E24A33"); rl.LineStyle = LineStyle.Dashed; });
ax.AxHLine(30, rl => { rl.Color = Color.FromHex("#E24A33"); rl.LineStyle = LineStyle.Dashed; });
})
.WithSize(1200, 700)
.Save("financial_dashboard.svg");using MatPlotLibNet;
double[] open = [100, 102, 101, 104, 106, 105, 108, 110, 109, 112, 111, 114, 116, 115, 118, 117, 120, 122, 121, 124, 126, 125];
double[] close = [102, 101, 104, 106, 105, 108, 110, 109, 112, 111, 114, 116, 115, 118, 117, 120, 122, 121, 124, 126, 125, 128];
double[] high = [.. open.Zip(close, (o, c) => Math.Max(o, c) + 1.5)];
double[] low = [.. open.Zip(close, (o, c) => Math.Min(o, c) - 1.5)];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.Candlestick(open, high, low, close)
.Sma(period: 20)
.WithLegend())
.Save("candlestick.svg");using MatPlotLibNet;
double[] open = [100, 102, 101, 104, 106];
double[] close = [102, 101, 104, 106, 105];
double[] high = [103, 103, 105, 107, 107];
double[] low = [99, 100, 100, 103, 104];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.OhlcBar(open, high, low, close))
.Save("ohlc.svg");Three charts draw a price without a time axis, from the closes alone. PointAndFigure(prices, boxSize, reversal = 3) is a
series that draws columns of boxes, an X where the price rose and an O where it fell; Renko(prices, brickSize) and
Kagi(prices, reversal) are verbs of PriceAxesExtensions that build a staircase of bricks and one path that thickens and
thins from the series the library already has. Point and Figure asks for SetAspect(1.0 / boxSize) to make its boxes square;
Renko and Kagi carry no ticks on the X axis, since it is only an order. They are shown with their pictures on the
examples page.
using MatPlotLibNet;
double[] closes =
[
100, 102, 105, 103, 108, 112, 109, 107, 111, 115, 118, 114, 110, 106, 103,
107, 111, 116, 121, 119, 115, 112, 117, 123, 128, 125, 121, 118, 122, 127,
];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.PointAndFigure(closes, 2).SetAspect(1.0 / 2)).Save("point_and_figure.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Renko(closes, 3)).Save("renko.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Kagi(closes, 4)).Save("kagi.svg");

using MatPlotLibNet;
var data = new double[10, 12];
for (var r = 0; r < 10; r++)
for (var c = 0; c < 12; c++)
data[r, c] = r * c;
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.Heatmap(data)
.WithColorMap("plasma")
.WithColorBar(cb => cb with { Label = "Intensity" }))
.Save("heatmap.svg");using MatPlotLibNet;
double[] x = [.. Enumerable.Range(0, 20).Select(i => i * 0.5)];
double[] y = [.. Enumerable.Range(0, 15).Select(i => i * 0.5)];
var z = new double[y.Length, x.Length];
for (var r = 0; r < y.Length; r++)
for (var c = 0; c < x.Length; c++)
z[r, c] = (x[c] - 5) * (x[c] - 5) + (y[r] - 3.5) * (y[r] - 3.5);
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Contour(x, y, z)).Save("contour.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Contourf(x, y, z)).Save("contourf.svg");ShowLabels writes each level on its line. A label stands at the middle of its line, turned along the line and kept upright, in
the colour of the line and on a patch the colour of the theme's background; a line shorter than 1.2 times the width of its label
gets none, and LabelFormat and LabelFontSize set the number's format and size. The number is formatted the same in every
culture.

using MatPlotLibNet;
var bitmap = new double[8, 8];
for (var r = 0; r < 8; r++)
for (var c = 0; c < 8; c++)
bitmap[r, c] = (r + c) % 2;
double[] x = [.. Enumerable.Range(0, 20).Select(i => i * 0.5)];
double[] y = [.. Enumerable.Range(0, 15).Select(i => i * 0.5)];
var z = new double[y.Length, x.Length];
for (var r = 0; r < y.Length; r++)
for (var c = 0; c < x.Length; c++)
z[r, c] = x[c] + y[r];
// Pcolormesh takes the cell EDGES: one more than the cells in each direction.
double[] xEdges = [.. Enumerable.Range(0, x.Length + 1).Select(i => i * 0.5 - 0.25)];
double[] yEdges = [.. Enumerable.Range(0, y.Length + 1).Select(i => i * 0.5 - 0.25)];
// Scattered points for the hexbin and the triangulated series.
double[] px = [0, 1, 2, 0, 1, 2, 0.5, 1.5];
double[] py = [0, 0, 0, 1, 1, 1, 2, 2];
double[] pz = [1, 2, 1, 2, 3, 2, 4, 4];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Image(bitmap)).Save("image.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Hexbin(px, py)).Save("hexbin.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Pcolormesh(xEdges, yEdges, z)).Save("pcolor.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Tricontour(px, py, pz)).Save("tricontour.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Tripcolor(px, py, pz)).Save("tripcolor.svg");Triplot(px, py) draws the edges of the same triangulation, one line for every edge, each shared edge once, and takes triangles of
your own as a flat list of point indices. See the examples page.
There are twelve 3-D series. They share a perspective camera, per-face lighting, numeric tick marks along the bounding-box edges, and interactive SVG rotation. A shared cross-series depth queue composites multiple 3-D series on one axes correctly, whatever order you add them in.
using MatPlotLibNet;
// A grid for the surfaces: z[row, column] over y[row] and x[column].
double[] x = [.. Enumerable.Range(0, 10).Select(i => i * 0.5)];
double[] y = [.. Enumerable.Range(0, 10).Select(i => i * 0.5)];
var z = new double[y.Length, x.Length];
for (var r = 0; r < y.Length; r++)
for (var c = 0; c < x.Length; c++)
z[r, c] = x[c] * y[r] / 10;
// Points for the series that take one (x, y, z) per point.
double[] px = [0, 1, 2, 0, 1, 2, 0.5, 1.5];
double[] py = [0, 0, 0, 1, 1, 1, 2, 2];
double[] pz = [1, 2, 1, 2, 3, 2, 4, 4];
double[] heights = pz;
double[] u = [1, 0, 1, 0, 1, 0, 1, 0];
double[] v = [0, 1, 0, 1, 0, 1, 0, 1];
double[] w = [1, 1, 1, 1, 1, 1, 1, 1];
var filled = new bool[3, 3, 3];
filled[1, 1, 1] = true;
// Original 6
Plt.Create().Surface(x, y, z).Save("surface.svg");
Plt.Create().Wireframe(x, y, z).Save("wireframe.svg");
Plt.Create().Scatter3D(px, py, pz).Save("scatter3d.svg");
Plt.Create().Bar3D(px, py, heights).Save("bar3d.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.PlanarBar3D(px, py, heights)).Save("planar_bars.svg");
Plt.Create().Stem3D(px, py, pz).Save("stem3d.svg");
// v1.3.0 — 6 new series
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Plot3D(px, py, pz)).Save("line3d.svg"); // projected polyline
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Trisurf(px, py, pz)).Save("trisurf.svg"); // Delaunay triangulated surface
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Contour3D(x, y, z)).Save("contour3d.svg"); // marching-squares contour lines
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Quiver3D(px, py, pz, u, v, w)).Save("quiver3d.svg"); // 3D vector field
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Voxels(filled)).Save("voxels.svg"); // face-culled cubes
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Text3D(1, 1, 2, "label")).Save("text3d.svg"); // 3D annotation
This is the equivalent of matplotlib's ax.bar3d(...). It draws rectangular prisms with matplotlib-exact per-face shading (ambient + diffuse·(dot(n̂, l̂) + 1)/2, which is 0.65 + 0.35·dot(n̂, l̂) at the defaults), depth-sorted across all bars so occlusion is correct at any camera angle. Multiple Bar3D calls on one axes stack into a grid:

using MatPlotLibNet;
using MatPlotLibNet.Styling;
// One plane per Y, each with its own colour.
var rows = new Dictionary<double, Color>
{
[0] = Colors.Red, [1] = Colors.Green, [2] = Colors.Blue, [3] = Colors.Cyan, [4] = Colors.Gold,
};
double[] xs = [.. Enumerable.Range(0, 20).Select(i => (double)i)];
Plt.Create()
.WithTitle("3D Bar Chart — Grouped rows")
.WithSize(780, 620)
.AddSubPlot(1, 1, 1, ax =>
{
ax.WithCamera(elevation: 25, azimuth: -60)
.WithLighting(dx: -0.4, dy: -0.8, dz: 0.45);
foreach (var (y, color) in rows)
{
double[] ys = Enumerable.Repeat(y, 20).ToArray();
double[] zs = [.. xs.Select(x => 1 + (x + y) % 5)];
ax.Bar3D(xs, ys, zs, s => { s.Color = color; s.BarWidth = 0.4; });
}
})
.Save("bar3d_grouped.svg");
This is the equivalent of matplotlib's ax.bar(xs, heights, zs=y, zdir='y'), also called a "skyscraper plot". Each bar is a flat translucent rectangle in the XZ plane at a fixed Y, so you can read the bars through the rear planes. Use it to compare many series stacked on planes.
It reuses the rows and xs of the grouped example above:
Plt.Create()
.WithTitle("Planar 3D Bars")
.WithSize(780, 620)
.AddSubPlot(1, 1, 1, ax =>
{
ax.WithCamera(elevation: 25, azimuth: -60)
.SetXLabel("X").SetYLabel("Y").SetZLabel("Z");
foreach (var (y, color) in rows)
{
double[] ys = Enumerable.Repeat(y, 20).ToArray();
double[] zs = [.. xs.Select(x => 1 + (x + y) % 5)];
ax.PlanarBar3D(xs, ys, zs, s =>
{
s.Color = color;
s.BarWidth = 0.8;
s.Alpha = 0.8; // translucency
});
}
})
.Save("planar_bars.svg");PlanarBar3DSeries.Colors is a parallel Color[]? array, the same convention as ScatterSeries.Colors / PieSeries.Colors. When you set it, it overrides the per-series Color for each bar. That gives three ways to pick colours from one API:
-
Per Y / per plane — set
s.Color = planeColoron each series -
Per X — set
s.Colors = xs.Select(x => lookup(x)).ToArray() -
Combined — set both:
Colors[i]wins where defined,Coloris the fallback

using MatPlotLibNet;
using MatPlotLibNet.Styling;
double[] xs = [-2, -1, 0, 1, 2];
double[] ys = [1, 1, 1, 1, 1];
double[] zs = [3, 4, 5, 4, 3];
var planeColor = Colors.Gold;
var darkCyan = Color.FromHex("#008B8B");
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.PlanarBar3D(xs, ys, zs, s =>
{
s.Color = planeColor;
s.BarWidth = 0.8;
s.Alpha = 0.8;
// Per-X override: every bar at x == 0 is dark cyan.
s.Colors = xs.Select(x => x == 0 ? darkCyan : planeColor).ToArray();
}))
.Save("planar_bars_x0_highlight.svg");See Advanced for how the library composites several 3-D series by depth.
Applies to every 3-D series:
using MatPlotLibNet;
double[] x = [.. Enumerable.Range(0, 41).Select(i => -8 + i * 0.4)];
double[] y = x;
var z = new double[y.Length, x.Length];
for (var r = 0; r < y.Length; r++)
for (var c = 0; c < x.Length; c++)
{
var radius = Math.Sqrt(x[c] * x[c] + y[r] * y[r]);
z[r, c] = radius == 0 ? 1 : Math.Sin(radius) / radius;
}
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Surface(x, y, z)
.SetXLabel("x").SetYLabel("y").SetZLabel("sin(r)/r")
.SetZLim(-0.3, 1.0)
.WithCamera(elevation: 35, azimuth: -55, distance: 8)
.WithLighting(dx: 1.0, dy: 0.0, dz: 0.5, ambient: 0.4, diffuse: 0.6))
.Save("surface3d.svg");distance controls perspective (orthographic when omitted). SetZLabel/SetZLim write to Axes.ZAxis (Axis3D), like SetXLabel/SetYLabel on 2-D charts. With3DRotation() is called on the figure builder and embeds JS for mouse-drag and keyboard rotation (arrow keys + Home reset).
Builder placement (v1.1.4+) — call
WithCamera/WithLightinginside theAddSubPlot(..., ax => ax...)lambda. At thePlt.Create().WithCamera(...)level they only configure the unused figure-level default axes on multi-subplot figures.

Draws a 3-D polyline through arbitrary (x, y, z) points. It is depth-sorted and projected through Projection3D.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
double[] t = Enumerable.Range(0, 200).Select(i => i * 0.1).ToArray();
double[] x = t.Select(v => Math.Cos(v)).ToArray();
double[] y = t.Select(v => Math.Sin(v)).ToArray();
double[] z = t;
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Plot3D(x, y, z, s => { s.Color = Colors.Blue; s.Label = "Helix"; })
.WithCamera(elevation: 25, azimuth: -60))
.Save("line3d.svg");
Triangulates unstructured (x, y, z) point clouds with Delaunay triangulation. Each face is depth-sorted, then shaded with Vec3.FaceNormal and Color.Shade().
using MatPlotLibNet;
using MatPlotLibNet.Styling;
// An unstructured cloud: 60 points on a spiral, z = x·y.
double[] x = [.. Enumerable.Range(0, 60).Select(i => i * 0.05 * Math.Cos(i * 0.5))];
double[] y = [.. Enumerable.Range(0, 60).Select(i => i * 0.05 * Math.Sin(i * 0.5))];
double[] z = [.. x.Zip(y, (a, b) => a * b)];
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Trisurf(x, y, z, s => { s.Color = Colors.Teal; s.Alpha = 0.8; })
.WithCamera(elevation: 30, azimuth: -45))
.Save("trisurf.svg");
Computes contour lines on a grid via marching squares and projects them into 3-D space at their Z level.
using MatPlotLibNet;
double[] x = [.. Enumerable.Range(0, 30).Select(i => -3 + i * 0.2)];
double[] y = x;
var z = new double[y.Length, x.Length];
for (var r = 0; r < y.Length; r++)
for (var c = 0; c < x.Length; c++)
z[r, c] = Math.Exp(-(x[c] * x[c] + y[r] * y[r]) / 2);
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Contour3D(x, y, z, s => { s.Levels = 10; })
.WithCamera(elevation: 35, azimuth: -55))
.Save("contour3d.svg");
Draws arrows in 3-D space, each with a shaft and a cone head. Use it for electromagnetic fields, fluid flow, or gradient vectors.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
// A swirl: at each point on a 4×4 grid in the z = 0 plane, an arrow turning about the z axis and rising.
double[] x = [.. Enumerable.Range(0, 16).Select(i => (double)(i % 4))];
double[] y = [.. Enumerable.Range(0, 16).Select(i => (double)(i / 4))];
double[] z = new double[16];
double[] u = [.. y.Select(b => -(b - 1.5))];
double[] v = [.. x.Select(a => a - 1.5)];
double[] w = [.. Enumerable.Repeat(0.5, 16)];
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Quiver3D(x, y, z, u, v, w, s => { s.Color = Colors.Red; s.ArrowLength = 0.3; })
.WithCamera(elevation: 25, azimuth: -60))
.Save("quiver3d.svg");
Renders a bool[,,] voxel grid as face-culled cubes; adjacent filled voxels suppress shared faces. Visible faces are depth-sorted through DepthQueue3D.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
var filled = new bool[5, 5, 5];
// fill some voxels...
filled[2, 2, 2] = true;
filled[2, 2, 3] = true;
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Voxels(filled, s => { s.Color = Colors.Orange; s.Alpha = 0.8; })
.WithCamera(elevation: 30, azimuth: -50))
.Save("voxels.svg");
Places text labels at arbitrary (x, y, z) positions, projected to 2-D pixel space.
using MatPlotLibNet;
using MatPlotLibNet.Styling;
double[] x = [.. Enumerable.Range(0, 21).Select(i => -2 + i * 0.2)];
double[] y = x;
var z = new double[y.Length, x.Length];
for (var r = 0; r < y.Length; r++)
for (var c = 0; c < x.Length; c++)
z[r, c] = Math.Exp(-(x[c] * x[c] + y[r] * y[r]));
Plt.Create()
.With3DRotation()
.AddSubPlot(1, 1, 1, ax => ax
.Surface(x, y, z)
.Text3D(0, 0, 1.0, "Peak", s => { s.Color = Colors.Red; })
.WithCamera(elevation: 35, azimuth: -55))
.Save("text3d.svg");using MatPlotLibNet;
double[] r = [1, 2, 3, 4, 5];
double[] theta = Enumerable.Range(0, 5).Select(i => i * Math.PI / 2.5).ToArray();
Plt.Create().PolarPlot(r, theta).Save("polar_line.svg");
Plt.Create().PolarScatter(r, theta).Save("polar_scatter.svg");
// Wind rose / radar bar
double[] speeds = [5, 10, 8, 3, 7, 12, 6, 9];
double[] dirs = Enumerable.Range(0, 8).Select(i => i * Math.PI / 4).ToArray();
Plt.Create().PolarBar(speeds, dirs, b => b.BarWidth = 0.7).Save("windrose.svg");A polar axes has one radial scale that the rings and every polar series read, and with SetBarMode(BarMode.Stacked) its
PolarBar series stand on one another, one call for each speed class; see Examples.
using MatPlotLibNet;
using MatPlotLibNet.Models;
var tree = new TreeNode
{
Label = "Revenue",
Children =
[
new TreeNode { Label = "Products", Value = 400 },
new TreeNode { Label = "Services", Value = 300 },
new TreeNode { Label = "Licensing", Value = 200 }
]
};
Plt.Create().Treemap(tree).Save("treemap.svg");
Plt.Create().Sunburst(tree).Save("sunburst.svg");Icicle(root) draws a TreeNode hierarchy as one row of cells for each level, the cells of a node side by side under it,
each as wide as its share of the node's width. It takes the colour map, labels and limits the other hierarchical series take;
the root itself is not drawn.
using MatPlotLibNet;
using MatPlotLibNet.Models;
var tree = new TreeNode
{
Label = "Revenue",
Children =
[
new TreeNode { Label = "Products", Value = 400 },
new TreeNode { Label = "Services", Value = 300 },
new TreeNode { Label = "Licensing", Value = 200 }
]
};
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Icicle(tree)).Save("icicle.svg");
Draws hierarchical clustering as the usual "U"-shaped segments. Each internal node is a merge, and its vertical position is the merge distance (TreeNode.Value). This follows the convention of SciPy's scipy.cluster.hierarchy.dendrogram.
using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Models.Series;
using MatPlotLibNet.Styling;
// Four leaves; each internal node's Value is the distance at which its children merged.
var tree = new TreeNode
{
Label = "root", Value = 3.0,
Children =
[
new TreeNode { Label = "ab", Value = 1.0, Children = [new TreeNode { Label = "a" }, new TreeNode { Label = "b" }] },
new TreeNode { Label = "cd", Value = 2.0, Children = [new TreeNode { Label = "c" }, new TreeNode { Label = "d" }] },
],
};
Plt.Create()
.Dendrogram(tree, s =>
{
s.Orientation = DendrogramOrientation.Top; // Top, Bottom, Left, Right
s.CutHeight = 1.5; // dashed reference line + cluster colours
s.CutLineColor = Colors.Red;
s.ColorByCluster = true; // qualitative IColorMap (default Tab10)
})
.Save("dendrogram.svg");CutHeight uses strict less-than (node.Value < cut), which matches SciPy's color_threshold convention. A single-leaf tree renders the lone label at the centre of the plot. If every merge distance is zero, the drawing collapses to the leaf baseline; a unit maxMerge fallback avoids a division by zero. See the cookbook entry.
Combines a heatmap with optional row and column dendrograms in one subplot, the equivalent of seaborn's sns.clustermap. When you supply trees, the rows and columns are reordered to match the dendrogram leaf order, so the cells line up with the tree.
using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Styling.ColorMaps;
double[,] data =
{
{ 0.9, 0.1, 0.8 },
{ 0.2, 0.7, 0.3 },
{ 0.8, 0.2, 0.9 },
};
// Rows 0 and 2 are alike, so they merge first; columns 0 and 2 likewise.
var rowTree = new TreeNode
{
Value = 1.2,
Children =
[
new TreeNode { Value = 0.3, Children = [new TreeNode { Value = 0 }, new TreeNode { Value = 2 }] },
new TreeNode { Value = 1 },
],
};
var colTree = new TreeNode
{
Value = 1.1,
Children =
[
new TreeNode { Value = 0.2, Children = [new TreeNode { Value = 0 }, new TreeNode { Value = 2 }] },
new TreeNode { Value = 1 },
],
};
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.Clustermap(data, s =>
{
s.RowTree = rowTree; // leaf Value = original row index
s.ColumnTree = colTree; // leaf Value = original column index
s.RowDendrogramWidth = 0.15; // fraction of width, default 0.15, clamped [0, 0.9]
s.ColumnDendrogramHeight = 0.15;
s.ColorMap = DivergingColorMaps.RdBu;
s.ShowLabels = true;
s.LabelFormat = "F2";
}))
.ToSvg();A leaf's TreeNode.Value must be the zero-based index of the original row or column, and internal nodes carry the merge distance. A malformed tree falls back to the identity order without reporting an error. See the cookbook entry.
Builds an N×N matrix of subplots from N variables, the equivalent of seaborn's pairplot. Diagonal cells show the distribution of one variable, as a histogram or a KDE. Off-diagonal cells show (i, j) scatters of two variables, optionally coloured by hue group.
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
double[] petalLength = [1.4, 1.3, 4.7, 4.5, 6.0, 5.1];
double[] petalWidth = [0.2, 0.2, 1.4, 1.5, 2.5, 1.9];
double[] sepalLength = [5.1, 4.9, 7.0, 6.4, 6.3, 5.8];
double[] sepalWidth = [3.5, 3.0, 3.2, 3.2, 3.3, 2.7];
Species[] species = [Species.Setosa, Species.Setosa, Species.Versicolor, Species.Versicolor, Species.Virginica, Species.Virginica];
double[][] vars = [petalLength, petalWidth, sepalLength, sepalWidth];
int[] hue = species.Select(s => (int)s).ToArray();
string[] hLab = ["Setosa", "Versicolor", "Virginica"];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.PairGrid(vars, s =>
{
s.Labels = ["Petal L", "Petal W", "Sepal L", "Sepal W"];
s.HueGroups = hue;
s.HueLabels = hLab;
s.DiagonalKind = PairGridDiagonalKind.Kde; // Histogram (default), Kde, None
s.Triangular = PairGridTriangle.LowerOnly; // Both (default), LowerOnly, UpperOnly
}))
.ToSvg();
enum Species { Setosa, Versicolor, Virginica }Diagonal histograms stack one per hue group at Alpha = 0.6, and KDE draws one curve per group. Off the diagonal, the dot radius follows MarkerSize. OffDiagonalKind = None shows the diagonal marginals only. = Hexbin draws density grids for exploratory analysis of high-cardinality data, and ignores hue (v1.10). CellSpacing defaults to 0.02 and is clamped to [0, 0.2]. See the cookbook entry.

Steady pictures. The interactive view starts pixel-identical to the static SVG: every node at every depth is visible on first paint. Children paint over parents, so where rectangles overlap you see the deepest visible label. Clicking a parent rectangle collapses its subtree; click again to restore it. The visibility model walks the ancestry, so a collapsed subtree keeps its descendants' own collapse state. Multiple subtrees collapse independently. Leaves are not clickable.
The drilldown is keyboard-accessible through tabindex="0" and ARIA roles. Calling WithTreemapDrilldown() embeds the interactive script. Every rect is tagged with data-treemap-node / -depth / -parent, so the script can navigate without re-parsing. For static SVG output with deep trees, call .WithAutoSize(root) so the canvas fits every label.
using MatPlotLibNet;
using MatPlotLibNet.Models;
var catalogue = new TreeNode
{
Label = "Catalogue",
Children =
[
new TreeNode { Label = "Books", Children = [new TreeNode { Label = "Fiction", Value = 120 }, new TreeNode { Label = "Science", Value = 80 }] },
new TreeNode { Label = "Music", Children = [new TreeNode { Label = "Jazz", Value = 60 }, new TreeNode { Label = "Classical", Value = 40 }] },
],
};
Plt.Create()
.WithTreemapDrilldown()
.AddSubPlot(1, 1, 1, ax => ax.Treemap(catalogue, s => s.ShowLabels = true))
.Save("treemap_drilldown.svg");Draws indented rows with right-aligned numeric columns, the shape of htop's process tree. The ARIA spec has a
role for it (treegrid); a treemap has no role at all.
Why it sits beside the treemap instead of replacing it. A treemap shows at a glance which items are big, and it stops there. NN/g puts its useful depth at two or three levels, and a rectangle too small for its label has to fall back to a tooltip. Measured on one running system, two of its twenty-three lanes carried every message in an hour. Drawn as area, that is two rectangles and twenty-one slivers. Drawn as rows, it is a column of numbers you can compare exactly. Use the treemap when the question is how a whole divides up. Use the tree grid when you need to compare values.
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
using MatPlotLibNet.Styling;
TreeGridRow[] rows =
[
new("Svc.Ingest", ["56.0%", "1450 MB", "34"]) { Depth = 0, Expanded = true, Url = "/?process=ingest" },
new("obs-ingest", ["31.2%", "—", "12"]) { Depth = 1 },
new("metrics-fold", ["18.7%", "—", "9"]) { Depth = 1, Accent = Colors.Orange },
new("Ldr.Billing", ["46.0%", "920 MB", "27"]) { Depth = 0, Expanded = false, Url = "/?process=billing" },
];
Plt.Create()
.AddSubPlot(1, 1, 1, ax =>
{
ax.TreeGrid(rows, g =>
{
g.ColumnHeaders = ["load", "working set", "threads"];
g.RowHeight = 22;
g.IndentWidth = 18;
});
ax.HideAllAxes();
})
.Save("tree_grid.svg");The caller flattens the tree. Only the caller knows which subtrees are open, so the series draws the rows it
is handed and nothing else. A row's Url wraps that row in an SVG <a href>, so expanding a subtree is a link
to another URL and needs no script. That is also what makes the control-room descent keyboard-reachable.
Expanded drives the chevron (▾ open, ▸ closed) and the row's aria-expanded; null marks a leaf, which has
nothing to disclose. Depth becomes aria-level, which gives the grid a navigable structure rather than a
purely visual one. Accent sets the colour of a row's text. Use it only where a value deviates, which is the
rule for colour everywhere else on a wall display.
The grid carries no data of its own and adds nothing to the axes range. It fills the region it is given.

Renders a two-level TreeNode as an inner pie with an outer breakdown ring. It wraps Sunburst(...) with InnerRadius = 0.
using MatPlotLibNet;
using MatPlotLibNet.Models;
var departments = new TreeNode
{
Label = "Revenue",
Children = [
new() { Label = "Electronics", Children = [
new() { Label = "Phones", Value = 40 },
new() { Label = "Laptops", Value = 35 },
new() { Label = "Audio", Value = 25 }] },
/* more departments... */
]
};
Plt.Create()
.WithTitle("Revenue by department and product")
.AddSubPlot(1, 1, 1, ax => ax.NestedPie(departments))
.Save("nested_pie.svg");Draws nodes and edges in 2D. Use it for correlation networks (Pearson edge weights), lead-lag flow (TransferEntropy directed edges), Louvain community visualisation (node colour is the community ID), and minimum spanning trees. Five layouts place the nodes: Manual keeps the coordinates you give, Circular, Hierarchical and Arc are fixed arithmetic (Arc puts the nodes in a row, in the order stored, and draws every edge as a semicircle above it, a half circle under SetAspect(1)), and ForceDirected (Fruchterman–Reingold) pulls connected nodes together, starting from positions drawn with a seed, so the same graph and seed give the same picture.
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
using MatPlotLibNet.Styling.ColorMaps;
GraphNode[] nodes =
[
new("AAPL", ColorScalar: 0.2),
new("MSFT", ColorScalar: 0.2),
new("GOOG", ColorScalar: 0.7),
];
GraphEdge[] edges =
[
new("AAPL", "MSFT", Weight: 0.85),
new("AAPL", "GOOG", Weight: 0.42, IsDirected: true),
new("MSFT", "GOOG", Weight: 0.55, IsDirected: true),
];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.NetworkGraph(nodes, edges, s =>
{
s.Layout = GraphLayout.Circular; // Manual / Circular / Hierarchical
s.ColorMap = ColorMaps.Viridis;
s.ShowNodeLabels = true;
s.NodeRadiusScale = 8.0;
}))
.ToSvg();An edge with IsDirected gets an arrowhead at the target end (ArrowHeadBuilder.FancyArrow). EdgeThicknessScale multiplies each edge's Weight to give the stroke width, and NodeRadiusScale multiplies each node's SizeScalar to give the circle radius. There is a DataFrame extension too: df.NetworkGraph("source", "target", weightCol: "weight", directedCol: "directed") derives the nodes from the union of the source and target columns.
Plots a JdK-style 2D scatter of (RS-Ratio, RS-Momentum) for each asset against a benchmark. Each asset gets a fading tail, and a 100/100 quadrant grid splits the plot into Leading, Weakening, Lagging and Improving. There are three RS formulas: DualEma (the default, for trending markets), ZScore (for mean reversion) and LogReturn (for high volatility). The series arrived in v1.11.0, and overlay support in v1.11.2.
using MatPlotLibNet;
using MatPlotLibNet.Styling.ColorMaps;
// Sixty daily closes per asset; each drifts at its own rate against the benchmark.
double[] Closes(double start, double drift) => [.. Enumerable.Range(0, 60).Select(i => start * (1 + drift * i + 0.01 * (i % 7)))];
double[] btcCloses = Closes(60_000, 0.001);
double[] ethCloses = Closes(3_000, 0.003);
double[] bnbCloses = Closes(500, -0.001);
double[] solCloses = Closes(150, 0.002);
// Basic — three alts vs BTC benchmark
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.SetXLabel("RS-Ratio")
.SetYLabel("RS-Momentum")
.RelativeRotation(
new[] { ethCloses, bnbCloses, solCloses },
btcCloses,
new[] { "ETH", "BNB", "SOL" },
s => { s.TailLength = 12; s.ColorMap = ColorMaps.Plasma; }))
.ToSvg();
// With absorption + ENB overlays (Layer 3 → Layer 2 feedback)
double[][] assetCloses = [ethCloses, bnbCloses, solCloses];
double[] bench = btcCloses;
string[] labels = ["ETH", "BNB", "SOL"];
double[] absorptionPerBar = [.. Enumerable.Range(0, 60).Select(i => 0.4 + 0.005 * i)];
double[] enbPerBar = [.. Enumerable.Range(0, 60).Select(i => 2.0 + (i % 10) * 0.1)];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.RelativeRotation(assetCloses, bench, labels, s =>
{
s.AbsorptionRatioPerBar = absorptionPerBar; // double[] [0..1], green→red fill
s.EnbPerBar = enbPerBar; // double[], radius ∝ ENB
}))
.ToSvg();See RelativeRotationSeries for the full property reference, formula details, and minimum bar requirements.

The v1.1.4 renderer runs an 8-step pipeline: column assignment, node alignment, iterative vertical relaxation, gradient link colour modes, sub-labels, inside-labels, hover emphasis and vertical orientation. The sample gallery is in images/sankey_*.png:
-
sankey_process_distribution.svg— a 5-column process cascade with tonnage sub-labels, gradient links and hover emphasis -
sankey_income_statement.svg— the J&J Q1 FY25 income statement, with green and red node colouring and "+2% Y/Y" sub-label deltas -
sankey_customer_journey.svg— a 4-timestep alluvial diagram whereHomereappears across columns, pinned with an explicitSankeyNode.Column -
sankey_un_expenses.svg— a 2-column baseline with outside labels on aHideAllAxes()canvas -
sankey_severity_cascade.svg— 4-column patient severity transitions with 24 relaxation iterations -
sankey_vertical.svg— a vertical (top-to-bottom) conversion funnel
Minimal example:
using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Models.Series;
using MatPlotLibNet.Styling;
SankeyNode[] nodes = [
new("Coal", Color.FromHex("#6B8E23")),
new("Gas", Color.FromHex("#4682B4")),
new("Electricity", Color.FromHex("#FFD700"), SubLabel: "80 TWh"),
new("Heat", Color.FromHex("#D2691E"), SubLabel: "40 TWh")];
SankeyLink[] links = [new(0, 2, 50), new(1, 2, 30), new(1, 3, 20)];
Plt.Create()
.WithSankeyHover() // v1.1.4 hover emphasis script
.AddSubPlot(1, 1, 1, ax => ax
.HideAllAxes()
.Sankey(nodes, links, s =>
{
s.NodeWidth = 24;
s.Iterations = 20; // vertical relaxation passes
s.LinkColorMode = SankeyLinkColorMode.Gradient;
s.Orient = SankeyOrientation.Horizontal; // or Vertical
}))
.Save("sankey.svg");Key properties (v1.1.4):
| Property | Meaning |
|---|---|
SankeyNode.SubLabel / SubLabelColor
|
A secondary label drawn one line below at 80 % font size. Useful for metric and delta pairs such as "$13.9B +2%" |
SankeyNode.Column |
Overrides the computed column. It pins the node to a semantic column whatever the BFS topology says. Use it for alluvial and time-step Sankeys |
SankeySeries.NodeAlignment |
Justify (default) / Left / Right / Center — matches D3 sankey* modes |
SankeySeries.Iterations |
The number of vertical relaxation passes; the default is 6. Use more for denser topologies |
SankeySeries.LinkColorMode |
Source / Target / Gradient (default). Gradient blends from the colour of the source to the colour of the target along each link, in an SVG, a PNG and a PDF alike. |
SankeySeries.Orient |
Horizontal (default) / Vertical — top-to-bottom flow |
SankeySeries.InsideLabels |
Draw labels centred inside the node rect (when NodeWidth is wide enough) |
Chord(names, flows) draws the flows between groups as a ring of arcs with a ribbon through the centre for each flow. An arc is as
long as the row of its group, and a ribbon takes the colour of the group that sends more of the two. A ChordSeries has no axes
around it:
using MatPlotLibNet;
string[] regions = ["North", "South", "East", "West"];
double[,] moves =
{
{ 0, 12, 8, 3 },
{ 9, 0, 5, 7 },
{ 4, 6, 0, 10 },
{ 2, 8, 11, 0 },
};
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Chord(regions, moves)).Save("chord.svg");
| Property | Meaning |
|---|---|
Names, Flows
|
The groups, and Flows[i, j], how much goes from group i to group j. The matrix must be square, with no negative or infinite flow and at least one above zero, or the call is refused by name |
Padding |
The gap between neighbouring arcs, in radians; 0.05 unless said otherwise |
RibbonAlpha |
How opaque a ribbon is, from 0 to 1; 0.67 unless said otherwise, so a ribbon behind another shows |
Colors |
The colour of each group, by position; what the theme hands out when null |
The ribbons inside an arc run in the order that makes the ribbons of a complete graph cross least (0, 1, 5 and 15 crossings for three to six groups, the fewest any layout can give). The names stand outside the ring and never read upside down. The table is source, target and value for each flow there is.
using MatPlotLibNet;
// A rotating field on a 5×5 grid.
double[] x = [.. Enumerable.Range(0, 25).Select(i => (double)(i % 5) - 2)];
double[] y = [.. Enumerable.Range(0, 25).Select(i => (double)(i / 5) - 2)];
double[] u = [.. y.Select(b => -b)];
double[] v = x;
// Barbs take a speed and a compass direction (degrees, 0 = north, clockwise) per point.
double[] speed = [.. u.Zip(v, (a, b) => 10 * Math.Sqrt(a * a + b * b))];
double[] direction = [.. u.Zip(v, (a, b) => (450 - Math.Atan2(b, a) * 180 / Math.PI) % 360)];
// Streamplot takes the field on a grid: gu[row, column] over gy[row] and gx[column].
double[] gx = [-2, -1, 0, 1, 2];
double[] gy = [-2, -1, 0, 1, 2];
var gu = new double[gy.Length, gx.Length];
var gv = new double[gy.Length, gx.Length];
for (var r = 0; r < gy.Length; r++)
for (var c = 0; c < gx.Length; c++)
{
gu[r, c] = -gy[r];
gv[r, c] = gx[c];
}
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Quiver(x, y, u, v)).Save("quiver.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Barbs(x, y, speed, direction)).Save("barbs.svg"); // wind speed + direction
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Streamplot(gx, gy, gu, gv)).Save("stream.svg"); // u, v on a grid
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
string[] categories = ["Speed", "Power", "Range", "Comfort", "Price"];
double[] values = [4, 3, 5, 2, 4];
string[] labels = ["Start", "Sales", "Costs", "Tax", "End"];
double[] deltas = [100, 40, -25, -10, 105];
double[] timeSeries = [3, 4, 3.5, 5, 4.8, 6, 5.5];
string[] headers = ["Name", "Qty"];
string[][] rows = [["Apples", "12"], ["Pears", "7"]];
double[] signal = [.. Enumerable.Range(0, 4096).Select(i => (i % 100) / 100.0)];
BarRange[][] ranges = [[new(1, 2), new(4, 1.5)], [new(0.5, 1), new(3, 2.5)]]; // one row per array: (start, width)
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Radar(categories, values)).Save("radar.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Waterfall(labels, deltas, w => w.TotalIndices = [0, 4])).Save("waterfall.svg"); // Start and End are totals: they stand on zero
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Funnel(labels, values)).Save("funnel.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Gauge(0.72, g => { g.Min = 0; g.Max = 1; })).Save("gauge.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.ProgressBar(0.65)).Save("progress.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Sparkline(timeSeries)).Save("sparkline.svg");
Plt.Create().Table(rows, t => t.ColumnHeaders = headers).Save("table.svg");
Plt.Create().Spectrogram(signal, sampleRate: 44100).Save("spec.svg");
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.BrokenBarH(ranges)).Save("brokenbar.svg");A spectrogram weights each window once with the Hann window, transforms it at exactly the window's length, and draws each row on the frequency of its bin, so a window of 100 samples at 1000 samples a second gives 51 rows 10 Hz apart and a 125 Hz tone fills the rows of 120 Hz and 130 Hz. See the examples page.
Waffle(categories, values) fills a grid of square cells, ten by ten unless Rows and Columns say otherwise, by category in
proportion to its share, so "46 of every 100" is forty-six squares you can count. The cells left over after each category took
the whole number its share comes to go to the largest remainders, and the cells fill from the bottom-left corner, row by row.
using MatPlotLibNet;
string[] heating = ["Gas", "Electric", "Heat pump", "Other"];
double[] households = [46, 31, 17, 6];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Waffle(heating, households).WithLegend()).Save("waffle.svg");
A waterfall can mark a level on each bar: Targets holds one absolute level for each step, on the scale the bars stand on, and the
chart draws a short mark across the bar at each level, over it. A missing level (NaN) is a step with no mark, and the value axis
makes room for the highest. A TableSeries takes ColumnAlignments (left, centre or right for each data column) and
ColumnWidths (relative widths).

Three parts that make a whole, in a triangle. TernaryScatter(top, left, right) puts a marker on each composition and
TernaryPlot(top, left, right) joins them with a line. A ternary axes is a fourth coordinate system beside cartesian, polar and 3-D
(CoordinateSystem.Ternary), with a grid at every fifth and ticks that run counter-clockwise: the right part along the base, the top
part up the right edge, the left part down the left edge. The corners are named by the labels the axes already has: SetYLabel at
the apex, SetZLabel at the bottom left and SetXLabel at the bottom right.
using MatPlotLibNet;
double[] clay = [0.55, 0.30, 0.12, 0.20, 0.40, 0.08, 0.28, 0.65];
double[] silt = [0.25, 0.40, 0.18, 0.60, 0.15, 0.12, 0.22, 0.15];
double[] sand = [0.20, 0.30, 0.70, 0.20, 0.45, 0.80, 0.50, 0.20];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.SetYLabel("Clay").SetZLabel("Silt").SetXLabel("Sand")
.TernaryScatter(clay, silt, sand))
.Save("soil.svg");
The amounts stay as the author knew them, a count, a percentage or a mass, and are closed to fractions only to be placed, so (2, 5, 3)
and (20, 50, 30) stand at the same place and the table still shows what was typed. A sample with a negative, infinite or missing part, or
with three zeros, is refused by its row and its part. The triangle is a fixed frame: a wheel, a drag or Home over it leaves the chart as
it is, because a composition cannot lie outside it. Hovering a point names its composition. The chart is held against mpltern in the
fidelity suite.
Piper(samples) takes a WaterSample in mg/L for each water (calcium, magnesium, sodium, potassium, bicarbonate, carbonate,
sulphate and chloride) and draws the cations on one triangle, the anions on a second and both on a diamond between and above them, one
colour for each water on all three panels. The percentages are of milliequivalents:
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
WaterSample[] waters =
[
new() { Label = "Mountain spring", Ca = 60, Mg = 10, Na = 8, K = 2, HCO3 = 200, SO4 = 20, Cl = 10 },
new() { Label = "Brine well", Ca = 400, Mg = 100, Na = 5000, K = 50, HCO3 = 150, SO4 = 300, Cl = 8000 },
new() { Label = "Sodium spring", Ca = 10, Mg = 3, Na = 300, K = 5, HCO3 = 600, SO4 = 30, Cl = 40 },
];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.Piper(waters)).Save("piper.svg");
The table gives the six percentages, the charge balance error and "balanced" or "unbalanced" for each water: a water whose charges do
not balance, beyond 5 %, is flagged and not dropped. A water with an amount that is negative, infinite or missing, or with no cations or
no anions, is refused by its row and its ion. A PiperSeries has no axes around it.
WordCloud(words, weights) sets each word in a size that follows the square root of its weight, between two sizes (12 and 60 unless
Sizes says otherwise), the heaviest word at the centre and the rest on a spiral at the first place where they fit with two pixels
clear:
using MatPlotLibNet;
string[] words = ["price", "support", "speed", "design", "docs", "bugs", "export", "themes", "charts", "install", "license", "maps"];
double[] weights = [42, 35, 30, 24, 22, 18, 16, 14, 13, 9, 7, 5];
Plt.Create().AddSubPlot(1, 1, 1, ax => ax.WordCloud(words, weights)).Save("cloud.svg");
Nothing is dropped silently: when the words do not all fit, every size shrinks toward the smallest together, and a word that still has no
room is announced on ChartDiagnostics. A word is moved toward the text colour until it reads against the background (4.5 to 1, 3 to 1
for large words), each word carries its weight as a title ("price: 42"), and the SVG asks the browser to set each word to the width it was
measured at, so a different font cannot make one run into its neighbour. A weight that is not above zero, an empty word and a repeated
word are refused when the call is made. The table has a row for each word, heaviest first, with its weight and size.
The control room is one screen for an operator. It holds KPI tiles, state timelines, and a shared trend panel.
All of them use one time window that the caller supplies; the library never reads the wall clock. The 1.14 line is
built around this screen. Plt.OpsDashboard() arrived with it, and the releases since then add fixes and
extensions for running the screen live.
The picture is the output of the code below, at 1200×630.
using MatPlotLibNet;
using MatPlotLibNet.Models.Series;
using MatPlotLibNet.Styling;
// The caller supplies the end of the window; the library never reads the clock itself.
var now = DateTime.UtcNow;
double At(int secondsAgo) => now.AddSeconds(-secondsAgo).ToOADate();
double[] clock = [.. Enumerable.Range(0, 300).Select(i => At(300 - i))];
double[] latency = [.. Enumerable.Range(0, 300).Select(i => 180 + (i % 40) * 3.0)];
double[] decideHistory = [4.1, 3.9, 4.0, 3.7, 3.8, 3.6, 3.8];
double[] latencyTail = [240, 251, 262, 270, 288, 301, 312];
StateSegment[] services =
[
new(At(300), At(200), "up", Colors.Green),
new(At(200), At(140), "degraded", Colors.Orange),
new(At(140), At(90), "no data", Colors.Gray),
new(At(90), At(0), "up", Colors.Green),
];
Plt.OpsDashboard()
.WithTitle("Fleet — control room")
.WithWindow(now, TimeSpan.FromMinutes(5))
.WithNormalBand(0, 250) // the band the trend panel is judged against
// A plain reading: no threshold, just the count and what it is out of.
.AddTile(9, t => { t.Label = "Processes"; t.Format = "0"; t.Caption = "of 9 · 25 lanes"; })
// A cost per message, inside its threshold. The caption carries the DENOMINATOR the number
// was measured over — 3.8 µs over what? — and the sparkline shows which way it is moving.
.AddTile(3.8, t =>
{
t.Label = "Relay (µs/msg)";
t.Format = "0.0";
t.Target = 6; // the comparative: a number alone cannot be judged
t.Caption = "threshold 6" + Environment.NewLine + "2 148 msg · 1 s";
t.Trend = decideHistory; // inline Tufte sparkline: no axis, no frame, no ticks
})
// The same tile, breached: the caption states the overshoot and colour appears — ONLY because
// it is out of band, and it comes from the theme's alarm palette, never the series cycle.
.AddTile(312, t =>
{
t.Label = "Bus latency (µs)";
t.Format = "0";
t.Target = 250;
t.Caption = "threshold 250 · +62" + Environment.NewLine + "2 148 msg · 1 s";
t.Trend = latencyTail;
t.AccentColor = Theme.OpsNight.Alarm.Warning;
})
// A loss counter that is SUPPOSED to read zero. A measured zero is a real answer.
.AddTile(0, t => { t.Label = "Sent offline (msg)"; t.Format = "0"; t.Target = 0; t.Caption = "threshold 0"; })
// No signal: the hatch says "no information", and the format's ZERO section prints a dash, so the
// tile never shows a number it did not measure.
.AddTile(0, t =>
{
t.Label = "Price feed";
t.Format = "0.0;-0.0;—";
t.Caption = "no signal";
t.Hatch = HatchPattern.ForwardDiagonal;
})
.AddTimeline(services, l => l.Label = "Bus") // up / degraded / no data / up
.AddTrend(clock, latency, s => s.Label = "latency µs")
.Build()
.WithTheme(Theme.OpsNight)
.Build()
.SaveSvg("control_room.svg");| Property | What it does |
|---|---|
Format |
the number format for the headline. With three sections (positive;negative;zero), an unknown reading prints a dash instead of a zero that was never measured |
Target |
the value the reader judges the headline against. Without one, a reading such as "68" is neither good nor bad |
Caption |
the line or lines under the label: the threshold, and what the number was measured over. Newlines stack the lines; a line wider than the tile wraps at word boundaries |
Trend |
an inline sparkline inside the tile. It has no axis, no frame and no ticks, and it never changes the headline's scale |
Hatch |
marks the tile as having no information. It draws a pattern, never a colour, because a source with no data is different from a broken one |
AccentColor |
sets a colour. Use it only when something is wrong. Take it from Theme.Alarm so it never collides with a series colour |
Condition |
what the reading is in, on two axes. The tile then takes the colour and the pattern by itself, so the mapping is written once instead of at every call site |
A number is not a state. Your application decides what counts as bad — the library holds no opinion about when something is broken — and hands the tile the verdict:
using MatPlotLibNet;
using MatPlotLibNet.Models;
double p99 = 27.4; // your measurement
Plt.OpsDashboard()
.WithWindow(DateTime.UtcNow, TimeSpan.FromMinutes(5))
.AddTile(p99, t =>
{
t.Label = "Request p99";
t.Condition = p99 > 30 ? new OpsCondition(OpsSeverity.Critical)
: p99 > 25 ? new OpsCondition(OpsSeverity.Warning)
: OpsCondition.Resting;
})
.Build()
.Save("p99_tile.svg");The condition answers two separate questions:
| Axis | Values | What it says |
|---|---|---|
OpsSeverity |
Normal, Warning, Critical
|
how bad the reading is. The only axis that compares |
OpsVisibility |
Observed, Unknown, Shelved
|
whether the reading can be believed, and whether someone silenced it |
A silent source is not a degraded one and a muted alarm is not a solved problem, so neither sits on the severity
ladder. Normal wears no colour; Warning and Critical take the theme's alarm colours; Unknown and
Shelved each wear their own pattern, so an operator can tell at a glance which of the two is on the wall. A
timeline band takes the same condition.
OpsCondition.RollUp(children) reads a group the way an operator does: the worst child that can actually be
seen decides the colour, while the unseen and the muted are counted rather than ranked. A shelved child leaves
the colour alone but never leaves the screen, and a parent whose children have all gone quiet is itself unseen.
using MatPlotLibNet;
var now = DateTime.UtcNow;
var deployedAt = now.AddMinutes(-2);
double[] clock = [.. Enumerable.Range(0, 300).Select(i => now.AddSeconds(i - 300).ToOADate())];
double[] latency = [.. Enumerable.Range(0, 300).Select(i => 180 + (i % 40) * 3.0)];
Plt.OpsDashboard()
.WithWindow(now, TimeSpan.FromMinutes(5))
.WithEventMarker(deployedAt, m => m.Label = "deploy 1.4.2")
.AddTile(latency[^1], t => t.Label = "Bus latency (µs)")
.AddTrend(clock, latency, s => s.Label = "latency µs")
.Build()
.Save("deploy_marker.svg");One vertical line on the trend and on every timeline — every panel that has a clock. Set it once, so a reader comparing two panels can trust that the same line means the same instant. The tile row and the topology panel have no clock and never receive it.
A threshold line marks which side counts as a breach, and the zone on that side is shaded up to the edge of the plot. A threshold beyond the data widens the axis so its line and zone are drawn. The legend shows the live reading beside the series name.
using MatPlotLibNet;
using MatPlotLibNet.Models;
using MatPlotLibNet.Styling;
double[] hours = [0, 1, 2, 3, 4, 5, 6, 7];
double[] load = [42, 55, 61, 78, 86, 73, 64, 58];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.Plot(hours, load, s => s.Label = "CPU load %")
.Threshold(80.0, Orientation.Horizontal, ThresholdBreach.Above, color: Colors.Red, label: "Alarm")
.SetXLabel("Hour")
.SetYLabel("Load %")
.WithLegend()
.WithLegendValues()) // the reading beside the name; the order of the two calls does not matter
.Save("load.svg");| Piece | What it is |
|---|---|
StatTileSeries |
the tile above: headline, label, caption, Target, inline Trend, Hatch
|
StateSegment timeline |
one band per state over the window; a gap state means "no information", the same as a hatched tile |
BulletGraphSeries |
Stephen Few's design: a bar with a target and bands. Use it instead of a radial gauge |
Theme.Alarm / AlarmPalette
|
a theme names its Resting / Warning / Critical / Unknown colours |
OpsNight / OpsPanel / OpsWarm / OpsContrast
|
operator backgrounds for a wall display; they are not meant for a report |
See Styling#operator-themes-and-the-alarm-palette for the palette and the themes.
dotnet run --project Samples/MatPlotLibNet.Samples.ControlRoomThis is a separate sample project, because it is a reference implementation of a whole screen rather than an example of one control. It simulates a 15-bus federation with its own domain (a bus holds processes, a process holds lanes, plus alarm conditioning and a staleness clock) and a background simulator that keeps running whether or not a browser is looking.
The screen descends from fleet to bus to process to lanes, and no level is ever replaced by the next one. The level you leave becomes the rail on the left, still coloured, so a sibling is one click away and you never lose sight of what stands next to the thing you are reading. That is the piece three earlier attempts were missing. Each of them drew one level well and then had no way to go further.
There are two gestures, and you need both:
| gesture | what it does |
|---|---|
| click a block | drills down one level in the hierarchy |
| click the max or min in the strip | drills through: it leaves the aggregate and opens the member that produced the value (the exemplar). That is the purpose of making an aggregate clickable |
At the bottom level neither gesture opens anything, because a link that leads nowhere is worse than no link at all.
A block is one size at every level and at every count: a fixed track, never a fraction of the row. Two buses are two blocks with an empty row beside them. Stretching those two blocks across the whole row would make the fleet look wider than it is, so the empty space is part of what the reader sees.
Lanes are drawn as rows rather than cards, because they are the bottom level and they answer a different question. You ask a bus or a process how hard it is working, and load answers that. You ask a lane whether it is keeping up, which load cannot answer at all: a lane that has stopped delivering burns no load. So a lane row carries backlog, latency and errors with a status chip.
The whole state lives in the URL (?bus=, ?process=). It survives every redraw, every block is an anchor so
the descent is keyboard-reachable with no script, and you can paste it to a colleague during an incident.
This package adds 13 map projections, GeoPolygonSeries (which renders any closed ring as a styled polygon, holes included: a map coloured by a value, ocean fill, land fill or a custom shape), and embedded Natural Earth 110m data (coastlines and countries, with no external download).
using MatPlotLibNet;
using MatPlotLibNet.Geo;
using MatPlotLibNet.Geo.Projections;
using MatPlotLibNet.Styling;
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.WithProjection(new Mercator())
.Ocean(new Mercator(), new Color(180, 220, 255))
.Land(new Mercator(), new Color(240, 230, 200))
.Coastlines(new Mercator(), Colors.Black, lineWidth: 0.5)
.Borders(new Mercator(), Colors.Gray, lineWidth: 0.3))
.Save("worldmap.svg");| Family | Projection | Notes |
|---|---|---|
| Cylindrical | PlateCarree |
Equirectangular — the simplest 1:1 lat/lon mapping |
| Cylindrical | Mercator |
Conformal — preserves angles (web maps) |
| Cylindrical | TransverseMercator |
Mercator rotated 90° — UTM zones |
| Pseudo-cylindrical | Mollweide |
Equal-area; world maps |
| Pseudo-cylindrical | Robinson |
Compromise; common in atlases |
| Pseudo-cylindrical | Sinusoidal |
Equal-area; meridian-true |
| Pseudo-cylindrical | EqualEarth |
Modern equal-area (Šavrič et al. 2018) |
| Pseudo-cylindrical | NaturalEarthProjection |
Compromise; aesthetic for global view |
| Conic | AlbersEqualArea |
Equal-area conic — ideal for mid-latitude regions (US, Europe) |
| Conic | LambertConformal |
Conformal conic — aviation charts |
| Azimuthal | Orthographic |
"View from space" — globe rendering |
| Azimuthal | Stereographic |
Conformal azimuthal — polar regions |
| Azimuthal | AzimuthalEquidistant |
Distances from centre preserved |
All projections implement IGeoProjection. Forward(latitude, longitude) returns a ProjectedPoint and Inverse(x, y) returns a GeoCoordinate?, so a coordinate round-trips. Since v1.13.0 both are record structs instead of tuples: readonly record struct ProjectedPoint(double X, double Y) and readonly record struct GeoCoordinate(double Latitude, double Longitude). Call sites that deconstruct them are unaffected. Bounds returns GeoBounds(XMin, XMax, YMin, YMax), which is now a record struct as well and was a 4-tuple.
using MatPlotLibNet;
using MatPlotLibNet.Geo;
using MatPlotLibNet.Geo.Data;
using MatPlotLibNet.Geo.Projections;
using MatPlotLibNet.Geo.Series;
using MatPlotLibNet.Styling;
using MatPlotLibNet.Styling.ColorMaps;
// One series for each country, coloured by hand (Choropleth, below, does this for every country at once).
var countryData = new Dictionary<string, double> { ["France"] = 68, ["Germany"] = 84, ["Spain"] = 48 };
double maxValue = countryData.Values.Max();
var projection = new Robinson();
Plt.Create()
.AddSubPlot(1, 1, 1, ax =>
{
ax.WithProjection(projection);
foreach (var (country, value) in countryData)
{
var poly = NaturalEarth110m.Countries().First(c => c.Properties["NAME"] == country);
ax.AddSeries(new GeoPolygonSeries([poly], projection)
{
Color = ColorMaps.Viridis.GetColor(value / maxValue),
StrokeColor = Colors.White,
StrokeWidth = 0.3,
});
}
})
.Save("choropleth.svg");The package bundles GeoJSON for the Natural Earth 110m cultural and physical datasets. There is no HTTP fetch, no download and no external dependency.
| Member | Contents |
|---|---|
NaturalEarth110m.Coastlines() |
World coastline rings (134 features) |
NaturalEarth110m.Countries() |
World countries with Properties["NAME"], ["ISO_A2"], etc. (177 features) |
using MatPlotLibNet;
using MatPlotLibNet.Geo;
using MatPlotLibNet.Geo.Projections;
using MatPlotLibNet.Styling;
var projection = new EqualEarth();
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.WithProjection(projection) // Hides the axes chrome for a map
.Ocean(projection, new Color(180, 220, 255)) // Fills entire map background
.Land(projection, new Color(240, 230, 200)) // Fills land masses
.Coastlines(projection, Colors.Black, 0.5) // Adds Natural Earth 110m coastlines
.Borders(projection, Colors.Gray, 0.3)) // Adds Natural Earth 110m country borders
.Save("equal_earth.svg");Each of these is built on GeoPolygonSeries, the lowest-level primitive, so you can mix them with your own GeoPolygonSeries calls in the same axes for custom-shape rendering.
A map axes has one projection: WithProjection records it, and every layer reads it. Land(color), Ocean(color), Borders() and Coastlines() need no projection of their own, and are refused with "call WithProjection first" when the axes has none. A layer that is given a projection has to give the axes' (the same kind with the same numbers), or it is refused with an ArgumentException naming both.
using MatPlotLibNet;
using MatPlotLibNet.Geo;
using MatPlotLibNet.Geo.Projections;
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.WithProjection(GeoProjection.Robinson)
.Coastlines()
.Borders())
.Save("world_map.svg");using MatPlotLibNet;
using MatPlotLibNet.Geo;
using MatPlotLibNet.Geo.Projections;
using MatPlotLibNet.Styling.ColorMaps;
// Each country takes the colour its value has in the colour map; the values are joined by the three-letter code.
var populationMillions = new Dictionary<string, double> { ["FRA"] = 68, ["DEU"] = 84, ["ESP"] = 48 };
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax
.WithProjection(GeoProjection.Robinson)
.Choropleth(populationMillions, ColorMaps.Viridis)
.WithColorBar())
.Save("choropleth_values.svg");| Verb | Draws |
|---|---|
Choropleth(values, colorMap), Choropleth("POP_EST", colorMap)
|
each country in the colour its value has in the colour map: values joined by ADM0_A3, or a numeric property of the features (GeoFeature.NumberOf, read invariantly); a country with no value takes the map's bad colour, and WithColorBar() reads the range of the values that can be drawn |
Points(latitudes, longitudes) |
a scatter of places |
Bubbles(latitudes, longitudes, values) |
a bubble at each place, its area in proportion to its value, the largest 400 square points |
HexBins(latitudes, longitudes) |
hexagon bins in the plane of the map |
Flows(flows, color, maxWidth) |
a line along the great circle for each GeoFlow(From, To, Value), as wide as its quantity, cut where it crosses the antimeridian |
Cartogram(values) |
each country resized so that its area is in proportion to its value, about its own centre (a non-contiguous cartogram) |
Dorling(values) |
a bubble at the centre of each country, its area in proportion to its value |
A place the projection cannot place, such as the far side of an orthographic globe, is left out. The layers are shown with pictures on the examples page.
A map writes its projection (its kind and the numbers it was made with, by their PROJ names lon_0, lat_0, lat_1, lat_2), its features (the id of the built-in Natural Earth set, or a GeoJSON FeatureCollection with the holes kept, written by GeoJsonWriter), and its values, colour map and normaliser, into the package data of the series, and a figure that holds one comes back from JSON as the picture it was. The package registers the reader when it loads; a process that reads the JSON before it has used any type of MatPlotLibNet.Geo calls GeoPolygonSeries.RegisterReader() once. The MCP server does not list maps.
MatPlotLibNet 1.29.0
- Home
- Getting Started
- Fluent Cheatsheet
- Examples — each release in code
-
Package Map
- MatPlotLibNet — the core package
- Chart Types
- Styling
- Advanced
- Streaming
- Bidirectional SignalR
- Interactive Controls
- MCP Server
- Verso
- Notebooks
- Accessibility
- International Text
- Benchmarks
- Roadmap
- Contributing












