Table of Contents

matplotlib in C#

Install: dotnet add package MatPlotLibNet — add MatPlotLibNet.Skia as well for PNG, PDF and GIF.

If you know matplotlib and you are now writing C#, you do not need to learn a new charting library from scratch. This library was built around matplotlib's own model — a figure holds axes, an axes holds series, a series carries its own style — so most of what you know transfers name for name. This page is the lookup table: the pyplot call on the left, the line that draws the same chart on the right.

Two differences are worth reading before the tables, because they explain almost every row.

There is no global current figure. In pyplot, plt.plot(...) draws on whichever figure is current, and plt.gca() hands you the axes behind it. Here you always hold the object you are drawing on, so there is nothing to get and nothing to close. That removes plt.gca, plt.gcf, plt.cla, plt.clf, plt.close and plt.hold from the table: none of them has an equivalent, because none of them has anything to do.

Calls chain. Every verb returns the thing you called it on, so a chart is one expression instead of a paragraph of statements. That is why the C# column reads ax.Plot(...).WithTitle(...) where Python reads two lines.

The same chart, twice

# Python
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 6))
ax.plot(x, y, color='steelblue', linewidth=2, label='Signal')
ax.set_xlabel('Time (s)')
ax.set_ylabel('Amplitude')
ax.set_title('Measurement')
ax.legend()
ax.grid(True)
fig.savefig('chart.png', dpi=150)
// C#
using MatPlotLibNet;
using MatPlotLibNet.Styling;

Plt.Create()
    .WithSize(800, 600)   // in pixels at 96 dpi
    .WithDpi(150)         // the PNG is drawn at 150 dpi: 1250 x 938 pixels, the same layout drawn finer
    .AddSubPlot(1, 1, 1, ax => ax
        .Plot(x, y, s => { s.Color = Colors.SteelBlue; s.LineWidth = 2; s.Label = "Signal"; })
        .SetXLabel("Time (s)")
        .SetYLabel("Amplitude")
        .WithTitle("Measurement")
        .WithLegend()
        .WithGrid(g => g with { Visible = true }))
    .Save("chart.png");

Figures and axes

matplotlib MatPlotLibNet Notes
plt.figure() Plt.Create() Starts a figure. .Build() hands you the Figure; .Save(…) finishes it in one go.
plt.subplots() figure.AddSubPlot(1, 1, 1, ax => …) The axes is handed to the lambda instead of returned.
plt.subplots(2, 3) figure.AddSubPlot(2, 3, index, ax => …) Called once per panel; index runs 1…6, row by row, as in matplotlib.
plt.figure(figsize=(8, 6)) figure.WithSize(800, 600) Pixels at 96 dpi, not inches.
plt.figure(dpi=150) figure.WithDpi(150) A PNG, and each frame of a GIF, is drawn with dpi / 96 pixels per unit: 800 × 600 at 150 dpi is a 1250 × 938 PNG with the same layout, text and line widths. An SVG, a PDF page and JSON have no pixels, so the resolution changes none of them.
fig.savefig('c.png', dpi=300) figure.Build().Save("c.png", new SaveOptions { Dpi = 300 }) The resolution for this one save. SaveOptions also carries an SVG's indentation and decimals, and a title and author that go into an SVG, a PNG and a PDF.
fig.suptitle('…') figure.WithTitle("…") The figure's own title, above the panels.
plt.tight_layout() figure.TightLayout()
plt.subplots(constrained_layout=True) figure.ConstrainedLayout()
gridspec.GridSpec(2, 2, height_ratios=…) figure.WithGridSpec(2, 2, heightRatios) Ratios are arrays, same meaning.
plt.subplots_adjust(hspace=…) figure.WithSubPlotSpacing(sp => sp with { VerticalGap = … })
plt.subplot_mosaic('AB;CC') Plt.Mosaic("AB;CC") Same string layout language.
fig.add_axes([…]) ax.AddInset(bounds, inset => …) An inset belongs to the axes it sits in.
plt.savefig('c.png') figure.Save("c.png") The extension picks the format: .svg, .png, .pdf, .gif.
fig.canvas.tostring_rgb() figure.ToSvg() The markup as a string, for a web response.
plt.show() — Nothing renders to a screen by itself; see the Playground or the Blazor, WPF, MAUI, Avalonia and Uno controls.
plt.gca(), plt.gcf() — There is no current figure to get.
plt.close(), plt.clf() — Nothing is held open.

The plots

