# Advanced --- ## Date Axes ![Date axis with auto ticks](images/date_axis.png) Plot time-series data with automatic tick placement. The `DateTime[]` overload auto-sets the X axis to `AxisScale.Date`. ```csharp DateTime[] dates = Enumerable.Range(0, 365) .Select(i => new DateTime(2025, 1, 1).AddDays(i)) .ToArray(); double[] values = dates.Select((_, i) => Math.Sin(i / 30.0)).ToArray(); Plt.Create() .Plot(dates, values) .WithXLabel("Date") .Save("dates.svg"); // ticks: "Jan 2025", "Feb 2025", … ``` Sub-day data: ```csharp DateTime[] hours = Enumerable.Range(0, 48) .Select(i => DateTime.Today.AddHours(i)) .ToArray(); Plt.Create().Plot(hours, measurements).Save("hourly.svg"); // ticks: "00:00", "06:00", "12:00", … ``` `AutoDateLocator` selects tick intervals across: Years → Months → Weeks → Days → Hours → Minutes → Seconds. Manual override: ```csharp ax.SetXTickLocator(new AutoDateLocator()) .SetXTickFormatter(new AutoDateFormatter()); ``` --- ## Math Text Labels ![Math text labels with Greek letters and super/subscript](images/math_text.png) Use mini-LaTeX syntax in any title, axis label, annotation, or legend entry. Wrap math in `$...$`. ```csharp Plt.Create() .WithTitle("$\alpha$ vs $\beta$ correlation") .AddSubPlot(1, 1, 1, ax => { ax.WithTitle("R$^{2}$ = 0.97"); ax.SetYLabel("$\sigma$ (Pa)"); ax.SetXLabel("Time $\Delta t$ (ms)"); ax.Plot(x, y); }) .Save("math.svg"); ``` **Quick reference:** | Syntax | Renders as | |---|---| | `$\alpha$` | α | | `$\sigma^{2}$` | σ² | | `$x_{i}$` | xᵢ | | `$\pm$` | ± | | `$\infty$` | ∞ | | `$\leq$` | ≤ | | `$\degree$C` | °C | | `$\frac{a}{b}$` | fraction a/b (stacked) | | `$\sqrt{x}$` | √x with overline bar | | `$\sqrt[3]{x}$` | cube root of x | | `$\hat{x}$` | x̂ (circumflex accent) | | `$\bar{y}$` | ȳ (overline accent) | | `$\vec{v}$` | v⃗ (arrow accent) | | `$\mathrm{sin}$` | sin (roman/upright font) | | `$\mathbf{F}$` | **F** (bold font) | | `$\mathcal{L}$` | ℒ (calligraphic font) | | `$\mathbb{R}$` | ℝ (blackboard bold) | | `$\text{ if }$` | " if " (roman text inside math) | | `$a\quad b$` | a b (em space) | | `$\left(...\right)$` | scaling delimiters | Text outside `$...$` renders as-is. SVG backends emit `` for super/subscripts and stacked `` for fractions. Non-SVG backends (Skia, MAUI) use plain-text Unicode substitution as fallback. > **MathText features:** fractions (`\frac`), square roots (`\sqrt`), accents (`\hat`, `\bar`, `\tilde`, `\dot`, `\vec`), font variants (`\mathrm`, `\mathbf`, `\mathit`, `\mathcal`, `\mathbb`), `\text{}`, spacing (`\,`, `\:`, `\;`, `\quad`, `\qquad`), scaling delimiters (`\left`/`\right`), and 96 symbol mappings (Greek + math operators + arrows + set/logic + blackboard bold) including blackboard bold (ℝ, ℂ, ℤ, ℕ, ℚ), arrows (→, ⇒, ←, ↔), and set/logic operators (∀, ∃, ∈, ∉, ∪, ∩). ![MathText — fractions, sqrt, accents, font variants](images/math_text_v130.png) --- ## Constrained Layout Automatically compute margins from actual text extents instead of hardcoded defaults. ```csharp // TightLayout() and ConstrainedLayout() are equivalent Plt.Create() .TightLayout() .AddSubPlot(2, 2, 1, ax => { ax.SetYLabel("Population (millions)"); ax.Plot(x, y1); }) .AddSubPlot(2, 2, 2, ax => { ax.SetYLabel("GDP ($\times 10^{9}$)"); ax.Plot(x, y2); }) .Save("constrained.svg"); ``` The engine measures Y-tick label widths, axis label sizes, and subplot title heights, then computes exact `SubPlotSpacing` margins clamped to sensible ranges. When neither flag is set, fixed defaults (`MarginLeft=60`, `MarginBottom=50`, …) are used. ### Outside legends ![Outside legend — right margin reserved](images/legend_outside.png) Four new `LegendPosition` values place the legend box *outside* the plot area: `OutsideRight`, `OutsideLeft`, `OutsideTop`, `OutsideBottom`. The constrained-layout engine measures the legend box via the new shared `LegendMeasurer` (the same formulas the renderer uses for draw-time positioning) and widens the corresponding figure margin by the full box width + 16 px gap. The hard `[10, 140]` right-margin clamp is dynamic: it raises to at least `legendBoxWidth + 40` for any `OutsideRight` legend so a 200 px legend never gets clipped by the default ceiling. ```csharp Plt.Create() .WithSize(900, 500) .TightLayout() .AddSubPlot(1, 1, 1, ax => { ax.Plot(x, sin, s => s.Label = "sin(x)"); ax.Plot(x, cos, s => s.Label = "cos(x)"); ax.Plot(x, decay, s => s.Label = "exp(-x/5)·cos(x)"); ax.WithLegend(l => l with { Position = LegendPosition.OutsideRight, Title = "Series" }); }) .Save("legend_outside.svg"); ``` Without `TightLayout()` / `ConstrainedLayout()` the legend would clip at the figure edge — the reservation pass only runs when one of those flags is set. ### Label collision avoidance Dense pies, sunbursts, Sankeys, and bar charts used to stack labels on top of each other for overlapping anchors. v1.1.4 introduces the [`LabelLayoutEngine`](https://github.com/xkqg/MatPlotLibNet/blob/main/Src/MatPlotLibNet/Rendering/Layout/LabelLayoutEngine.cs) — an iterative pair-wise repulsion solver that: - Computes the bounding rectangle of each label via `ChartServices.FontMetrics` (same provider the renderer uses, so layout and draw agree byte-identical). - For each overlapping pair, applies a **minimum-translation vector (MTV)** shift along the smaller-overlap axis, weighted by per-label priority. - Clamps every label rect back inside the plot bounds on each pass. - Terminates at convergence or 20 iterations (whichever comes first). - When a label moves more than `leaderThreshold` pixels from its original anchor, records a **leader-line anchor** so the caller can draw a connector back to the semantic position — `CalloutBoxRenderer.DrawLeaderLine(ctx, anchor, finalPoint, color)`. The engine is used automatically by `PieSeriesRenderer`, `SunburstSeriesRenderer`, `SankeySeriesRenderer`, and `BarSeriesRenderer`. `TreemapSeriesRenderer` uses a per-cell measured-fit check instead (each label is constrained to its own rect, so there's no cross-cell collision to resolve). You can use the engine directly for custom label layouts: ```csharp var candidates = myAnchors.Select(a => new LabelCandidate(a.Point, a.Text, font)).ToList(); var placements = LabelLayoutEngine.Place( candidates, plotBounds, ChartServices.FontMetrics, leaderThreshold: 6.0); foreach (var p in placements) { if (p.LeaderLineStart is { } anchor) CalloutBoxRenderer.DrawLeaderLine(ctx, anchor, p.FinalPoint, Colors.Black); ctx.DrawText(p.Text, p.FinalPoint, p.Font, p.Alignment); } ``` --- ## Layouts ![GridSpec layout](images/gridspec_layout.png) ### GridSpec — Unequal Subplot Sizes ```csharp Plt.Create() .WithGridSpec(2, 2, heightRatios: [2.0, 1.0], widthRatios: [3.0, 1.0]) .AddSubPlot(GridPosition.Single(0, 0), ax => ax.Plot(x, y).WithTitle("Main")) .AddSubPlot(GridPosition.Single(0, 1), ax => ax.Scatter(x, y)) .AddSubPlot(GridPosition.Span(1, 2, 0, 2), ax => ax.Bar(cats, vals)) .Save("gridspec.svg"); ``` ### Shared Axes ```csharp .AddSubPlot(2, 1, 1, ax => ax.ShareX("group1").Plot(x, y1)) .AddSubPlot(2, 1, 2, ax => ax.ShareX("group1").Plot(x, y2)) ``` ### Inset Axes ```csharp ax.Plot(x, y) .AddInset(0.6, 0.6, 0.35, 0.35, inset => inset .Plot(xZoom, yZoom).WithTitle("Detail")); ``` ### Custom Spacing ```csharp Plt.Create() .WithSubPlotSpacing(s => s with { MarginLeft = 80, HorizontalGap = 20 }) .AddSubPlot(1, 2, 1, ax => ax.Plot(x, y)) .AddSubPlot(1, 2, 2, ax => ax.Bar(["A"], [10])) .Save("spacing.svg"); ``` --- ## Animations (Browser / SignalR) ```csharp using MatPlotLibNet.Animation; var animation = new AnimationBuilder(frameCount: 60, frame => Plt.Create() .WithTitle($"t = {frame * 0.1:F1}") .Plot(x, x.Select(v => Math.Sin(v + frame * 0.1)).ToArray()) .Build()) { Interval = TimeSpan.FromMilliseconds(50), Loop = true }; var handle = await Plt.Create().Plot(x, y).Build().ShowAsync(); await handle.AnimateAsync(animation); // Stop after 10 seconds using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await handle.AnimateAsync(animation, cts.Token); ``` --- ## GIF Export Export frames directly to an animated GIF (requires `MatPlotLibNet.Skia`). ```csharp using MatPlotLibNet.Animation; using MatPlotLibNet.Skia; var animation = new AnimationBuilder(60, frame => Plt.Create() .WithTitle($"t = {frame * 0.1:F1} s") .Plot(x, x.Select(v => Math.Sin(v + frame * 0.2)).ToArray()) .Build()) { Interval = TimeSpan.FromMilliseconds(80), Loop = true }; animation.SaveGif("wave.gif"); // save to file byte[] gif = animation.ToGif(); // or get bytes (e.g. for Blazor download) ``` The encoder writes GIF89a with a NETSCAPE2.0 loop extension, 252-color uniform quantization (6×7×6 RGB cube), and LZW-compressed per-frame image data. No FFmpeg dependency. --- ## 3-D Charts — Camera, Lighting, and Rotation MatPlotLibNet renders 3-D charts as SVG using a perspective camera with depth-sorted polygons. ### Cross-series depth composition Every 3-D axes runs a single **shared depth queue** (`DepthQueue3D`) across all 3-D series on that axes. Each series — `Bar3D`, `PlanarBar3D`, `Surface3D`, and the others — pushes its drawable primitives into the queue as closures with a centroid depth. After all series have rendered, the axes renderer sorts the queue back-to-front in one pass and invokes the closures in depth order. **Why this matters:** matplotlib's `ax.bar3d(...)` has a well-known limitation — adding rows in the wrong order (front-to-back instead of back-to-front) visibly breaks compositing because each call sorts only its own faces and draws immediately. MatPlotLibNet lifts that restriction: you can call `ax.Bar3D(...)` or `ax.PlanarBar3D(...)` any number of times in any order and the shared depth queue interleaves the faces from every series correctly before painting. Insertion order doesn't affect the output. ```csharp // These two samples produce the SAME output regardless of loop order — // the shared depth queue handles cross-series sort. Plt.Create() .AddSubPlot(1, 1, 1, ax => { ax.PlanarBar3D(xs, ysRow0, zs0, s => s.Color = Colors.Red); // front ax.PlanarBar3D(xs, ysRow1, zs1, s => s.Color = Colors.Green); ax.PlanarBar3D(xs, ysRow4, zs4, s => s.Color = Colors.Gold); // back }) .Save("planar_bars.svg"); ``` ### Matplotlib face shading 3-D series with `WithLighting(...)` use matplotlib's **exact** `_shade_colors` formula: `k = 0.65 + 0.35 · dot(n̂, l̂)`, mapping the raw signed dot product from `[−1, 1]` to `[0.3, 1.0]`. No Lambertian `max(0, dot)` clamp — back-facing faces darken to 0.3× base brightness instead of going to zero, hue is preserved, and the full shade range matches matplotlib's `art3d._shade_colors` output pixel-for-pixel. ### SVG vs PNG rendering parity A 3-D axes rendered to SVG is byte-identical in layout to the same axes rendered to PNG: - **Text measurement** is unified: both `SvgRenderContext` and `SkiaRenderContext` delegate to `ChartServices.FontMetrics` → `SkiaFontMetrics` (bundled DejaVu Sans glyph widths). No more font-fallback drift. - **Glyph rendering** in SVG: text elements are emitted as `` with real glyph outlines from Skia (matches matplotlib's default `svg.fonttype='path'` behaviour). The SVG renders identically in any viewer regardless of installed fonts. - **Figure-level spacing** (`TightLayout` / `ConstrainedLayout`) runs once via a shared `ChartRenderer.PrepareSpacing` helper called by both the SVG and PNG transform paths. No state mutation on the figure. ### Camera ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => { ax.Surface3D(x, y, z); ax.WithCamera(elevation: 35, azimuth: -55, distance: 8); }) .Save("surface3d.svg"); ``` - `elevation` — camera tilt in degrees (default 30°) - `azimuth` — camera rotation in degrees (default −60°) - `distance` — perspective distance ≥ 2.0 (omit for orthographic) ### Lighting ```csharp ax.WithLighting(ambient: 0.4, diffuse: 0.6, dirX: 1.0, dirY: 0.5, dirZ: -1.0); ``` Uses Lambertian (ambient + diffuse) shading. Face normals are computed per-quad; shaded color replaces the series fill color per face. ### Interactive SVG Rotation ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.Surface3D(x, y, z)) .With3DRotation() .Save("interactive.svg"); ``` Embeds a small inline JS block (`Svg3DRotationScript`). Mouse drag re-computes elevation + azimuth and re-sorts polygons by depth. Keyboard: arrow keys (±5°), Home resets to initial view. No framework dependency — works in any browser or HTML email viewer that allows SVG scripts. --- ## Sankey hover emphasis ![Sankey with hover emphasis](images/sankey_process_distribution.png) `FigureBuilder.WithSankeyHover()` embeds an interactive script that dims every link not reachable upstream or downstream from the hovered node. Matches ECharts' `focus: adjacency` behaviour; keyboard-accessible via `tabindex="0"` + focus/blur events. Every Sankey node rect carries `data-sankey-node-id`, and every link path carries `data-sankey-link-source` / `data-sankey-link-target` — the script walks those attributes to do a BFS in both directions from the hovered node and applies a visual opacity reduction to every element NOT in the reachable set. ```csharp Plt.Create() .WithSankeyHover() // embeds the script .AddSubPlot(1, 1, 1, ax => ax .HideAllAxes() // bare canvas — no cartesian axes .Sankey(nodes, links, s => { s.Iterations = 20; // minimise link crossings s.LinkColorMode = SankeyLinkColorMode.Gradient; })) .Save("sankey.svg"); ``` --- ## Real-Time Charts ### ASP.NET Core + SignalR **Server:** ```csharp builder.Services.AddMatPlotLibNetSignalR(); // Push update anywhere in your app await publisher.PublishSvgAsync("sensor-1", figure); ``` **Blazor client:** ```razor ``` **React client:** ```tsx ``` ### Interactive Popup (no server) ```csharp using MatPlotLibNet.Interactive; var handle = await Plt.Create().Plot(x, y).Build().ShowAsync(); // Recalculate and push update await handle.UpdateAsync(); ```