# Styling --- ## Themes MatPlotLibNet ships with built-in themes that mirror matplotlib style sheets. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; Plt.Create().WithTheme(Theme.Default).Plot(x, y).Save("default.svg"); Plt.Create().WithTheme(Theme.Dark).Plot(x, y).Save("dark.svg"); Plt.Create().WithTheme(Theme.Solarize).Plot(x, y).Save("solarized.svg"); ``` ### Theme.ColorBlindSafe This theme uses the Okabe-Ito 8-color palette, which is colorblind-safe for deuteranopia, protanopia, and tritanopia. The background is white and the text is black. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [12, 15, 14, 18, 21]; Plt.Create() .WithTheme(Theme.ColorBlindSafe) .Plot(x, y, s => s.Label = "Revenue") .Save("accessible.svg"); ``` The same colors are also available as a colormap (`"okabe_ito"` / `"okabe_ito_r"`): ```csharp using MatPlotLibNet.Styling.ColorMaps; var map = ColorMaps.Get("okabe_ito"); ``` ### Theme.MatplotlibClassic and Theme.MatplotlibV2 These two themes reproduce the matplotlib look in pure .NET; no Python runtime is required. `Theme.MatplotlibClassic` mimics matplotlib's pre-2.0 look (white background, `bgrcmyk` 7-color cycle, DejaVu Sans 12pt). `Theme.MatplotlibV2` mimics the modern default (white background, soft-black `#262626` text, `tab10` 10-color cycle, DejaVu Sans 10pt). Both ship with the grid hidden by default, matching matplotlib's `plt.grid(False)`. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; double[] y2 = [1, 3, 2, 4, 3]; Plt.Create() .WithTheme(Theme.MatplotlibV2) .AddSubPlot(1, 1, 1, ax => ax .Plot(x, y, s => s.Label = "Series 1") .Plot(x, y2, s => s.Label = "Series 2") .WithLegend()) .Save("matplotlib_v2.svg"); ``` See [[MatplotlibThemes]] for the full color cycles, comparison table, and implementation notes. ### Theme.HighContrast This theme targets WCAG AAA: white and black give a 21:1 contrast ratio. It uses a bold 13pt font, a thick 1.5px dark grid, and an 8-color high-chroma cycle. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; Plt.Create() .WithTheme(Theme.HighContrast) .Plot(x, y) .Save("high-contrast.svg"); ``` ### Custom Theme ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; var theme = Theme.CreateFrom(Theme.Dark) .WithBackground(Color.FromHex("#1a1a2e")) .WithFont(f => f with { Family = "Consolas", Size = 14 }) .Build(); Plt.Create().WithTheme(theme).Plot(x, y).Save("custom.svg"); ``` A figure's theme travels with it: `ToJson` writes it as data, property by property, and `FromJson`, the MCP server and every transport read it back, so a theme made with `ThemeBuilder` comes back whole too. A figure on the default theme writes no theme at all. --- ### Operator themes and the alarm palette Four themes have backgrounds designed for a wall display rather than a report: `Theme.OpsNight`, `OpsPanel`, `OpsWarm`, `OpsContrast`. Every theme also defines its own alarm colours, so a state mark never takes a colour from the series cycle: ```csharp using MatPlotLibNet.Styling; var theme = Theme.OpsNight; Color resting = theme.Alarm.Resting; // nothing wrong — the neutral shade Color warning = theme.Alarm.Warning; // out of band Color critical = theme.Alarm.Critical; // acted on now Color unknown = theme.Alarm.Unknown; // no information (a hatch, never a colour) var custom = Theme.CreateFrom(Theme.Dark) .WithAlarmPalette(new AlarmPalette(Colors.Gray, Colors.Orange, Colors.Crimson, Colors.DarkGray)) .Build(); ``` In these themes, colour is reserved for states that need attention. See [[Chart Types#control-room]] for the tiles that use the alarm palette. --- ## PropCycler ![PropCycler — four series cycling color and line style](images/prop_cycler.png) A `PropCycler` cycles Color, LineStyle, MarkerStyle, and LineWidth at the same time across series. Each property cycles at its own length, and the series index wraps around at the least common multiple (LCM) of those lengths. When `PropCycler` is null, the theme's `CycleColors[]` is used instead. All four are drawn. A series that leaves its line style, width or marker at what a new series holds (solid, 1.5 points, no marker; a scatter holds a circle) takes the cycler's entry for its place; a series that sets another value keeps its own, as a series with a colour of its own does. A value equal to the default cannot be told from one left alone, so under a cycler of dashes a series cannot be forced back to solid by setting solid. The entries reach lines, steps, scatters, the outline of an area, error-bar widths, density curves, ECDFs, regression, signal, polar and 3-D lines, both secondary axes and every legend swatch. A cycler that lists only colours changes nothing else, and no built-in theme has a cycler. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [0, 1, 2, 3, 4]; double[][] y = [ [1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7], [4, 5, 6, 7, 8], ]; var cycler = new PropCyclerBuilder() .WithColors(Colors.Blue, Colors.Orange, Colors.Green, Colors.Red) .WithLineStyles(LineStyle.Solid, LineStyle.Dashed) .WithMarkerStyles(MarkerStyle.Circle, MarkerStyle.Square) .WithLineWidths(2.0, 1.5) .Build(); // A cycler travels in a theme: embed it once and reuse the theme across figures var theme = Theme.CreateFrom(Theme.Dark) .WithPropCycler(cycler) .Build(); Plt.Create() .WithTheme(theme) .AddSubPlot(1, 1, 1, ax => { for (int i = 0; i < 4; i++) ax.Plot(x, y[i], s => s.Label = $"Series {i + 1}"); ax.WithLegend(); }) .Save("cycler.svg"); ``` --- ## Color Maps MatPlotLibNet includes 74 base maps. Each one is registered automatically with a reversed `_r` variant, which makes 148 colormaps in six families: ![Colormap comparison](images/colormap_comparison.png) | Family | Examples | |---|---| | Sequential | `viridis`, `plasma`, `inferno`, `magma`, `cividis`, `blues`, `greens`, `gray`, `hot`, `spring`, `summer`, `autumn`, `winter`, `cool`, `afmhot` | | Diverging | `rdbu`, `rdylgn`, `bwr`, `seismic`, `coolwarm`, `prgn`, `rdgy` | | Qualitative | `tab10`, `tab20`, `set1`, `set2`, `set3`, `paired`, `dark2`, `accent` | | Cyclic | `hsv`, `twilight`, `twilight_shifted` | | Accessible | `okabe_ito`, `petroff6`, `petroff8`, `petroff10` | | Misc | `rainbow`, `ocean`, `terrain`, `cmrmap`, `jet`, `turbo` | The accessible family is for a chart that has to be read by everyone. `okabe_ito` is the eight-colour palette from Okabe and Ito; the three Petroff sequences come from *Accessible Color Sequences for Data Visualization* and pick colours so that every pair inside a sequence stays apart under the common forms of colour blindness, and in grayscale as well. Pick the one whose number matches how many things you are drawing. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; using MatPlotLibNet.Styling.ColorMaps; double[,] data = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; // Apply to a heatmap Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Heatmap(data) .WithColorMap("plasma") .WithColorBar(cb => cb with { Label = "Intensity", Extend = ColorBarExtend.Both })) .Save("heatmap.svg"); // Registry lookup — case-insensitive, append _r for reversed variant var map = ColorMapRegistry.Get("rdylgn"); var reversed = ColorMapRegistry.Get("viridis_r"); // Custom gradient with positioned stops var custom = LinearColorMap.FromPositions("my_map", [ new ColorStop(0.0, Color.FromHex("#000080")), new ColorStop(0.3, Color.FromHex("#FFFFFF")), new ColorStop(1.0, Color.FromHex("#800000")), ]); // Discrete categorical colormap (no interpolation) var discrete = new ListedColorMap("my_cat", [Colors.Blue, Colors.Red, Colors.Green]); ``` A map that ships goes into JSON by its name. A map built in code, like the two above, goes in as what it is made of: its colours, their positions, and its under, over and bad colours, so it comes back from `FromJson`, the MCP server and every transport as itself. A map of a type you write yourself is code; it travels only by a name registered with `ColorMapRegistry.Register`, which the reader has to register too. ### Colormappable Series These are all the series that support `IColorMap` (via `IColormappable`): `HeatmapSeries`, `ImageSeries`, `Histogram2DSeries`, `ContourSeries`, `ContourfSeries`, `SurfaceSeries`, `ScatterSeries`, `PcolormeshSeries`, `HexbinSeries`, `SpectrogramSeries`, `TripcolorSeries`, `TricontourSeries`, `HierarchicalSeries`. `ScatterSeries` supports per-point colormap coloring via the `C` scalar array: ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling.ColorMaps; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; double[] values = [5, 30, 55, 80, 100]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.Scatter(x, y, s => { s.C = values; // scalar per point s.ColorMap = ColorMaps.Viridis; s.VMin = 0; s.VMax = 100; s.Normalizer = new PowerNormNormalizer(gamma: 0.5); })) .Save("scatter_colormap.svg"); ``` ### Normalizers ```csharp using MatPlotLibNet.Styling.ColorMaps; INormalizer[] normalizers = [ LinearNormalizer.Instance, // (v - min) / (max - min) — default new LogNormalizer(), // log scale mapping new TwoSlopeNormalizer(center: 0.0), // diverging, center at value new BoundaryNormalizer([0.0, 10.0, 50.0, 100.0]), // discrete boundaries new SymLogNormalizer(linthresh: 1.0), // symmetric log — linear near zero, log beyond new PowerNormNormalizer(gamma: 0.5), // power-law: gamma < 1 expands low values new CenteredNormNormalizer(vcenter: 0), // maps chosen center to 0.5 NoNormNormalizer.Instance, // pass-through — data already in [0, 1] ]; ``` ### Extreme Values (Under / Over / Bad) ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; using MatPlotLibNet.Styling.ColorMaps; double[,] data = { { -5, 2, 3 }, { 4, double.NaN, 6 }, { 7, 8, 15 } }; Color[] stops = [Color.FromHex("#000080"), Color.FromHex("#FFFFFF"), Color.FromHex("#800000")]; var cm = new LinearColorMap("custom", stops) { UnderColor = Color.FromHex("#222222"), // values below VMin OverColor = Color.FromHex("#FFFFFF"), // values above VMax BadColor = Color.FromHex("#FF00FF"), // NaN / masked values }; // ColorBar extension slots show under/over colors Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Heatmap(data) .WithColorMap(cm) .WithColorBar(cb => cb with { Extend = ColorBarExtend.Both })) .Save("extremes.svg"); ``` The missing cell (NaN) is drawn in the bad colour, as in matplotlib; a map without a `BadColor` leaves it transparent. --- ## Axis Formatting ### Log Scale ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; using MatPlotLibNet.Rendering.TickFormatters; double[] x = [1, 2, 3, 4, 5]; double[] y = [1, 10, 100, 1_000, 10_000]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .SetYScale(AxisScale.Log) .SetYTickFormatter(new LogTickFormatter()) .Plot(x, y)) .Save("logscale.svg"); ``` A log axis has no place for 0, so a mark that stands on 0 (a bar, a histogram bin, a filled area, a stacked area, a box or a violin) runs from the bottom edge of the plot, as matplotlib's log scale clips it (`nonpositive='clip'`), and the axis starts a margin below the lowest value such marks reach, so the shortest one shows. A point at 0 or below is left out, and a line breaks there. ### Date Axis (manual override) ```csharp using MatPlotLibNet; using MatPlotLibNet.Rendering.TickFormatters; using MatPlotLibNet.Rendering.TickLocators; DateTime[] dates = [.. Enumerable.Range(0, 30).Select(day => DateTime.Today.AddDays(day))]; double[] y = [.. Enumerable.Range(0, 30).Select(day => (double)(day % 7))]; var locator = new AutoDateLocator(); Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Plot(dates, y) .SetXTickLocator(locator) .SetXTickFormatter(new AutoDateFormatter(locator))) .Save("date_axis.svg"); ``` See [[Advanced#date-axes]] for the automatic date axis workflow. --- ## Spines ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; double[] x = [-2, -1, 0, 1, 2]; double[] y = [-4, -1, 0, 1, 4]; Plt.Create() .AddSubPlot(1, 2, 1, ax => ax .Plot(x, y) // Hide top and right (publication style) .HideTopSpine().HideRightSpine()) .AddSubPlot(1, 2, 2, ax => ax .Plot(x, y) // Move x-axis to y = 0 .WithSpines(s => s with { Bottom = s.Bottom with { Position = SpinePosition.Data, PositionValue = 0 } })) .Save("spines.svg"); ``` --- ## Grid Lines Grid lines are **on by default** with all built-in themes, matching matplotlib's default: solid `#B0B0B0` lines at width 0.8. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; Plt.Create() // Default — major grid on both axes .AddSubPlot(2, 3, 1, ax => ax.Plot(x, y)) // Customize color and style .AddSubPlot(2, 3, 2, ax => ax.Plot(x, y).WithGrid(g => g with { Color = Color.FromHex("#444444"), LineStyle = LineStyle.Dashed, LineWidth = 0.5 })) // Only X-axis grid lines .AddSubPlot(2, 3, 3, ax => ax.Plot(x, y).WithGrid(g => g with { Axis = GridAxis.X })) // Add minor grid lines .AddSubPlot(2, 3, 4, ax => ax.Plot(x, y).WithGrid(g => g with { Which = GridWhich.Both })) // Turn off entirely .AddSubPlot(2, 3, 5, ax => ax.Plot(x, y).ShowGrid(false)) .Save("grids.svg"); ``` `GridWhich` controls tick level: `Major` (default), `Minor`, `Both`. `GridAxis` controls which axis: `X`, `Y`, `Both` (default). --- ## Chrome Configuration ### TextStyle `TextStyle` is a nullable partial font override. Apply it to a theme `Font` via `ApplyTo(Font)`: ```csharp using MatPlotLibNet.Styling; var style = new TextStyle { FontSize = 18, FontWeight = FontWeight.Bold, Color = Colors.Navy }; var font = style.ApplyTo(Theme.Default.DefaultFont); ``` ### Title and Axis Label Styling ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4]; double[] y = [12, 15, 14, 18]; var figure = Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Plot(x, y) .WithTitle("Revenue 2026", ts => ts with { FontSize = 22, Color = Css4Colors.DarkBlue }) .SetXLabel("Quarter", ts => ts with { FontSize = 14 }) .SetYLabel("USD (M)", ts => ts with { FontSize = 14, FontSlant = FontSlant.Italic })) .Build(); // Title alignment, set on the built axes figure.SubPlots[0].TitleLoc = TitleLocation.Left; // Left / Center (default) / Right figure.Save("titles.svg"); ``` ### Legend Configuration ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; double[] y2 = [1, 3, 2, 4, 3]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Plot(x, y, s => s.Label = "North") .Plot(x, y2, s => s.Label = "South") .WithLegend(l => l with { NCols = 2, Title = "Series", FontSize = 11, FrameOn = true, FrameAlpha = 0.9, FancyBox = true, Shadow = true, EdgeColor = Colors.Gray, MarkerScale = 1.2, Position = LegendPosition.LowerRight })) .Save("legend.svg"); ``` ### Tick Customization ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; var figure = Plt.Create().Plot(x, y).Build(); var ax = figure.SubPlots[0]; ax.XAxis.MajorTicks = ax.XAxis.MajorTicks with { Direction = TickDirection.In, // In / Out (default) / InOut Length = 8, Width = 1.2, Color = Colors.DarkGray, LabelSize = 11, LabelColor = Css4Colors.DimGray, Pad = 5 }; figure.Save("ticks.svg"); ``` A right-hand or top axis takes the same settings through `figure.SubPlots[0].SecondaryYAxis!.MajorTicks` and `SecondaryXAxis!.MajorTicks`; the [examples page](Examples#1190) shows one with its ticks turned inward. ### Spine Styling ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4, 5]; double[] y = [2, 4, 3, 5, 4]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Plot(x, y) .WithSpines(s => s with { Top = s.Top with { Visible = false }, Right = s.Right with { Visible = false }, Left = s.Left with { Color = Colors.Navy, LineStyle = LineStyle.Solid }, Bottom = s.Bottom with { LineWidth = 1.5 } })) .Save("spine_styling.svg"); ``` ### Axis Colour `SetYAxisColor` and `SetXAxisColor` colour an axis' spine, tick marks, tick labels and title in one call, so an axis reads as belonging to the line drawn in that colour. They exist on the axes builder (left and bottom axis) and on `WithSecondaryYAxis` / `WithSecondaryXAxis` (right and top axis); everything else on the axis is kept, and the colour survives `ToJson` / `FromJson`. Keep an axis title that names the series and its unit, so the colour is never the only thing that tells a reader which axis a line belongs to. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] hour = [0, 6, 12, 18, 24]; double[] temperature = [12, 11, 21, 17, 13]; double[] humidity = [88, 90, 52, 63, 81]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Plot(hour, temperature, s => s.Color = Colors.Crimson) .SetYLabel("Temperature (°C)") .SetYAxisColor(Colors.Crimson) .WithSecondaryYAxis(right => right .Plot(hour, humidity, s => s.Color = Colors.SteelBlue) .SetYLabel("Relative humidity (%)") .SetYAxisColor(Colors.SteelBlue))) .Save("axis_color.svg"); ``` The result, with both lines named at their ends, is pictured on the [examples page](Examples#1190). ### ColorBar Orientation and Shrink ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[,] data = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Heatmap(data) .WithColorBar(cb => cb with { Orientation = ColorBarOrientation.Horizontal, Shrink = 0.8, DrawEdges = true, Label = "Intensity" })) .Save("colorbar_horizontal.svg"); ``` --- ## Hatch Patterns Fill a bar, area, or pie slice with a hatch pattern instead of (or on top of) a solid fill. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; string[] categories = ["A", "B", "C", "D"]; double[] values = [4, 7, 3, 5]; double[] x = [0, 1, 2, 3, 4]; double[] y1 = [1, 3, 2, 4, 3]; double[] y2 = [0, 1, 0.5, 1.5, 1]; Plt.Create() // Bar series with cross hatch .AddSubPlot(1, 3, 1, ax => ax.Bar(categories, values, s => { s.Color = Colors.SteelBlue; s.Hatch = HatchPattern.Cross; s.HatchColor = Colors.White; })) // Area series with forward-diagonal lines .AddSubPlot(1, 3, 2, ax => ax.FillBetween(x, y1, y2, s => { s.Hatch = HatchPattern.ForwardDiagonal; s.HatchColor = Colors.DarkGray; })) // Pie with per-slice hatches .AddSubPlot(1, 3, 3, ax => ax.Pie(values, categories, s => { s.Hatches = [HatchPattern.Horizontal, HatchPattern.Vertical, HatchPattern.DiagonalCross, HatchPattern.Dots]; })) .Save("hatches.svg"); ``` **Available patterns:** `None`, `ForwardDiagonal` (/), `BackDiagonal` (\\), `Horizontal` (-), `Vertical` (|), `Cross` (+), `DiagonalCross` (x), `Dots` (.), `Stars` (*). Series with hatch support: `BarSeries`, `HistogramSeries`, `AreaSeries`, `StackedAreaSeries`, `PieSeries`, `ContourfSeries`. --- ## StackedBaseline Control how the zero baseline is positioned in a stacked area chart. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [0, 1, 2, 3, 4]; double[][] ySets = [ [1, 2, 3, 2, 1], [2, 1, 2, 3, 2], [1, 3, 1, 2, 3], ]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.StackPlot(x, ySets, s => { s.Baseline = StackedBaseline.Wiggle; // matplotlib's baseline='wiggle' })) .Save("streamgraph.svg"); ``` | Value | Description | |---|---| | `Zero` (default) | Stack grows upward from y=0 | | `Symmetric` | Stack centered at y=0 (ThemeRiver) | | `Wiggle` | matplotlib's `wiggle`: the middles of the layers stay as near to zero as the stack allows, the lower layers weighing more | | `WeightedWiggle` | matplotlib's `weighted_wiggle`, the streamgraph layout: also follows each layer's slope, weighted by its size | Both wiggles are matplotlib's own formulas. A missing value (NaN) leaves a gap at its own point only. `StreamGraph(x, layers, baseline)` on the axes builder draws each layer as an area of its own, with its own legend entry; a `StackPlot` layer's name does not reach the legend. --- ## AreaSeries Enhancements ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [0, 1, 2, 3, 4]; double[] y1 = [1, 3, 2, 4, 3]; double[] y2 = [0, 1, 0.5, 1.5, 1]; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.FillBetween(x, y1, y2, s => { s.EdgeColor = Css4Colors.DarkBlue; // separate stroke for boundary lines s.StepMode = DrawStyle.StepsPre; // step interpolation (Pre/Mid/Post) })) .Save("area_steps.svg"); ``` `StepMode` uses the same `DrawStyle` enum as `LineSeries`: `Default` (smooth), `StepsPre`, `StepsMid`, `StepsPost`. --- ## SurfaceSeries Enhancements ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[] x = [.. Enumerable.Range(0, 40).Select(i => i * 0.25)]; double[] y = [.. Enumerable.Range(0, 40).Select(i => i * 0.25)]; var z = new double[y.Length, x.Length]; for (int row = 0; row < y.Length; row++) for (int col = 0; col < x.Length; col++) z[row, col] = Math.Sin(x[col]) * Math.Cos(y[row]); Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.Surface(x, y, z, s => { s.EdgeColor = Colors.Black.WithAlpha(40); // wireframe stroke (null = default) s.RowStride = 2; // render every 2nd row — faster for large grids s.ColStride = 2; // render every 2nd column })) .Save("surface.svg"); ``` --- ## Figure-Level ColorBar Render a single colorbar shared across all subplots, positioned outside the plot area. ```csharp using MatPlotLibNet; using MatPlotLibNet.Styling; double[,] dataA = { { 1, 2, 3 }, { 4, 5, 6 } }; double[,] dataB = { { 6, 5, 4 }, { 3, 2, 1 } }; Plt.Create() .WithColorBar(cb => cb with { Label = "Signal Intensity", Orientation = ColorBarOrientation.Vertical, // or Horizontal Shrink = 0.9, Padding = 10 }) .AddSubPlot(1, 2, 1, ax => ax.Heatmap(dataA)) .AddSubPlot(1, 2, 2, ax => ax.Heatmap(dataB)) .Save("shared_colorbar.svg"); ``` The colorbar picks up its colormap and range from the first `IColorBarDataProvider` series found across all subplots. --- ## TwinY — Secondary X-Axis Add a second X-axis at the top of a subplot, independent of the primary X-axis. ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; using MatPlotLibNet.Styling; double[] x = [1, 2, 3, 4]; double[] y = [10, 20, 15, 25]; double[] x2 = [100, 200, 300, 400]; double[] celsius = [-10, 5, 20, 35, 50]; double[] power = [1.8, 1.4, 1.0, 1.3, 1.9]; // On the Axes model var figure = new Figure(); var axes = figure.AddSubPlot(); axes.Plot(x, y); axes.TwinY(); var secondary = axes.PlotXSecondary(x2, y); secondary.Color = Colors.Red; secondary.Label = "Secondary"; figure.Save("twin_y.svg"); // Or via AxesBuilder Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Plot(x, y) .WithSecondaryXAxis(b => { b.SetXLabel("Temperature (°C)"); b.SetXLim(-10, 50); b.PlotXSecondary(celsius, power, s => s.Color = Css4Colors.OrangeRed); })) .Save("secondary_x.svg"); ``` Top-edge ticks and label are rendered automatically. The secondary X range is computed from `XSecondarySeries` data. --- ## SaveOptions `Save` takes a `SaveOptions` record, and each format uses what it can: | Setting | What it does | |---|---| | `Dpi` | sizes a PNG: a figure is laid out in pixels at 96 dpi, and the PNG is drawn with dpi / 96 pixels for each, so 500 × 350 at 150 dpi is 781 × 547. Left at 96 it leaves the figure's own `WithDpi` in force; any other value overrides it for that save. An SVG, a PDF page and JSON have no pixels and ignore it. | | `PrettifySvg` | writes the SVG indented, one element to a line | | `SvgDecimalPrecision` | rounds positions and sizes in the SVG to 0 to 15 decimals, half away from zero; stroke widths, opacities and text keep their digits | | `Title`, `Author` | the title and creator in the SVG's metadata, the PDF's document information and the PNG's text chunks | A GIF follows the dpi of each frame's figure. A format that cannot use a setting ignores it, so the same options can be handed to any save. `WithDpi` on the figure builder sets the resolution once for every PNG of the figure. ```csharp using MatPlotLibNet; double[] x = [1, 2, 3, 4]; double[] y = [12, 15, 14, 18]; var fig = Plt.Create().Plot(x, y).Build(); fig.Save("output.svg", new SaveOptions { PrettifySvg = true, SvgDecimalPrecision = 1, Title = "Annual Revenue", Author = "Finance Team" }); fig.Save("output.png", new SaveOptions { Dpi = 150, Title = "Annual Revenue" }); // needs MatPlotLibNet.Skia ``` --- ## Annotations ### Reference Line Labels Labels on `AxHLine` / `AxVLine` are now rendered automatically. A line beyond the data widens the automatic range, as matplotlib's `axhline` does, so it is drawn inside the plot; beyond a limit you set (or a zoom), neither the line nor its label is drawn. ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; using MatPlotLibNet.Styling; double[] x = [0, 10, 20, 30, 40, 50]; double[] y = [20, 60, 110, 150, 90, 70]; var figure = new Figure(); var ax = figure.AddSubPlot(); ax.Plot(x, y); var line = ax.AxHLine(50.0); line.Label = "Mean"; line.Color = Colors.Red; // Horizontal: label appears right-aligned at the right edge, above the line. // Vertical: label appears near the top of the line. ``` ### Connection Styles Control how the arrow connects the annotation text to its target. ```csharp var ann = ax.Annotate("Peak", 42.0, 150.0); ann.ArrowTargetX = 45.0; ann.ArrowTargetY = 90.0; ann.ConnectionStyle = ConnectionStyle.Arc3; // or Straight / Angle / Angle3 ann.ConnectionRad = 0.4; // curvature for Arc3/Angle3 ann.ArrowStyle = ArrowStyle.FancyArrow; ann.ArrowHeadSize = 10; ``` | `ConnectionStyle` | Shape | |---|---| | `Straight` | Direct line (default) | | `Arc3` | Bezier arc, offset by `ConnectionRad × distance` | | `Angle` | Horizontal then vertical (elbow) | | `Angle3` | Smoothed elbow with bezier rounding | ### Arrow Head Styles ```csharp ann.ArrowStyle = ArrowStyle.Wedge; // wider filled arrowhead ann.ArrowStyle = ArrowStyle.CurveB; // open curved head at target ann.ArrowStyle = ArrowStyle.BracketAB; // perpendicular brackets at both ends ann.ArrowStyle = ArrowStyle.None; // no arrowhead (line only) ``` ### Callout Boxes Wrap annotation text in a styled box. ```csharp var note = ax.Annotate("Note", 1.5, 1.5); note.BoxStyle = BoxStyle.Round; note.BoxPadding = 6; note.BoxCornerRadius = 8; note.BoxFaceColor = Colors.White; note.BoxEdgeColor = Colors.Gray; note.BoxLineWidth = 1.5; ``` | `BoxStyle` | Shape | |---|---| | `None` | No box (default) | | `Square` | Plain rectangle | | `Round` | Rounded corners (bezier arcs) | | `RoundTooth` | Rounded rect with zigzag bottom edge | | `Sawtooth` | Sawtooth edges on all sides | ### Fluent API — Annotation with Arrow ```csharp using MatPlotLibNet; using MatPlotLibNet.Models; double[] x = [0, 10, 20, 30, 40, 50]; double[] y = [20, 60, 110, 150, 90, 70]; // Single call sets text, position, and arrow target: Plt.Create() .Plot(x, y) .Annotate("High", 42.0, 150.0, arrowX: 45.0, arrowY: 90.0, ann => { ann.ConnectionStyle = ConnectionStyle.Angle; ann.ArrowStyle = ArrowStyle.FancyArrow; }) .ToSvg(); ``` ### Span Region Border and Label ```csharp var span = ax.AxHSpan(1.0, 2.0); span.Label = "Support zone"; span.LineStyle = LineStyle.Dashed; span.LineWidth = 1.5; span.EdgeColor = Colors.DarkGray; figure.Save("annotations.svg"); ```