matplotlib MatPlotLibNet Notes
ax.plot(x, y) ax.Plot(x, y)
ax.scatter(x, y) ax.Scatter(x, y)
ax.scatter(x, y, c=density) ax.DensityScatter(x, y) Counts the points per cell and colours by it; no density array to compute.
ax.bar(labels, heights) ax.Bar(labels, heights)
ax.bar with an offset per group ax.GroupedBar(categories, groups) The offset arithmetic every grouped bar chart repeats is done for you.
ax.barh(labels, widths) ax.Bar(labels, values, s => s.Orientation = BarOrientation.Horizontal) One series flag rather than a second verb.
ax.step(x, y) ax.Step(x, y)
ax.stem(x, y) ax.Stem(x, y)
ax.stackplot(x, ys) ax.StackPlot(x, ySets)
ax.fill_between(x, y1, y2) ax.FillBetween(x, y, y2)
ax.errorbar(x, y, yerr=e) ax.ErrorBar(x, y, errorLow, errorHigh)
ax.pie(sizes, labels=…) ax.Pie(sizes, labels)
donut via wedgeprops ax.Donut(sizes, labels)
ax.eventplot(positions) ax.Eventplot(positions)
ax.table(cellText=…) ax.Table(cells)
— ax.Sankey(nodes, links)
squarify.plot ax.Treemap(root) matplotlib has no treemap; this one is built in.
nested ax.pie ax.Sunburst(root)

Charts matplotlib draws from a recipe

matplotlib has no single call for these charts: they are several calls and some arithmetic, or another package. Here each is one call that adds the series the chart is made of, so it saves, exports and reads back from JSON like any other chart. The bar charts, distribution, line charts and heatmaps pages show each one with its picture.

matplotlib / Python MatPlotLibNet Notes
ax.spy(matrix) ax.Spy(matrix) Square markers on the cells whose magnitude is above a threshold, row 0 on top.
scipy.stats.probplot(x, plot=ax) ax.QQPlot(sample) scipy's plotting positions and least-squares reference line; normal distribution only.
ax.stackplot(x, ys, baseline='wiggle') ax.StreamGraph(x, layers) One area per layer, each named in the legend.
pandas.plotting.parallel_coordinates(df, 'class') ax.ParallelCoordinates(dimensions, rows) Each column scaled to 0..1; rows that share a name are one class.
lifelines.KaplanMeierFitter().fit(t, e).plot_survival_function() ax.KaplanMeier(groups) The product-limit curve only, no band or censor marks.
joypy.joyplot(df) ax.Ridgeline(groups) One shared grid and bandwidth, so the ridges compare.
sorted ax.bar plus a twinx cumulative line ax.Pareto(categories, counts) Sorts, accumulates to exactly 100 % and aligns both axes.
ax.stem over category positions ax.Lollipop(categories, values) One stem series: one colour, one legend entry.
ax.vlines plus two ax.scatter ax.Dumbbell(categories, from, to) The connector spans the two dots whichever is higher.
one ax.plot of two points per category ax.SlopeGraph(categories, from, to) The names at both ends, no legend.
ax.scatter per group over category rows ax.DotPlot(categories, groups) Sorted by the first group, largest on top.
ax.plot per group with the Y axis inverted ax.BumpChart(x, ranks) Rank 1 on top, the names at the line ends.
ax.plot plus ax.axhline for the limits ax.ControlChart(x, values) Limits estimated from the average moving range; values beyond them marked.
calplot / july ax.CalendarHeatmap(dates, values) Seven weekday rows by one column per week, months named.
ax.bar with widths and bottoms per segment ax.Marimekko(columns, segments) Column widths and cell heights as shares.

Labels, limits, scales and the frame

matplotlib MatPlotLibNet Notes
ax.set_xlabel('…') ax.SetXLabel("…")
ax.set_ylabel('…') ax.SetYLabel("…")
ax.set_title('…') ax.WithTitle("…")
ax.set_xlim(0, 10) ax.SetXLim(0, 10)
ax.set_ylim(0, 10) ax.SetYLim(0, 10)
ax.set_xscale('log') ax.SetXScale(AxisScale.Log) Linear, Log, SymLog and Logit, as in matplotlib.
ax.set_yscale('log') ax.SetYScale(AxisScale.Log)
ax.legend() ax.WithLegend()
ax.legend(loc='upper left') ax.WithLegend(LegendPosition.UpperLeft)
ax.text(x[-1], y[-1], 'Web', color=line.get_color()) ax.WithDirectLabel() Names the series added last at the end of its line, in its colour, instead of in the legend. See direct labels.
ax.grid(True) ax.WithGrid(g => g with { Visible = true }) The grid is a value you change, so one call sets colour, style and which axis at once.
ax.minorticks_on() ax.WithMinorTicks()
ax.margins(x=0.1) ax.SetXMargin(0.1)
ax.margins(0) ax.WithTightMargins()
ax.spines['top'].set_visible(False) ax.HideTopSpine() HideRightSpine() for the other one.
ax.tick_params(axis='y', colors='C1') and ax.spines['left'].set_color('C1') ax.SetYAxisColor(Colors.Tab10Orange) One call colours the spine, the tick marks, the tick labels and the title. SetXAxisColor for X; the secondary-axis builders have the same verbs.
ax.axhline(y=0) ax.AxHLine(0)
ax.axvline(x=0) ax.AxVLine(0)
ax.axhspan(1, 2) ax.AxHSpan(1, 2)
ax.axvspan(1, 2) ax.AxVSpan(1, 2)
ax.annotate('…', xy=…) ax.Annotate("…", x, y) Arrows, callout boxes and axes-fraction placement are all on the same call.
ax.twinx() ax.WithSecondaryYAxis(right => …)
ax.sharex(other) ax.ShareX("time")
plt.xticks(rotation=45) ax.WithXTickLabelRotation(45)
ax.xaxis.set_major_formatter(f) ax.SetXTickFormatter(formatter)
ax.xaxis.set_major_locator(l) ax.SetXTickLocator(locator)
mdates.DateFormatter('%H:%M') ax.SetXDateFormat("HH:mm") .NET format strings, not strftime.

