# Chart Types > **Try it live:** Open the [Playground](https://xkqg.github.io/MatPlotLibNet/playground/) to experiment with charts in your browser. Browse the [Cookbook](https://xkqg.github.io/MatPlotLibNet/cookbook/) (25 pages) for copy-paste code examples with rendered output. MatPlotLibNet has **83 series types** across 14 categories — four of them streaming — plus 13 map projections and geographic polygon rendering 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](#geographic--maps--matplotlibnetgeo) below for everything the geo package adds. --- ## Gallery The fidelity suite renders these charts and compares them pixel for pixel against matplotlib. Click a tile for the full-resolution PNG. | | | | |---|---|---| | [![Line chart — KDE](images/gallery_kde.png)](images/gallery_kde.png) **KDE** (Gaussian kernel density) | [![Violin plot](images/gallery_violin.png)](images/gallery_violin.png) **Violin** (3 groups) | [![Box plot](images/gallery_box.png)](images/gallery_box.png) **Box** (4 groups) | | [![Hexbin density](images/gallery_hexbin.png)](images/gallery_hexbin.png) **Hexbin** (2-D binning) | [![Stem plot](images/gallery_stem.png)](images/gallery_stem.png) **Stem** (sampled signal) | [![Waterfall chart](images/gallery_waterfall.png)](images/gallery_waterfall.png) **Waterfall** (finance) | | [![Sankey diagram](images/gallery_sankey.png)](images/gallery_sankey.png) **Sankey** (flow) | [![Radar chart](images/gallery_radar.png)](images/gallery_radar.png) **Radar** (5 axes) | [![Streamplot](images/gallery_streamplot.png)](images/gallery_streamplot.png) **Streamplot** (vector field) | | [![Polar scatter](images/gallery_polar_scatter.png)](images/gallery_polar_scatter.png) **Polar scatter** | [![Treemap](images/gallery_treemap.png)](images/gallery_treemap.png) **Treemap** (squarified) | [![Pie chart](images/gallery_pie.png)](images/gallery_pie.png) **Pie** (with labels) | --- ## Basic ![Bar and line charts](images/bar_labels.png) ### Line — `Plot()` ```csharp Plt.Create().Plot(x, y).Save("line.svg"); Plt.Create().Plot(x, y, s => { s.Color = Color.Blue; s.LineWidth = 2; s.Label = "Series A"; }).Save("line.svg"); ``` ### Scatter — `Scatter()` ```csharp Plt.Create().Scatter(x, y).Save("scatter.svg"); Plt.Create().Scatter(x, y, s => { s.MarkerStyle = MarkerStyle.Circle; s.MarkerSize = 8; }).Save("scatter.svg"); ``` ### Bar — `Bar()` ```csharp Plt.Create().Bar(["Q1", "Q2", "Q3", "Q4"], [100, 200, 150, 250]).Save("bar.svg"); ``` ### Histogram — `Hist()` ```csharp double[] data = /* ... */; Plt.Create().Hist(data, bins: 20).Save("hist.svg"); ``` ### Pie / Donut ```csharp Plt.Create().Pie(["A", "B", "C"], [40, 35, 25]).Save("pie.svg"); Plt.Create().Donut(["A", "B", "C"], [40, 35, 25]).Save("donut.svg"); ``` ### Step, Fill, Error Bars, Stem, Box, Violin ```csharp Plt.Create().Step(x, y).Save("step.svg"); Plt.Create().Fill(x, y1, y2).Save("fill.svg"); // fill between two lines Plt.Create().ErrorBar(x, y, yErr).Save("errbar.svg"); Plt.Create().Stem(x, y).Save("stem.svg"); Plt.Create().Box(groups).Save("box.svg"); // groups: double[][] Plt.Create().Violin(groups).Save("violin.svg"); ``` ### SignalSeries / SignalXYSeries — High-Performance Large Datasets 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. ```csharp // 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"); ``` --- ## Statistical ```csharp 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, labels).Save("strip.svg"); Plt.Create().Swarmplot(groups, labels).Save("swarm.svg"); Plt.Create().Pointplot(groups, labels).Save("point.svg"); Plt.Create().Eventplot(events).Save("event.svg"); Plt.Create().Regression(x, y).Save("regression.svg"); // with LeastSquares fit line Plt.Create().Residual(x, y, yFit).Save("residual.svg"); Plt.Create().Count(categories).Save("count.svg"); ``` --- ## Financial ![Financial dashboard](images/financial_dashboard.png) `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. ### Financial dashboard (3-panel) ```csharp 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"); ``` ### Candlestick ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Candlestick(open, high, low, close) .Sma(period: 20) .WithLegend()) .Save("candlestick.svg"); ``` ### OHLC ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.Ohlc(open, high, low, close)) .Save("ohlc.svg"); ``` --- ## Grid / Field ![Heatmap with colormap](images/heatmap_colormap.png) ### Heatmap ```csharp double[,] data = /* 2-D array */; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Heatmap(data) .WithColorMap("plasma") .WithColorBar(cb => cb with { Label = "Intensity" })) .Save("heatmap.svg"); ``` ### Contour & Filled Contour ```csharp Plt.Create().Contour(x, y, z).Save("contour.svg"); Plt.Create().Contourf(x, y, z).Save("contourf.svg"); ``` ### Others ```csharp Plt.Create().Image(bitmap).Save("image.svg"); Plt.Create().Hexbin(x, y).Save("hexbin.svg"); Plt.Create().Pcolormesh(x, y, z).Save("pcolor.svg"); Plt.Create().Tricontour(x, y, z).Save("tricontour.svg"); Plt.Create().Tripcolor(x, y, z).Save("tripcolor.svg"); ``` --- ## 3-D 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. ```csharp // Original 6 Plt.Create().Surface3D(x, y, z).Save("surface.svg"); Plt.Create().Wireframe3D(x, y, z).Save("wireframe.svg"); Plt.Create().Scatter3D(x, y, z).Save("scatter3d.svg"); Plt.Create().Bar3D(x, y, heights).Save("bar3d.svg"); Plt.Create().PlanarBar3D(x, y, heights).Save("planar_bars.svg"); Plt.Create().Stem3D(x, y, z).Save("stem3d.svg"); // v1.3.0 — 6 new series Plt.Create().Plot3D(x, y, z).Save("line3d.svg"); // projected polyline Plt.Create().Trisurf(x, y, z).Save("trisurf.svg"); // Delaunay triangulated surface Plt.Create().Contour3D(x, y, z).Save("contour3d.svg"); // marching-squares contour lines Plt.Create().Quiver3D(x, y, z, u, v, w).Save("quiver3d.svg"); // 3D vector field Plt.Create().Voxels(filled).Save("voxels.svg"); // face-culled cubes Plt.Create().Text3D(x, y, z, "label").Save("text3d.svg"); // 3D annotation ``` ### Bar3D — solid cuboid bars ![Bar3D single series](images/threed_bar3d_interactive.png) This is the equivalent of matplotlib's `ax.bar3d(...)`. It draws rectangular prisms with matplotlib-exact per-face shading (`0.65 + 0.35·dot(n̂, l̂)`), depth-sorted across all bars so occlusion is correct at any camera angle. Multiple `Bar3D` calls on one axes stack into a grid: ![Bar3D grouped — 5 rows](images/threed_bar3d_grouped.png) ```csharp record Row(double Y, Color Color); Row[] rows = [new(0, Colors.Red), new(1, Colors.Green), new(2, Colors.Blue), new(3, Colors.Cyan), new(4, Colors.Gold)]; 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 = /* ... */; ax.Bar3D(xs, ys, zs, s => { s.Color = color; s.BarWidth = 0.4; }); } }) .Save("bar3d_grouped.svg"); ``` ### PlanarBar3D — flat translucent 3-D bars ![PlanarBar3D skyscraper](images/threed_planar_bars.png) 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. ```csharp 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 = /* ... */; ax.PlanarBar3D(xs, ys, zs, s => { s.Color = color; s.BarWidth = 0.8; s.Alpha = 0.8; // translucency }); } }) .Save("planar_bars.svg"); ``` #### Per-bar colour override via `Colors[]` `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 = planeColor` on each series - **Per X** — set `s.Colors = xs.Select(x => lookup(x)).ToArray()` - **Combined** — set both: `Colors[i]` wins where defined, `Color` is the fallback ![PlanarBar3D x=0 highlight](images/threed_planar_bars_x0_highlight.png) ```csharp var darkCyan = Color.FromHex("#008B8B"); 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(); }); ``` See [[Advanced]] for how the library composites several 3-D series by depth. ### Camera, lighting, and rotation Applies to every 3-D series: ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Surface3D(x, y, z) .SetXLabel("x").SetYLabel("y").SetZLabel("sin(r)/r") .SetZLim(-0.3, 1.0) .WithCamera(elevation: 35, azimuth: -55, distance: 8) .WithLighting(ambient: 0.4, diffuse: 0.6, dirX: 1.0) .With3DRotation()) .Save("surface3d.svg"); ``` `distance` controls perspective (orthographic when omitted). `SetZLabel`/`SetZLim` write to `Axes.ZAxis` (`Axis3D`), like `SetXLabel`/`SetYLabel` on 2-D charts. `With3DRotation()` embeds JS for mouse-drag and keyboard rotation (arrow keys + Home reset). > **Builder placement (v1.1.4+)** — call `WithCamera` / `WithLighting` inside the `AddSubPlot(..., ax => ax...)` lambda. At the `Plt.Create().WithCamera(...)` level they only configure the unused figure-level default axes on multi-subplot figures. ### Line3D — projected polyline (v1.3.0) ![Line3D helix](images/threed_line3d_helix.png) Draws a 3-D polyline through arbitrary `(x, y, z)` points. It is depth-sorted and projected through `Projection3D`. ```csharp 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() .AddSubPlot(1, 1, 1, ax => ax .Plot3D(x, y, z, s => { s.Color = Colors.Blue; s.Label = "Helix"; }) .WithCamera(elevation: 25, azimuth: -60) .With3DRotation()) .Save("line3d.svg"); ``` ### Trisurf3D — Delaunay triangulated surface (v1.3.0) ![Trisurf3D](images/threed_trisurf.png) Triangulates unstructured `(x, y, z)` point clouds with Delaunay triangulation. Each face is depth-sorted, then shaded with `Vec3.FaceNormal` and `Color.Shade()`. ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Trisurf(x, y, z, s => { s.Color = Colors.Teal; s.Alpha = 0.8; }) .WithCamera(elevation: 30, azimuth: -45) .With3DRotation()) .Save("trisurf.svg"); ``` ### Contour3D — 3-D contour lines (v1.3.0) ![Contour3D](images/threed_contour3d.png) Computes contour lines on a grid via marching squares and projects them into 3-D space at their Z level. ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Contour3D(x, y, z, s => { s.Levels = 10; }) .WithCamera(elevation: 35, azimuth: -55) .With3DRotation()) .Save("contour3d.svg"); ``` ### Quiver3D — 3-D vector field (v1.3.0) ![Quiver3D](images/threed_quiver3d.png) Draws arrows in 3-D space, each with a shaft and a cone head. Use it for electromagnetic fields, fluid flow, or gradient vectors. ```csharp Plt.Create() .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) .With3DRotation()) .Save("quiver3d.svg"); ``` ### Voxels — volumetric cubes (v1.3.0) ![Voxels](images/threed_voxels.png) Renders a `bool[,,]` voxel grid as face-culled cubes; adjacent filled voxels suppress shared faces. Visible faces are depth-sorted through `DepthQueue3D`. ```csharp var filled = new bool[5, 5, 5]; // fill some voxels... filled[2, 2, 2] = true; filled[2, 2, 3] = true; Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Voxels(filled, s => { s.Color = Colors.Orange; s.Alpha = 0.8; }) .WithCamera(elevation: 30, azimuth: -50) .With3DRotation()) .Save("voxels.svg"); ``` ### Text3D — 3-D annotations (v1.3.0) ![Text3D](images/threed_text3d.png) Places text labels at arbitrary `(x, y, z)` positions, projected to 2-D pixel space. ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax .Surface3D(x, y, z) .Text3D(0, 0, 1.0, "Peak", s => { s.Color = Colors.Red; }) .WithCamera(elevation: 35, azimuth: -55) .With3DRotation()) .Save("text3d.svg"); ``` --- ## Polar ```csharp 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"); ``` --- ## Hierarchical / Flow ### Treemap & Sunburst ```csharp 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"); ``` ### Dendrogram 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`. ```csharp 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](../docs/cookbook/dendrograms.md). ### Clustermap 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. ```csharp 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 = ColorMaps.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](../docs/cookbook/clustermap.md). ### Pair Grid 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. ```csharp 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(); ``` 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](../docs/cookbook/pairplot.md). ### Treemap drilldown (v1.1.4, rewritten in v1.7.2 Phase P, default flipped in v1.7.2 Phase W) ![Treemap drilldown](images/treemap_drilldown.png) **Steady pictures (v1.7.2 Phase W).** 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. ```csharp Plt.Create() .WithTreemapDrilldown() .AddSubPlot(1, 1, 1, ax => ax.Treemap(catalogue, s => s.ShowLabels = true)) .Save("treemap_drilldown.svg"); ``` ### Tree grid 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 a live fleet, two of the 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. ```csharp TreeGridRow[] rows = [ new("Cortex.Ingest", ["56.0%", "1450 MB", "34"]) { Depth = 0, Expanded = true, Url = "/?process=cortex" }, new("obs-ingest", ["31.2%", "—", "12"]) { Depth = 1 }, new("metrics-fold", ["18.7%", "—", "9"]) { Depth = 1, Accent = Colors.Orange }, new("Ldr.Binance", ["46.0%", "920 MB", "27"]) { Depth = 0, Expanded = false, Url = "/?process=binance" }, ]; 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 ``, 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. ### Nested pie (v1.1.4) ![Nested pie](images/nested_pie.png) Renders a two-level `TreeNode` as an inner pie with an outer breakdown ring. It wraps `Sunburst(...)` with `InnerRadius = 0`. ```csharp 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"); ``` ### Network Graph 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. Three deterministic layouts ship in v1.10 PR 1: `Manual`, `Circular` and `Hierarchical`. `ForceDirected` (Fruchterman–Reingold) follows in PR 2. ```csharp 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. ### Relative Rotation Graph (RRG) 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. ```csharp // 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) 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](RelativeRotationSeries) for the full property reference, formula details, and minimum bar requirements. ### Sankey ![Process distribution Sankey](images/sankey_process_distribution.png) 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 where `Home` reappears across columns, pinned with an explicit `SankeyNode.Column` - **`sankey_un_expenses.svg`** — a 2-column baseline with outside labels on a `HideAllAxes()` 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: ```csharp 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 emits an SVG `` per link. | | `SankeySeries.Orient` | `Horizontal` (default) / `Vertical` — top-to-bottom flow | | `SankeySeries.InsideLabels` | Draw labels centred inside the node rect (when `NodeWidth` is wide enough) | --- ## Vector / Flow ```csharp Plt.Create().Quiver(x, y, u, v).Save("quiver.svg"); Plt.Create().Barbs(x, y, u, v).Save("barbs.svg"); Plt.Create().Streamplot(x, y, u, v).Save("stream.svg"); ``` --- ## Specialty ![Sparkline dashboard](images/sparkline_dashboard.png) ```csharp Plt.Create().Radar(categories, values).Save("radar.svg"); Plt.Create().Waterfall(labels, deltas).Save("waterfall.svg"); Plt.Create().Funnel(labels, values).Save("funnel.svg"); Plt.Create().Gauge(value: 0.72, min: 0, max: 1).Save("gauge.svg"); Plt.Create().ProgressBar(value: 0.65).Save("progress.svg"); Plt.Create().Sparkline(timeSeries).Save("sparkline.svg"); Plt.Create().Table(headers, rows).Save("table.svg"); Plt.Create().Spectrogram(signal, sampleRate: 44100).Save("spec.svg"); Plt.Create().BrokenBar(yranges, xranges).Save("brokenbar.svg"); ``` --- ## Control room 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. [![Control room](images/control_room.png)](images/control_room.png) The picture is the output of the code below, at 1200×630. ```csharp 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 = "Nexus (µ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 = "Exchange 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"); ``` ### The tile | 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 | ### What a tile is in 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: ```csharp .AddTile(p99, t => { t.Label = "RFx p99"; t.Condition = p99 > 30 ? new OpsCondition(OpsSeverity.Critical) : p99 > 25 ? new OpsCondition(OpsSeverity.Warning) : OpsCondition.Resting; }) ``` 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. ### Marking a deploy ```csharp .WithEventMarker(deployedAt, m => m.Label = "deploy 1.4.2") ``` 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. ### Thresholds on a chart A threshold line marks which side counts as a breach. The legend shows the live reading beside the series name. ```csharp Plt.Create() .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() // NOTE: WithLegend replaces the legend record wholesale — call it BEFORE .WithLegendValues() // WithLegendValues, or the values silently switch off again .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. ### The runnable one: `MatPlotLibNet.Samples.ControlRoom` ```bash dotnet run --project Samples/MatPlotLibNet.Samples.ControlRoom ``` This 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. --- ## Geographic / Maps — `MatPlotLibNet.Geo` This package adds **13 map projections**, **`GeoPolygonSeries`** (which renders any closed ring as a styled polygon: choropleth, ocean fill, land fill or a custom shape), and **embedded Natural Earth 110m data** (coastlines and countries, with no external download). ```csharp using MatPlotLibNet.Geo.Projections; 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"); ``` ### Projections (13) | 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. ### `GeoPolygonSeries` — render any closed ring ```csharp // Choropleth: colour countries by a metric. var ax = Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.WithProjection(new Robinson())); foreach (var (country, value) in countryData) { var poly = NaturalEarth110m.Countries.First(c => c.Properties["NAME"] == country); ax.AddSeries(new GeoPolygonSeries(poly, new Robinson()) { FillColor = ColorMaps.Viridis.GetColor(value / maxValue), EdgeColor = Colors.White, EdgeWidth = 0.3, }); } ``` ### `NaturalEarth110m` — embedded reference data The package bundles GeoJSON for the [Natural Earth 110m](https://www.naturalearthdata.com/) cultural and physical datasets. There is no HTTP fetch, no download and no external dependency. | Property | Contents | |---|---| | `NaturalEarth110m.Coastlines` | World coastline rings (134 features) | | `NaturalEarth110m.Countries` | World countries with `Properties["NAME"]`, `["ISO_A2"]`, etc. (177 features) | ### Convenience extensions ```csharp .WithProjection(IGeoProjection) // Sets axes coordinate transform .Coastlines(projection, color, lineWidth) // Adds Natural Earth 110m coastlines .Borders(projection, color, lineWidth) // Adds Natural Earth 110m country borders .Ocean(projection, color) // Fills entire map background .Land(projection, color) // Fills land masses ``` 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 choropleth or custom-shape rendering.