Skip to content
xkqg edited this page Apr 19, 2026 · 90 revisions

MatPlotLibNet Wiki

Matplotlib-inspired charting for .NET — SVG/PNG/PDF output, 74 series types, fluent API, streaming & realtime, and first-class support for Blazor, MAUI, Avalonia, Uno Platform, ASP.NET Core, and JS frameworks.

Current version: 1.7.2 · NuGet · Changelog · License: MIT

🧭 Stabilisation phase

After eleven feature releases (v1.0 → v1.7.2) MatPlotLibNet now covers the practical 90% of matplotlib's surface: 74 series types, 13 map projections with embedded Natural Earth data, 26 themes, MathText with operator limits and matrices, streaming with O(1) indicators, native UI controls for Blazor / Avalonia / Uno / WPF / MAUI, fidelity tests against a pinned matplotlib reference, and 13 NuGet packages.

v1.7.1 started the stabilisation period; v1.7.2 continues it. The focus shifts from "ship more features" to:

  • 🐛 Bug fixes only (no new public API), driven by community use and the ≥90/90 coverage gate
  • 🧪 Test coverage uplift (the eight-phase plan in COVERAGE.md) — coverage moved from 85.2% / 68.4% (v1.7.0 baseline) to ~95% line / ~86% branch with 7 203 tests across 9 test projects. Sub-90 substantive classes pushed close to single digits (was 241 at v1.7.0, 110 mid-Phase-W, 64 post-Phase-X); Phases X+Y combined graduated ~70 classes through stacked-OO test bases (ModifierTestBase<T>, RendererCoverageTestBase<T>, StreamingHostFixture) + 8 documented exemptions for provably-unreachable defensive arms. Strict-mode flip is now feasible — discuss after the next CI confirms 0-5 substantive sub-90 classes.
  • 📚 Documentation polish — cookbook examples, API XML doc completeness
  • 🌱 Listening — what should v2 be? Open a Discussion or Issue with what's missing for your use case. The next major direction will be guided by what real users need, not by a feature checklist.

No timeline for v1.8.0 yet — when it ships, it will be community-driven.


v1.7.2 — Browser-interaction subsystem hardened end-to-end (13-phase TDD plan + matplotlib-parity follow-on) + bug fixes + coverage uplift + CI hardening. Continuation of the v1.7.1 stabilisation track. Now the stable release.

Matplotlib-parity follow-on (Phases A–C + F) — summary:

  • Phase F (depth-sort tier isolation, 2026-04-18): After Phases A–C shipped, the playground's 3D Surface STILL showed back panes painting over back-corner surface quads on any drag. Root cause was one line in Svg3DRotationScript.resortDepth — it sorted ALL [data-v3d] children together (panes + grid + labels + ticks + series). At many camera angles (including matplotlib default az=-60, el=30, and playground default az=-50, el=35) back-corner surface quads have more negative viewZ than panes → sorted earlier in DOM → drawn first → opaque panes covered them. Matplotlib never depth-sorts axis infrastructure with data (axes3d.py:458-470); we now mirror that via three explicit subgroups inside the scene group: <g class="mpl-3d-back"> (panes / edges / grid / axis labels), <g class="mpl-3d-data"> (series quads — JS sorts in place here), <g class="mpl-3d-front"> (tick marks + labels). Back and front tiers stay in server render order regardless of camera angle — guaranteed no-occlusion painter's order.

