# Advanced --- ## Date Axes ![Date axis with auto ticks](images/date_axis.png) Plot time-series data with automatic tick placement. The `DateTime[]` overload sets the X axis to `AxisScale.Date` automatically. ```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", … ``` The same call works for 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` chooses the tick interval from years, months, weeks, days, hours, minutes, and seconds. To set the locator and formatter yourself: ```csharp ax.SetXTickLocator(new AutoDateLocator()) .SetXTickFormatter(new AutoDateFormatter()); ``` --- ## Math Text Labels ![Math text labels with Greek letters and super/subscript](images/math_text.png) You can use a small subset of LaTeX syntax in any title, axis label, annotation, or legend entry. Wrap the 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) fall back to plain-text Unicode substitution. Math text also supports accents (`\hat`, `\bar`, `\tilde`, `\dot`, `\vec`), font variants (`\mathrm`, `\mathbf`, `\mathit`, `\mathcal`, `\mathbb`), `\text{}`, spacing (`\,`, `\:`, `\;`, `\quad`, `\qquad`), and 96 symbol mappings that cover Greek letters, math operators, arrows, set and logic symbols, and blackboard bold, for example ℝ, ℂ, ℤ, ℕ, ℚ, →, ⇒, ←, ↔, ∀, ∃, ∈, ∉, ∪, ∩. ![MathText — fractions, sqrt, accents, font variants](images/math_text_v130.png) --- ## Constrained Layout Constrained layout computes the figure margins from the measured text extents instead of using 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"); ``` It measures the Y-tick label widths, the axis label sizes, and the subplot title heights, then computes exact `SubPlotSpacing` margins and clamps them to sensible ranges. Without either flag, the fixed defaults apply (`MarginLeft=60`, `MarginBottom=50`, …). ### Outside legends ![Outside legend — right margin reserved](images/legend_outside.png) Four `LegendPosition` values place the legend box *outside* the plot area: `OutsideRight`, `OutsideLeft`, `OutsideTop`, `OutsideBottom`. The constrained-layout engine measures the legend box with `LegendMeasurer`, which uses the same formulas as the renderer at draw time, and widens the matching figure margin by the box width plus a 16 px gap. The `[10, 140]` right-margin clamp is dynamic: for `OutsideRight` it rises to at least `legendBoxWidth + 40`, so a wide legend never clips. ```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()` or `ConstrainedLayout()` the legend clips at the figure edge, because the margin reservation pass only runs when one of those flags is set. ### Label collision avoidance Dense pies, sunbursts, Sankeys, and bar charts can stack their labels on top of each other. [`LabelLayoutEngine`](https://github.com/xkqg/MatPlotLibNet/blob/main/Src/MatPlotLibNet/Rendering/Layout/LabelLayoutEngine.cs) is an iterative pair-wise repulsion solver that spreads them apart: - It computes each label's bounding rectangle with `ChartServices.FontMetrics`. That is the same provider the renderer uses, so layout and drawing agree byte for byte. - For each overlapping pair, it applies a **minimum-translation vector (MTV)** shift along the axis with the smaller overlap, weighted by each label's priority. - It clamps every label rectangle back inside the plot bounds on each pass. - It stops at convergence or after 20 iterations. - When a label has moved more than `leaderThreshold` pixels from its original anchor, it records a **leader-line anchor** so the caller can draw a connector back with `CalloutBoxRenderer.DrawLeaderLine(ctx, anchor, finalPoint, color)`. `PieSeriesRenderer`, `SunburstSeriesRenderer`, `SankeySeriesRenderer`, and `BarSeriesRenderer` use the engine automatically. `TreemapSeriesRenderer` uses a per-cell measured-fit check instead: each label is constrained to its own cell rectangle, so labels in different cells cannot collide. You can also call the engine directly: ```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 output is a GIF89a file with a NETSCAPE2.0 loop extension. Colors are quantized to a uniform 252-color palette (a 6×7×6 RGB cube), and each frame's data is LZW-compressed. There is no FFmpeg dependency. --- ## 3-D Charts — Camera, Lighting, and Rotation 3-D charts render as SVG using a perspective camera with depth-sorted polygons. ### Cross-series depth composition Every 3-D axes runs one shared depth queue (`DepthQueue3D`) for all 3-D series on that axes. Each series (`Bar3D`, `PlanarBar3D`, `Surface3D`, and the others) pushes its drawable primitives into the queue as closures, each with a centroid depth. After all series have rendered, the axes renderer sorts the queue back to front once and invokes the closures in that order. Matplotlib's `ax.bar3d` sorts each call's faces independently, so there the draw order can visibly break the compositing. Here the insertion order does not 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 `_shade_colors` formula: `k = 0.65 + 0.35 · dot(n̂, l̂)`, mapping the signed dot product from `[−1, 1]` to `[0.3, 1.0]`. There is no Lambertian `max(0, dot)` clamp: a back-facing face darkens to 0.3× its base brightness instead of going to zero, and its hue is preserved. ### 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: `SvgRenderContext` and `SkiaRenderContext` both delegate to `ChartServices.FontMetrics`, which delegates in turn to `SkiaFontMetrics` (bundled DejaVu Sans glyph widths), so there is no font-fallback drift. - **Glyph rendering** in SVG emits text as `` elements with real glyph outlines from Skia, which matches matplotlib's `svg.fonttype='path'`, so the text renders identically whatever fonts are installed. - **Figure-level spacing** (`TightLayout` / `ConstrainedLayout`) runs once via a shared `ChartRenderer.PrepareSpacing` helper used by both the SVG and PNG transform paths. ### 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` is the camera tilt in degrees (default 30°). - `azimuth` is the camera rotation in degrees (default −60°). - `distance` is the perspective distance and must be at least 2.0. Omit it for an orthographic projection. ### Lighting ```csharp ax.WithLighting(ambient: 0.4, diffuse: 0.6, dirX: 1.0, dirY: 0.5, dirZ: -1.0); ``` This is Lambertian shading with an ambient and a diffuse term. Face normals are computed per quad, and the shaded color replaces the series fill color on each face. ### Interactive SVG Rotation ```csharp Plt.Create() .AddSubPlot(1, 1, 1, ax => ax.Surface3D(x, y, z)) .With3DRotation() .Save("interactive.svg"); ``` `With3DRotation()` embeds an inline JavaScript block (`Svg3DRotationScript`). Dragging with the mouse recomputes the elevation and azimuth and re-sorts the polygons by depth. On the keyboard, the arrow keys rotate the view by ±5° and Home resets it to the initial view. There is no framework dependency: the chart 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 a script that dims every link not reachable upstream or downstream from the hovered node. This matches ECharts' `focus: adjacency`. The emphasis is keyboard-accessible through `tabindex="0"` and focus/blur events. Every Sankey node rect carries a `data-sankey-node-id` attribute, and every link path carries `data-sankey-link-source` and `data-sankey-link-target`. The script uses those attributes to run a breadth-first search (BFS) in both directions from the hovered node and dims everything outside 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(); ```