Colour and style

matplotlib MatPlotLibNet Notes
color='steelblue' Colors.SteelBlue All 148 CSS4 names are there.
color='#1f77b4' Color.FromHex("#1f77b4")
color='red' Color.FromName("red")
linestyle='--' LineStyle.Dashed
marker='o' MarkerStyle.Circle
cmap='viridis' ColorMaps.Viridis 148 colormaps, matplotlib's own included.
cmap='coolwarm' ColorMaps.Coolwarm
plt.colormaps() ColorMaps.All
plt.style.use('dark_background') figure.WithTheme(Theme.Dark) 30 themes; Theme.MatplotlibV2 is matplotlib's own default palette.
plt.style.use('ggplot') figure.WithTheme(Theme.Ggplot)
plt.style.use('seaborn') figure.WithTheme(Theme.Seaborn)
mpl.rcParams[…] = … Theme.CreateFrom(Theme.Default) Build a theme from an existing one instead of mutating a global dictionary.
fig.colorbar(im) ax.WithColorBar()

Distributions and statistics

matplotlib / seaborn MatPlotLibNet Notes
ax.hist(data, bins=20) ax.Hist(data, 20)
ax.hist2d(x, y) ax.Histogram2D(x, y)
ax.boxplot(data) ax.BoxPlot(data)
ax.violinplot(data) ax.Violin(data)
ax.hexbin(x, y) ax.Hexbin(x, y)
sns.kdeplot(data) ax.Kde(data)
sns.rugplot(data) ax.Rugplot(data)
sns.stripplot(…) ax.Stripplot(datasets)
sns.swarmplot(…) ax.Swarmplot(datasets)
sns.countplot(…) ax.Countplot(values)
sns.pointplot(…) ax.Pointplot(datasets)
ax.ecdf(data) ax.Ecdf(data)
sns.heatmap(matrix) ax.Heatmap(matrix)
sns.clustermap(matrix) ax.Clustermap(matrix)
sns.pairplot(frame) ax.PairGrid(variables)
scipy.cluster.hierarchy.dendrogram ax.Dendrogram(root)

Images, grids and vector fields

matplotlib MatPlotLibNet Notes
ax.imshow(matrix) ax.Image(matrix)
ax.pcolormesh(x, y, z) ax.Pcolormesh(xEdges, yEdges, values)
ax.contour(x, y, z) ax.Contour(x, y, z)
ax.contourf(x, y, z) ax.Contourf(x, y, z)
ax.quiver(x, y, u, v) ax.Quiver(x, y, u, v)
ax.quiverkey(q, …) ax.QuiverKey(x, y, u, "5 m/s")
ax.streamplot(x, y, u, v) ax.Streamplot(x, y, u, v)
ax.barbs(x, y, u, v) ax.Barbs(x, y, u, v)
ax.tricontour(…) ax.Tricontour(x, y, z)
ax.tripcolor(…) ax.Tripcolor(x, y, z)
ax.specgram(signal) ax.Spectrogram(signal)

Three dimensions

matplotlib MatPlotLibNet Notes
fig.add_subplot(projection='3d') ax.WithProjection(elevation, azimuth) The projection is a property of the axes, set where you draw.
ax.plot_surface(x, y, z) ax.Surface(x, y, z)
ax.plot_wireframe(x, y, z) ax.Wireframe(x, y, z)
ax.plot_trisurf(x, y, z) ax.Trisurf(x, y, z)
ax.scatter(x, y, z) ax.Scatter3D(x, y, z)
ax.plot(x, y, z) ax.Plot3D(x, y, z)
ax.contour3D(x, y, z) ax.Contour3D(x, y, z)
ax.bar3d(…) ax.Bar3D(x, y, z)
ax.voxels(filled) ax.Voxels(filled)
ax.text(x, y, z, '…') ax.Text3D(x, y, z, "…")
ax.view_init(elev, azim) ax.WithCamera(30, -60)

Polar

matplotlib MatPlotLibNet Notes
plt.subplot(projection='polar') + plot ax.PolarPlot(r, theta) The polar frame comes with the series. The radius comes first; the angle is in radians, counter-clockwise from the right, as in matplotlib.
polar scatter ax.PolarScatter(r, theta)
polar bar ax.PolarBar(r, theta)
radar chart, by hand ax.Radar(labels, values) matplotlib has no radar verb; this one is built in.

Where to go next