The 13-phase interaction-hardening shipped earlier in v1.7.2 fixed the wiring (one-call WithBrowserInteraction(), per-chart isolation, pointer events, 3D scene-group axes), but two end-to-end behaviours remained broken: (1) on a 3D chart, drag was captured by the 2D pan handler instead of the 3D rotation handler, so the user could only translate the chart; (2) even when 3D rotation fired, the JS reproject used a simplified projection that disagreed with server-side Projection3D, so on the first drag the cube would visually jump. This follow-on fixes both root-causes (defence-in-depth), ports matplotlib's full view+persp+fit projection pipeline to JS, and aligns drag math + 2D wheel-zoom rate with matplotlib's canonical formulas.

  • 3D drag now ROTATES the camera, not pans the chart. Root cause was a 2D/3D event collision: WithBrowserInteraction() enabled both pan/zoom and 3D rotate handlers; the SVG-root setPointerCapture overrode the scene's, stealing the drag for 2D pan. Fixed via e.stopPropagation() on the 3D handler + the 2D handler bailing out when an .mpl-3d-scene is present (mirrors matplotlib's NavigationToolbar2 disabling Pan/Zoom on 3D axes).
  • Full matplotlib Projection3D ported to JS — first-drag visual continuity (no more cube jumping to a different position before rotating). Server and client agree to within 1 px at every camera angle (verified against the playground's actual 20×20 sinc surface on [-3, 3]², not just synthetic [0, 1]³ cubes).
  • Drag math = matplotlib _on_move — dazim = -(dx/w)·180, delev = -(dy/h)·180. A full-axes drag rotates 180° (was: fixed 0.5°/pixel + inverted azimuth sign). ±90° elev clamp removed (matplotlib doesn't clamp; the V-vector flips at the pole naturally).
  • 2D wheel-zoom rate = 0.85^step (matplotlib NavigationToolbar2.scroll_handler); previously 1.10/0.90.
  • x / y modifier keys lock pan to one axis (matplotlib _base.py:format_deltas convention).

Headline interaction fixes (13-phase TDD plan):

  • 2D scroll-wheel actually zooms now (was passive-listener silently scrolling page).

  • 3D rotation moves the entire scene — axes, grid, panes, ticks, tick labels — not just data polygons.

  • 3D scroll-wheel zoom + Home-key full reset added.

  • Pointer Events + pinch-to-zoom for touch parity across every interaction script.

  • Per-chart isolation — eight scripts that previously cross-talked between charts on one page now self-locate via currentScript.parentNode.

  • Themable opacity + transition tokens via WithInteractionTheme(InteractionTheme theme).

  • URL-hash state persistence (opt-in) — refresh keeps zoom/pan state.

  • 3D lighting recomputation hooks — face normals + light direction emitted under rotation for camera-anchored re-shading.

  • Treemap interaction now steady-pictures, collapse-first (v1.7.2 Phase W) — initial view shows every node at every depth (interactive view = static SVG, no jump on first paint); z-order means the deepest visible label wins in any overlapping region. Click a parent rect to collapse its entire subtree (transitive); click again to restore. Multiple subtrees can be collapsed independently. Leaves are not clickable.

  • Behavioural test harness (Jint-backed DOM stub) — replaces static-emission-only pattern with real event-simulation + DOM-mutation assertions.

  • FigureBuilder.WithBrowserInteraction() now also enables 3D rotation, treemap expand/collapse, and sankey hover in addition to the 2D scripts. The playground 3D Surface example with Browser Interaction toggled on now responds to drag-to-rotate. Each chart-type-specific script is inert when its element isn't in the SVG, so 2D charts pay no runtime cost.

  • Phase P — six community themes now visually distinct. Grayscale (no hues), Paper (serif, 11pt, no grid), Presentation (bold, 16pt), Poster (bold, 20pt, 1.5-linewidth grid), GitHub (brand palette + #E1E4E8 grid), and Minimal (no grid, 11pt) were previously near-identical to Theme.Default; each now carries its own palette, font, and grid defaults. See MatplotlibThemes for the full table.

  • Theme Comparison cookbook image now actually shows six different themes instead of six identical Default-theme renders. Themes are figure-scoped (matplotlib parity), so the new image renders six small figures and composites them into a 2×3 grid via SkiaSharp (PNG) and nested <svg><g transform> (SVG).

  • 6-batch coverage uplift (Phases A-F) — NEAR-bucket polish, direct-invocation tests for 22 SeriesRenderers (17 graduated to 100/100); 16 series added to AllSeriesInstances with 5 new cross-cutting Theory methods; 5 streaming indicators graduated (Cci, Obv, Vwap, WilliamsR, Atr); 8 ColorMap normalizer + 3 TickFormatter + 3 Geo edge-case test files; 11 misc long-tail classes graduated. +1 192 tests (4 276 → 5 569).

  • Phase-9 deduplication — 78 per-series default-property tests folded into the central AllSeriesTests Theory pattern. Net: 5 569 → 5 468 tests, zero coverage regression. Adding a new series only requires one line in AllSeriesInstances plus its Visit overload — that single addition runs ~12 conformance tests automatically.

  • CI hardening — Skia tests now ship SkiaSharp.NativeAssets.{Linux.NoDependencies, Win32, macOS} so libSkiaSharp.so actually loads on Linux runners. SkiaSharp 3.x split native libs into per-OS packages; the v1.7.1 hotfix installed libfontconfig1/libfreetype6 (font deps) but not the binary itself.

  • Coverage gate — sub-90/90 class count went 241 → 154; default-mode regression check stays green; 14 documented exemptions for sample / interface / JS-template code. Baseline regenerated.

  • Two real bugs surfaced for follow-up (tracked in CHANGELOG): BaselineHelper.ComputeWiggle/ComputeWeightedWiggle throw on empty input; SymLogNormalizer.Normalize(NaN) throws.

Phase O follow-on — enum binary-compatibility hardening:

  • Every public enum now has explicit ordinals (= N on every member). 45 enums, 46 new tests in EnumOrdinalContractTests pin the (name → ordinal) mapping as a CI-gated contract. Reordering / removing / renumbering any member turns CI red with a clear message. Append-only rule documented in every enum's XML remarks.
  • Defensive JsonStringEnumConverter registered in ChartSerializer — no integer ordinal can leak into persisted JSON even if a future DTO wires an enum-typed property directly.

Phase N follow-on — root-cause response to the Phase L/M bug-hunting marathon:

  • N.1 — Magic-string elimination (playground). Four free-form string properties + 16-string example dispatcher replaced with typed PlaygroundExample enum + typed Theme / LineStyle / MarkerStyle / IColorMap properties. Razor dropdowns iterate Enum.GetValues<T>(); a typo in the playground is now a compiler error.
  • N.2 — Enum contract tests. New EnumOutputContract helper asserts every rendering-driving enum value produces byte-distinct SVG output. Covers TickDirection / HistType / BoxStyle / ConnectionStyle / ArrowStyle / AxisScale. First-run hit: 3 more silent-collapse bugs surfaced (ArrowStyle.CurveA==CurveB, BracketA==BracketB, AxisScale.Logit==Linear) — documented via [Fact(Skip=…)] tests that un-skip when the renderer is fixed.

Phase M follow-on — 2 user-reported defects + 1 deeper bug surfaced:

  • M.1 — Playground "Open in new tab" HTML wrap. Chart now fills the browser viewport AND pan/zoom/tooltips work in the new tab (pre-fix bare-SVG blob broke both).
  • M.2 — Shared MarkerRenderer fixes 11 silently-broken marker shapes. MarkerStyle has 13 members; pre-fix LineSeriesRenderer always drew a circle and ScatterSeriesRenderer only honoured Square. New internal helper dispatches all 13 shapes; both renderers delegate via one call each.

Phase L follow-on — 7 user-reported defects closed:

  • L.1 / L.2 — Responsive SVG by default (style="max-width:100%;height:auto" on the SVG root; opt-out with WithResponsiveSvg(false)).
  • L.5 — Playground width/height sliders removed (redundant with responsive default; intrinsic aspect 800×450, 16:9).
  • L.6 — Scatter marker dropdown + size slider now actually change the dots.
  • L.7 — Playground <iframe srcdoc> regression fixed: bare SVG in srcdoc stopped embedded scripts from running; new SvgIframeWrapper.WrapForIframe wraps in a proper HTML document so pan/zoom/tooltips/3D-rotate work again in the preview.
  • L.8 — Tick-label rotation (WithXTickLabelRotation / WithYTickLabelRotation + auto-rotate to 30° on overlap — matplotlib Figure.autofmt_xdate parity; fixes Candlestick's garbled X-axis).
  • L.9 — Contour colormap routing fixed + AxesBuilder.WithColorMap(string) now throws on unknown names (was silent fallback to Viridis).
  • L.11 — WithTightMargins() now actually makes data touch the spines (pre-fix nice-bounds expansion widened the axis anyway).

5 385 tests green (3 known-bug skips) across 9 test projects covering 13 NuGet packages (was 5 776 pre-Phase P — Phase P consolidated the treemap test set, 5 730 pre-Phase O, 5 714 pre-Phase N, 5 693 pre-Phase M, 5 594 pre-Phase L, 4 275 at v1.7.1, 3 967 in v1.7.0).

For earlier releases, see the full CHANGELOG or the Roadmap.


Who Should Use This?

MatPlotLibNet is built for .NET developers who need charting that fits naturally into their existing stack — no JavaScript framework, no third-party SaaS, no extra runtime.

You are… MatPlotLibNet gives you…
Blazor / ASP.NET Core developer Drop-in <MplChart> and <MplLiveChart> components; SignalR push; REST endpoint — all wired with standard DI
Avalonia developer Native MplChartControl via SkiaSharp — IsInteractive="True" for pan / zoom / reset / brush-select, no WebView
Uno Platform developer Native MplChartElement via SkiaSharp — Windows, Android, iOS, macCatalyst from one codebase
MAUI developer Native MplChartView control via Microsoft.Maui.Graphics — no WebView
Data scientist / analyst Inline SVG in Polyglot Notebooks or Jupyter; 74 series types; MatPlotLibNet.DataFrame for plotting, 16 financial indicators, and polynomial regression directly from Microsoft.Data.Analysis.DataFrame column names
Financial / trading app developer Candlestick, OHLC, 13 technical indicators (SMA, EMA, RSI, MACD, Bollinger, Stochastic, …), backtesting panels
Full-stack .NET + JS developer TypeScript clients for Angular, React, and Vue — same SignalR hub, same chart definitions
Backend / headless pipeline Render SVG, PNG, PDF, or animated GIF server-side with zero UI dependencies

Why MatPlotLibNet?

vs. ScottPlot / OxyPlot / LiveCharts2

These are excellent WinForms / WPF / MAUI renderers. MatPlotLibNet targets the web and server first: SVG rendered on the server, pushed to any browser client over SignalR. No canvas, no WebAssembly boot cost, no browser rendering differences.

vs. Plotly.NET

Plotly.NET emits JavaScript that runs in the browser. MatPlotLibNet renders entirely in .NET — the browser receives finished SVG. That means no JS dependency, consistent output, inline SVG in the DOM (CSS-styleable, accessible, print-quality), and rendering that scales with your server hardware.

vs. SciChart / Syncfusion

Commercial libraries with per-developer or per-app licensing. MatPlotLibNet is MIT — free for any use, open-source or commercial, with no copyleft conditions.

Key differentiators at a glance

MatPlotLibNet
Rendering Server-side SVG — zero client cost
Output SVG, PNG, PDF, JSON, animated GIF
Native controls MplChartControl (Avalonia 12), MplChartElement (Uno Platform) — SkiaSharp, no WebView
Real-time SignalR push + streaming series with ring buffers, 11 incremental indicators, throttled re-rendering
Series types 74 — including 12 3D, 4 streaming, polar, financial, hierarchical, Sankey, vector
Math text $\frac{a}{b}$, $\sqrt{x}$, $\hat{y}$, $\mathbb{R}$ — 96 symbol mappings
Performance AVX SIMD coordinate transform, parallel subplot rendering
License MIT — no copyleft, free for any use
Dependencies Zero for SVG; SkiaSharp opt-in for PNG/PDF/GIF/native controls

Quick Start

dotnet add package MatPlotLibNet
Plt.Create()
   .WithTitle("Hello MatPlotLibNet")
   .Plot(x, y, s => { s.Color = Color.Blue; s.Label = "Data"; })
   .WithLegend()
   .Save("chart.svg");

MatPlotLibNet dashboard


Documentation

Resource Contents
Playground Try charts live in your browser — pick an example, change theme/size, see the SVG update instantly
Cookbook Code examples with rendered images — copy, paste, chart
API Reference Full API documentation generated from XML doc comments

Wiki Pages

Page Contents
Getting Started Installation, quick start, output formats (SVG/PNG/PDF/JSON/GIF)
Fluent Cheatsheet One-page reference for Plt / FigureBuilder / AxesBuilder —
Package Map All 13 NuGet packages + 3 JS packages explained
Notebooks Polyglot Notebooks + Jupyter — inline SVG, Display(), tips
Chart Types All 74 series types with code examples
Styling Themes, colors, colormaps, rcParams, PropCycler
Advanced Layouts, date axes, math text, animations, real-time charts
Streaming ring buffers, StreamingFigure, axis scaling, 11 streaming indicators, platform controls
Interactive Controls Avalonia + Uno native controls, managed interaction layer, server mode
Bidirectional SignalR server-authoritative interactive charts, event hierarchy, FigureRegistry, hub wiring
Benchmarks SVG rendering, SIMD transforms, indicators, JSON, PNG/PDF
Roadmap Version history; v1.3.0 complete
Contributing How to build, test, and contribute

At a Glance

Series types 74 — line, scatter, bar, financial, statistical, 12 3D, 4 streaming, polar, hierarchical, Sankey, vector
Output formats SVG (built-in), PNG, PDF, JSON, animated GIF
Platforms .NET 10 / .NET 8, Blazor, Avalonia, Uno Platform, MAUI, ASP.NET Core, Angular, React, Vue, Jupyter
Native controls MplChartControl (Avalonia 12), MplChartElement (Uno/WinUI 3) — SkiaSharp, no WebView
Real-time SignalR push + bidirectional interaction (zoom, pan, reset, legend toggle, brush-select, hover)
Math text LaTeX-like inline math: $\frac{a}{b}$, $\sqrt{x}$, $\hat{y}$, $\mathbb{R}$, $\alpha^{2}$ — 96 symbol mappings
Date axes Auto tick selection from years down to seconds
Layout Constrained layout engine — auto-computes margins from text extents
Transform AVX SIMD single-pass coordinate transform — up to 11.8× faster than naive
Packages 13 NuGet + 3 npm — core, Skia, Blazor, AspNetCore, Avalonia, Uno, MAUI, DataFrame, Interactive, GraphQL, Notebooks
Tests 5 385 tests across 9 test projects (post-1.7.1 uplift wave + Phase-9 dedup + 13-phase interaction-hardening behavioural harness + matplotlib-parity follow-on + Phase L defect closure + Phase M marker-renderer & new-tab wrap + Phase N magic-string elimination + enum contract tests + Phase O enum ordinal hardening + Phase P treemap expand/collapse + theme-distinctness consolidation) — including 146 dual-theme matplotlib fidelity tests pixel-verified against matplotlib 3.10.8

Clone this wiki locally