# 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 v1.11.2 includes **79 series types** across 14 categories, plus 4 streaming series, 13 map projections, and geographic polygon rendering via `MatPlotLibNet.Geo`. See the [Geographic / Maps](#geographic--maps--matplotlibnetgeo) section near the bottom for the full geo surface.
---
## Gallery
Each of these renders is produced by the MatPlotLibNet fidelity suite and diffed pixel-for-pixel against a matplotlib reference. Click any tile to open the full-resolution PNG.
| | | |
|---|---|---|
| [](images/gallery_kde.png) **KDE** (Gaussian kernel density) | [](images/gallery_violin.png) **Violin** (3 groups) | [](images/gallery_box.png) **Box** (4 groups) |
| [](images/gallery_hexbin.png) **Hexbin** (2-D binning) | [](images/gallery_stem.png) **Stem** (sampled signal) | [](images/gallery_waterfall.png) **Waterfall** (finance) |
| [](images/gallery_sankey.png) **Sankey** (flow) | [](images/gallery_radar.png) **Radar** (5 axes) | [](images/gallery_streamplot.png) **Streamplot** (vector field) |
| [](images/gallery_polar_scatter.png) **Polar scatter** | [](images/gallery_treemap.png) **Treemap** (squarified) | [](images/gallery_pie.png) **Pie** (with labels) |
Every one of these chart types is available via a single fluent call — see the sections below for the builder signatures.
---
## Basic

### 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
For data ≥ 100 k points. Uses O(1) viewport slicing (uniform) or O(log n) binary search (non-uniform) + LTTB downsampling. Zero gen-2 GC allocation on the render path.
```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

`FigureTemplates.FinancialDashboard` produces a 3-panel layout (price 60 %, volume 15 %, oscillator 25 %) with grid lines on every panel, 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 over the Bollinger middle band) because both draw their input from the underlying OHLC series; chained indicators that want to operate on a prior indicator's output pass the output array explicitly.
### 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
```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
**Twelve 3-D series** with perspective camera, per-face lighting, numeric tick marks along the bounding-box edges, and interactive SVG rotation. 3-D axes share a single cross-series depth queue — multiple 3-D series on the same axes composite correctly regardless of the order in which you add them.
```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

Matplotlib's `ax.bar3d(...)` equivalent — rectangular prisms rising from the XY plane with matplotlib-exact per-face shading (`0.65 + 0.35·dot(n̂, l̂)`). Each bar has six shaded faces; the renderer depth-sorts all faces from all bars so occlusion is correct regardless of camera angle. **Add multiple `Bar3D` calls on one axes** for a stacked grid — the shared 3-D depth queue handles compositing:

```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

Matplotlib's `ax.bar(xs, heights, zs=y, zdir='y')` equivalent — also known as a **"skyscraper plot"** or *"2D bars in different planes"*. Each bar is a **single flat translucent rectangle** in the XZ plane at a fixed Y value (no cuboid, no shading), so you can read bars through the rear planes. Great for comparing many time-series or categorical distributions 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 (same convention as `ScatterSeries.Colors` / `PieSeries.Colors`). When set, it overrides the per-series `Color` on a per-bar basis — enabling three colour lookup modes 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
The sample below highlights every bar at `x = 0` in dark cyan, regardless of which plane it sits on:

```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();
});
```
No callbacks, no new API surface — one `Color[]?` array handles all three modes. See [[Advanced]] for the full multi-series depth-compositing story.
### Camera, lighting, and rotation
Camera, lighting, Z-axis label/range, and interactive 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` configure the Z-axis just like `SetXLabel` / `SetYLabel` do on 2-D charts — they write to the `Axes.ZAxis` (`Axis3D`) model property. `With3DRotation()` embeds JS for mouse-drag and keyboard rotation (arrow keys + Home reset). See [[Advanced]] for more.
> **Note on builder placement (v1.1.4+)** — call `WithCamera` / `WithLighting` inside the `AddSubPlot(..., ax => ax...)` lambda so they apply to the actual 3-D subplot. Calling them at the `Plt.Create().WithCamera(...)` level only configures the (unused) figure-level default axes on multi-subplot figures.
### Line3D — projected polyline (v1.3.0)

3-D polyline through arbitrary `(x, y, z)` points. Segments are depth-sorted and projected through the `Projection3D` pipeline.
```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)

