Skip to content

Interactive Controls

xkqg edited this page Apr 19, 2026 · 3 revisions

Interactive Controls

Native chart controls for Avalonia 12 and Uno Platform (WinUI 3) with 10 interaction modifiers — no browser, no WebView, no SignalR required.

All listed interactions are enabled in one switch (FigureBuilder.WithBrowserInteraction() in browser hosts; the Avalonia / Uno / WPF controls' IsInteractive property in native hosts). The library decides which scripts/handlers to wire per chart and which to omit. There is no per-feature toggle for the user to manage.

Interaction Gestures (v1.4.1, legend drag added v1.7.2 Phase S)

Gesture Modifier What it does
Left drag (none) Pan axes
Scroll wheel (none) Zoom in/out
Right drag (on 3D axes) Rotate 3D camera
Ctrl + left drag Rectangle zoom — draw box, release zooms
Shift + left drag Brush select — draw rectangle, fires event
Alt + left drag Span select — horizontal X-range selection
Double-click Reset axes to auto-scale
Arrow keys (on 3D axes) Rotate ±5° azimuth/elevation
Home (on 3D axes) Reset camera to default (30°, -60°)
Home / Escape (on 2D axes) Reset axes
Click legend item Toggle series visibility (or Enter / Space for keyboard)
Press-and-hold legend item, drag, release Reposition the legend group (v1.7.2 Phase S; client-only, lost on server re-render)
Hover Show nearest-point tooltip
Mouse move (passive) Crosshair lines at data coordinates

Quick start

Avalonia

dotnet add package MatPlotLibNet.Avalonia
<local:MplChartControl Figure="{Binding MyFigure}" IsInteractive="True" />

Uno Platform

dotnet add package MatPlotLibNet.Uno
<local:MplChartElement Figure="{x:Bind MyFigure}" IsInteractive="True" />

Architecture

The managed interaction layer lives entirely in the zero-dependency core package (MatPlotLibNet). Platform controls translate native pointer/keyboard events into neutral PointerInputArgs / ScrollInputArgs / KeyInputArgs records, then delegate to InteractionController.

Native event (Avalonia / Uno / WinUI)
    │
    ▼
AvaloniaInputAdapter / UnoInputAdapter   ← platform-specific, ~30 lines each
    │
    ▼
InteractionController                    ← core, composes 6 modifiers
    │
    ▼
IInteractionModifier (first match wins)  ← core, one per gesture
    │
    ▼
FigureInteractionEvent                   ← core, same hierarchy as SignalR path
    │
    ▼
evt.ApplyTo(figure)  →  InvalidateVisual()   ← local mode (default)
    or
Action<FigureInteractionEvent> sink           ← server mode (opt-in)

Controller

InteractionController is the central dispatcher. Two factory methods:

Factory Mode Use case
CreateLocal(figure, layout) Local In-process mutation, control repaints automatically
Create(figure, layout, sink) Custom Forward events to SignalR, logging, or any Action<FigureInteractionEvent>

Modifiers (priority order)

# Modifier Gesture Event Guard
1 LegendToggleModifier Click on legend item LegendToggleEvent Hit-test in legend bounds
2 ResetModifier Double-click or Home / Escape ResetEvent ClickCount >= 2 or key match
3 BrushSelectModifier Shift + left-drag BrushSelectEvent Button == Left && Shift
4 PanModifier Left-drag (no Shift) PanEvent Button == Left && !Shift
5 ZoomModifier Scroll wheel ZoomEvent Scroll inside plot area
6 HoverModifier Mouse move, no button HoverEvent Button == None in plot area

First matching modifier wins. Modifiers 1–4 are mutually exclusive (only one captures at a time). Modifiers 5–6 are passive (process independently).

ChartLayout

ChartLayout converts pixel positions to data-space coordinates. Built from ChartRenderer.ComputeLayout() after each render pass.

Method Purpose
HitTestAxes(pixelX, pixelY) Which subplot contains the point?
PixelToData(pixelX, pixelY, axesIndex) Convert pixel → data coordinates
GetDataRange(axesIndex) Current axis limits (XMin, XMax, YMin, YMax)
HitTestLegendItem(pixelX, pixelY, axesIndex) Which legend entry was clicked?

Event hierarchy

FigureInteractionEvent (abstract)
├── AxisRangeEvent (abstract, sealed ApplyTo)
│   ├── ZoomEvent        ← sets axis limits
│   └── ResetEvent       ← restores original limits
├── PanEvent             ← translates axis limits by delta
├── LegendToggleEvent    ← flips series visibility
└── FigureNotificationEvent (abstract, sealed no-op ApplyTo)
    ├── BrushSelectEvent ← rectangular selection (fire-and-forget)
    └── HoverEvent       ← data-space position (fire-and-forget)

Mutation events (ZoomEvent, PanEvent, ResetEvent, LegendToggleEvent) modify the figure and trigger a repaint. Notification events (BrushSelectEvent, HoverEvent) observe without mutating — ApplyTo is sealed empty.

Custom event sink (server mode)

For SignalR-connected charts, construct the controller with a custom sink:

var ctrl = InteractionController.Create(figure, layout, evt =>
{
    hubConnection.SendAsync("ApplyEvent", evt);
});

Or use the built-in extension method:

chartControl.WithServerInteraction(hubConnection);

Rubber-band visual

During a Shift+left-drag (brush select), a semi-transparent blue rectangle is drawn on the chart surface showing the current selection area. The rectangle is rendered directly on the Skia canvas after the chart, using the BrushSelectState exposed by the controller.

Hover tooltip

When hovering over data in an interactive chart, a tooltip appears showing the nearest data point's series name and coordinates. The NearestPointFinder searches all visible series within a configurable pixel distance (default: 20px) and returns the closest match.

Platform details

Avalonia (MplChartControl)

  • Inherits Control; renders via ICustomDrawOperationISkiaSharpApiLeaseFeature.Lease().SkCanvas
  • AffectsRender<MplChartControl>(FigureProperty) — auto-invalidates on Figure change
  • Layout update marshalled to UI thread via Dispatcher.UIThread.Post()
  • Targets .NET 10 + .NET 8; requires Avalonia 12.* with Skia backend

Uno (MplChartElement)

  • Inherits SKCanvasElement; RenderOverride(SKCanvas, Size) hands the canvas directly
  • DependencyProperty for Figure and IsInteractive
  • Layout update marshalled via DispatcherQueue.TryEnqueue()
  • Targets Windows 10 19041+, Android 21+, iOS 15+, macCatalyst 15+

See also

Clone this wiki locally