Repository navigation
Home
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
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/90coverage gate - 🧪 Test coverage uplift (the nine-phase plan in
COVERAGE.md) — coverage moved from 85.2% / 68.4% (v1.7.0 baseline) to ~96% line / ~88% branch with 7 345 tests across 9 test projects. Sub-90 substantive classes down to ~40 (was 241 at v1.7.0, 110 mid-Phase-W, 64 post-Phase-X, 50 post-Phase-Y); Phases X+Y+Z combined graduated ~85 classes through stacked-OO test bases (ModifierTestBase<T>,RendererCoverageTestBase<T>,StreamingHostFixture, custom-SeriesRenderer-subclass) + Theory-driven dispatch coverage + 11 documented exemptions for provably-unreachable defensive arms. Strict-mode flip stays deferred — the residual is concentrated in big-axes classes (cyclomatic 200+) where each fact only nudges 1-2pp on a 400-branch denominator. - 📚 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-sideProjection3D, 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-rootsetPointerCaptureoverrode the scene's, stealing the drag for 2D pan. Fixed viae.stopPropagation()on the 3D handler + the 2D handler bailing out when an.mpl-3d-sceneis present (mirrors matplotlib'sNavigationToolbar2disabling Pan/Zoom on 3D axes).- Full matplotlib
Projection3Dported 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: fixed0.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(matplotlibNavigationToolbar2.scroll_handler); previously1.10/0.90.x/ymodifier keys lock pan to one axis (matplotlib_base.py:format_deltasconvention).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 +
#E1E4E8grid), and Minimal (no grid, 11pt) were previously near-identical toTheme.Default; each now carries its own palette, font, and grid defaults. See MatplotlibThemes for the full table.
Theme Comparisoncookbook 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
AllSeriesInstanceswith 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
AllSeriesTestsTheory pattern. Net: 5 569 → 5 468 tests, zero coverage regression. Adding a new series only requires one line inAllSeriesInstancesplus itsVisitoverload — that single addition runs ~12 conformance tests automatically.CI hardening — Skia tests now ship
SkiaSharp.NativeAssets.{Linux.NoDependencies, Win32, macOS}solibSkiaSharp.soactually loads on Linux runners. SkiaSharp 3.x split native libs into per-OS packages; the v1.7.1 hotfix installedlibfontconfig1/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/ComputeWeightedWigglethrow on empty input;SymLogNormalizer.Normalize(NaN)throws.Phase O follow-on — enum binary-compatibility hardening:
- Every public enum now has explicit ordinals (
= Non every member). 45 enums, 46 new tests inEnumOrdinalContractTestspin 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
JsonStringEnumConverterregistered inChartSerializer— 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
PlaygroundExampleenum + typedTheme/LineStyle/MarkerStyle/IColorMapproperties. Razor dropdowns iterateEnum.GetValues<T>(); a typo in the playground is now a compiler error.- N.2 — Enum contract tests. New
EnumOutputContracthelper asserts every rendering-driving enum value produces byte-distinct SVG output. CoversTickDirection/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
MarkerRendererfixes 11 silently-broken marker shapes.MarkerStylehas 13 members; pre-fixLineSeriesRendereralways drew a circle andScatterSeriesRendereronly 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 withWithResponsiveSvg(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 insrcdocstopped embedded scripts from running; newSvgIframeWrapper.WrapForIframewraps 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 — matplotlibFigure.autofmt_xdateparity; 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.
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 |
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.
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.
Commercial libraries with per-developer or per-app licensing. MatPlotLibNet is MIT — free for any use, open-source or commercial, with no copyleft conditions.
| 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 |
dotnet add package MatPlotLibNetPlt.Create()
.WithTitle("Hello MatPlotLibNet")
.Plot(x, y, s => { s.Color = Color.Blue; s.Label = "Data"; })
.WithLegend()
.Save("chart.svg");
| 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 |
| 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 |
| 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 |
MatPlotLibNet 1.22.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