Takes unstructured `(x, y, z)` point clouds and generates a triangulated surface mesh via Delaunay triangulation. Each triangle face is depth-sorted and shaded via `Vec3.FaceNormal` + `Color.Shade()` extensions.
```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)

Computes contour lines on a grid using marching squares, then projects them into 3-D space. Each contour sits at its corresponding 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)

Draws arrows in 3-D space. Each arrow has a shaft (line segment) and a cone head. Useful for visualising 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)

Renders a `bool[,,]` voxel grid as face-culled cubes. Adjacent filled voxels suppress shared faces, reducing draw count. All 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)

Places text labels at arbitrary `(x, y, z)` positions. The 3-D coordinate is projected to 2-D pixel space and the text is drawn at the projected position.
```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
Hierarchical-clustering visualisation rendered as the canonical "U"-shape segments — every internal node is a *merge* whose vertical position equals the merge distance (carried in `TreeNode.Value`). Matches SciPy's `scipy.cluster.hierarchy.dendrogram` rendering convention.
```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`), matching SciPy's `dendrogram(color_threshold=…)` visual convention. Single-leaf trees render the lone label at the plot centre; all-zero merge distances collapse the U-shapes to the leaf baseline (a unit `maxMerge` fallback avoids division by zero). See the [cookbook entry](../docs/cookbook/dendrograms.md) for a full walkthrough.
### Clustermap
Composites a heatmap with optional row and column dendrograms into a single subplot — the seaborn `sns.clustermap` idiom. When trees are provided, rows and/or columns are automatically reordered to match the dendrogram leaf traversal order so cells align visually with the tree structure.
```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();
```
Leaf `TreeNode.Value` must be the zero-based original index of the row or column. Internal nodes carry the merge distance. Malformed trees (out-of-range indices, duplicates, wrong count) fall back to identity order silently. See the [cookbook entry](../docs/cookbook/clustermap.md) for a full walkthrough.
### Pair Grid
N×N matrix of subplots from N variables — the seaborn `pairplot` idiom. Diagonal cells show
univariate distributions (histogram or KDE); off-diagonal cells show bivariate scatters of
`(i, j)`. Optional hue groups colour the off-diagonal scatters by category — the killer
feature for cluster validation and category-aware EDA.
```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 are stacked-overlapping per hue group with `Alpha = 0.6`; KDE renders
one curve per group. Off-diagonal scatter dot radius is controlled by `MarkerSize`.
`OffDiagonalKind = None` produces a diagonal-only "marginals" view; `OffDiagonalKind = Hexbin`
renders flat-top hexagonal density grids for high-cardinality EDA where scatter overplots
(activated in v1.10; hue is ignored when Hexbin is active). Cell gutters (`CellSpacing`,
default `0.02`) are clamped to `[0, 0.2]`.
See the [cookbook entry](../docs/cookbook/pairplot.md) for a full walkthrough.
### Treemap drilldown (v1.1.4, rewritten in v1.7.2 Phase P, default flipped in v1.7.2 Phase W)

**Steady-pictures UX (v1.7.2 Phase W).** The initial interactive view is pixel-identical to the static SVG — every node at every depth (incl. depth-3 and beyond) is visible on first paint. Z-order means children paint OVER parents, so the deepest visible label is what the user reads in any overlapping region. Clicking a parent rectangle now *collapses* its entire subtree (transitively, via an ancestry-walk visibility model — descendants' own collapse state is preserved); click again to restore. Multiple subtrees can be collapsed independently. Leaves are not clickable.
> **Why default-expanded?** Earlier (Phase P) the script started with only depth-1 visible and required clicks to expand — but that meant entering interactive mode visually shifted the chart (the root header strip went blank), and the user had to click to discover content. Phase W flipped this: the user sees everything by default ("when no browserInteraction he sees all" applies in both modes), and clicks become an opt-in focus gesture instead of a discovery requirement.
Keyboard-accessible via `tabindex="0"` and ARIA roles. The interactive script is embedded in the SVG output when `WithTreemapDrilldown()` is called on the `FigureBuilder`, and every rect is tagged with `data-treemap-node` / `-depth` / `-parent` so the script can navigate the hierarchy without re-parsing the structure. For static SVG output with deep trees, call `.WithAutoSize(root)` so the canvas is big enough to fit every label cleanly without overflow.
```csharp
Plt.Create()
.WithTreemapDrilldown()
.AddSubPlot(1, 1, 1, ax => ax.Treemap(catalogue, s => s.ShowLabels = true))
.Save("treemap_drilldown.svg");
```
### Nested pie (v1.1.4)

A two-level `TreeNode` rendered as an inner pie + outer breakdown ring. Convenience wrapper around `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
Nodes-and-edges-in-2D for correlation networks (Pearson edge weights), lead-lag flow
(TransferEntropy directed edges), Louvain community visualisation (node colour = community
ID), and minimum spanning trees. Three deterministic layouts ship in v1.10 PR 1
(`Manual` / `Circular` / `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();
```
`IsDirected` edges get an arrowhead at the target end (reuses `ArrowHeadBuilder.FancyArrow`).
`EdgeThicknessScale` multiplies per-edge `Weight` to stroke width;
`NodeRadiusScale` multiplies per-node `SizeScalar` to circle radius.
DataFrame extension: `df.NetworkGraph("source", "target", weightCol: "weight",
directedCol: "directed")` derives nodes from the union of source/target columns.
### Relative Rotation Graph (RRG)
JdK-style 2D scatter of (RS-Ratio, RS-Momentum) per asset relative to a benchmark,
with a fading tail per asset and a 100/100 quadrant grid (Leading / Weakening / Lagging / Improving).
Three RS formulas ship: `DualEma` (default, trending markets), `ZScore` (mean-reversion),
`LogReturn` (high-volatility). Introduced in v1.11.0; 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

v1.1.4 rewrote the Sankey renderer around an 8-step pipeline: explicit column assignment, node alignment, iterative vertical relaxation, gradient link colour modes, sub-labels, inside-labels, hover emphasis, and a full vertical orientation. Five new samples ship in `images/sankey_*.png`:
- **`sankey_process_distribution.svg`** — 5-column process-industry cascade with tonnage sub-labels, gradient links, hover emphasis enabled (hover a node to isolate its reachable flow chain)
- **`sankey_income_statement.svg`** — J&J Q1 FY25 income statement with semantic green/red node colouring and "+2% Y/Y" sub-label deltas
- **`sankey_customer_journey.svg`** — 4-timestep alluvial where `Home` legitimately reappears in multiple columns (uses explicit `SankeyNode.Column` override to pin each node to its semantic timestep regardless of link topology)
- **`sankey_un_expenses.svg`** — 2-column baseline, outside labels, clean `HideAllAxes()` canvas
- **`sankey_severity_cascade.svg`** — 4-column patient severity state transitions with 24 relaxation iterations minimising crossings in a dense many-to-many topology
- **`sankey_vertical.svg`** — vertical orientation (top-to-bottom flow) 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` | Secondary label drawn one line below at 80 % font size; useful for "$13.9B +2%" metric/delta pairs |
| `SankeyNode.Column` | Explicit column override; pins node to a semantic column regardless of BFS topology (alluvial / time-step Sankeys) |
| `SankeySeries.NodeAlignment` | `Justify` (default) / `Left` / `Right` / `Center` — matches D3 `sankey*` modes |
| `SankeySeries.Iterations` | Vertical relaxation passes (default 6); higher 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

```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");
```
---
## Geographic / Maps — `MatPlotLibNet.Geo`
The `MatPlotLibNet.Geo` package adds **13 map projections**, the **`GeoPolygonSeries`** (renders any closed ring as a styled polygon — choropleth, ocean fill, land fill, custom shapes), and **embedded Natural Earth 110m data** (coastlines + countries, no external download required).
```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` and round-trip through `Forward(latitude, longitude)` → `ProjectedPoint` and `Inverse(x, y)` → `GeoCoordinate?` (v1.13.0: record structs, were tuples — `readonly record struct ProjectedPoint(double X, double Y)` / `readonly record struct GeoCoordinate(double Latitude, double Longitude)`; deconstruction call sites like `var (x, y) = proj.Forward(lat, lon);` are unaffected). `Bounds` returns the existing `GeoBounds(XMin, XMax, YMin, YMax)` record struct (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
Bundled GeoJSON for the [Natural Earth 110m](https://www.naturalearthdata.com/) cultural and physical datasets. No HTTP fetch, no download step, no external dependencies.
| 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 extension method is built on `GeoPolygonSeries` — the lowest-level primitive — so you can mix the convenience helpers with hand-rolled `GeoPolygonSeries` for choropleth or custom-shape rendering in the same axes.