Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions docs/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ active one as application resources, so switching theme repaints every component
| `Destructive`, `Success`, `Warning`, `Info` (+ `*Foreground`) | Status variants (badge, alert, progress, labels) |
| `Border`, `Input`, `Ring` | Dividers, field borders, focus ring |
| `Overlay` | Dialog / drawer / sheet backdrop |
| `Chart1` … `Chart5` | Chart series, in order (ShellUI's `--chart-1` … `--chart-5`) |

**Use the tokens in your own XAML** — each token is published as a Color (`ShellUI<Token>`) and a
Brush (`ShellUI<Token>Brush`):
Expand Down Expand Up @@ -112,6 +113,7 @@ Same names, properties and variants as MAUI, except:
| Carousel | Swipe, arrows, dots | Drag with the mouse or touch; the arrows are tab stops and Left / Right move while one has focus; with `AspectRatio="0"` set `Height` |
| Stepper | Shows the active step's view | Every step stays in the tree and only the active one is visible; reached steps are tab stops (Enter / Space) |
| Pagination | Labels hidden on phones | `ShowLabels` defaults to true; every page and arrow is a tab stop (Enter / Space) |
| Charts | Drawn with `GraphicsView`; on touch a tap or a drag shows the tooltip; `HeightRequest` | Drawn in `Render`, in the control's own font; `Height`; `ChartSeries.Color` and the token colors are Avalonia `Color`s (`Color.Parse("#E76E50")`) |
| ImageViewer | `Source` is an `ImageSource`; `Aspect`; pinch zooms | `Source` is an `IImage` (a resource path such as `/Assets/photo.jpg`); `Stretch` instead of `Aspect`; the wheel and the + / - / 0 keys zoom; the thumbnail and the controls are tab stops |
| PrevNextNav | Tap a card | The cards are also tab stops (Enter / Space) |
| ChatInput | The send button sends | Also Enter sends and Shift+Enter starts a new line |
Expand Down Expand Up @@ -1468,6 +1470,121 @@ Bordered callout with an icon. **Properties:** `Title`, `Message`, `Variant` (De

---

## Charts

Bar, line, area, pie, donut, radar and radial charts. They are drawn by the components
themselves (MAUI's `GraphicsView`, Avalonia's `Render`), so they need no charting package, and
they take their colors from the theme: `Chart1` to `Chart5`, in order. Every chart has a legend,
a tooltip under the pointer (or the finger) and a grow-in animation.

```bash
shellui-native add bar-chart line-chart area-chart pie-chart radar-chart radial-chart
```

Each chart installs `chart`, the shared core. The chart colors are new theme tokens: a project
whose `shell` came from an older CLI version needs `shellui-native update shell` first.

### Data

A chart's children are its series; `Labels` names the categories (or the slices of a pie). In
XAML both are comma-separated lists.

**ChartSeries:** `Name` (shown in the legend and tooltip), `Values`, `Color` (replaces the
theme color for that series)

```xml
<ui:BarChart Labels="Jan, Feb, Mar, Apr, May, Jun" HeightRequest="260">
<ui:ChartSeries Name="Desktop" Values="186, 305, 237, 73, 209, 214" />
<ui:ChartSeries Name="Mobile" Values="80, 200, 120, 190, 130, 140" />
</ui:BarChart>
```

From code:

```csharp
Sales.Labels = new ChartLabels(months);
Sales.Series.Add(new ChartSeries { Name = "Desktop", Values = new ChartValues(desktopVisits) });
```

Adding or removing a series, or setting a series' `Values`, `Name` or `Color`, redraws the chart.
After changing numbers inside an existing `Values` list, call `Refresh()`.

**Every chart:** `Labels`, `ShowLegend` (true), `ShowTooltip` (true), `IsAnimated` (true),
`ValueFormat` (a .NET number format; `#,0.##`), `EmptyText` ("No data"), `Description` (for
screen readers) · **Methods:** `Refresh()`, `PlayAnimation()`

**Bar, line and area charts also have:** `ShowGrid`, `ShowXAxis`, `ShowYAxis` (all true),
`Minimum`, `Maximum` (the ends of the value axis; by default they fit the data and include zero)

Give a chart a height (`HeightRequest`, or `Height` on Avalonia).

### Bar Chart
One bar per series for each category. **Properties:** `IsStacked`, `IsHorizontal` (bars lie
down; negative values go the other way), `CornerRadius` (4), `MaxBarSize` (56)

```xml
<ui:BarChart Labels="Jan, Feb, Mar" IsStacked="True" HeightRequest="260"> ... </ui:BarChart>
```

### Line Chart
One line per series, from edge to edge of the plot. **Properties:** `IsSmooth` (true; false
joins the points with straight segments), `ShowDots`, `StrokeWidth` (2)

### Area Chart
A line per series with the space under it filled. **Properties:** `IsSmooth` (true),
`IsStacked`, `FillOpacity` (0.4), `StrokeWidth` (2)

### Pie Chart and Donut Chart
The first series' values as slices, clockwise from twelve o'clock, named by `Labels` and colored
from the chart colors in order. The tooltip shows a slice's value and its share. `DonutChart` is
a pie with a hole. **Properties:** `InnerRadius` (0 for a pie, 0.6 for a donut), `Gap` (degrees
between slices; 1), `ShowLabels` (the value outside each slice), `CenterValue`, `CenterLabel`
(text in a donut's hole)

```xml
<ui:DonutChart Labels="Chrome, Safari, Firefox" CenterValue="662" CenterLabel="Visitors" HeightRequest="260">
<ui:ChartSeries Values="275, 200, 187" />
</ui:DonutChart>
```

### Radar Chart
One spoke per category around a ringed grid, and a filled shape per series. It needs at least
three categories. **Properties:** `Maximum` (the value at the outer ring), `FillOpacity` (0.5),
`ShowDots`

### Radial Chart
The first series' values as concentric rings on a muted track, each filling clockwise towards
`Maximum`; the first value is the outer ring. **Properties:** `Maximum` (a full ring; by default
the largest value), `InnerRadius` (0.35), `IsRounded` (true), `CenterValue`, `CenterLabel`

```xml
<ui:RadialChart Labels="Move, Exercise, Stand" Maximum="100" CenterValue="72%" CenterLabel="Move" HeightRequest="260">
<ui:ChartSeries Values="72, 48, 90" />
</ui:RadialChart>
```

### Colors
The chart colors are theme tokens, so they follow light and dark mode and can be replaced like
any other token, before the first page loads:

```csharp
ShellTheme.Light[ShellToken.Chart1] = Color.FromArgb("#E76E50");
ShellTheme.Dark[ShellToken.Chart1] = Color.FromArgb("#2662D9");
```

Past the fifth series (or slice) the colors repeat, fainter. A series' own `Color` wins over the
theme.

### What the charts don't do
Zooming and panning, mixing bars and lines in one chart, click events on a bar or slice, and very
large data sets (every point is drawn). For those, a charting library such as LiveCharts sits
fine next to ShellUI Native: color it from `ShellTheme.Get(ShellToken.Chart1)`.

To draw a chart of your own, derive from `ChartBase` and draw in `DrawChart` with the
`IChartCanvas` it hands you; the legend, tooltip and animation come with the base class.

---

## Overlay Components

### Dialog, Drawer, Sheet
Expand Down
9 changes: 5 additions & 4 deletions docs/COMPONENTS_ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Prioritized list of components to create for ShellUI Native, aligned with [Shell

Last revised: **2026-10-11** (second prerelease, `0.1.0-alpha.2`).

**Status at a glance:** P0–P6 ✅ done · P7 done except chart · `0.1.0-alpha.2`: 63 families, 94 CLI targets, each for MAUI and Avalonia · since then: button-group, input-group, menubar, command, navigation-menu, data-table, chat, form, image-viewer, prev-next-nav.
**Status at a glance:** P0–P7 ✅ done · `0.1.0-alpha.2`: 63 families, 94 CLI targets, each for MAUI and Avalonia · since then: button-group, input-group, menubar, command, navigation-menu, data-table, chat, form, image-viewer, prev-next-nav, charts.

---

Expand Down Expand Up @@ -202,7 +202,7 @@ Icons: `python scripts/generate-icons.py` regenerates `Icon.cs` from the ShellIc
### P7 — Optional (Charts, Pickers, etc.)
| Component | Priority | Notes |
|-----------|----------|-------|
| chart (line, bar, pie, area) | P7 | Consider external chart lib integration |
| **chart** ✅ | P7 | Done 2026-10-13, MAUI and Avalonia — drawn natively with no library: bar, line, area, pie / donut, radar and radial charts on a shared `chart` core (axes, legend, tooltip, animation), colored from five new theme tokens |
| combobox ✅ | P7 | Done 2026-10-04 — Select with a filter field, Enter picks the first match |
| **file-upload** ✅ | P7 | Done 2026-10-11, MAUI and Avalonia — drop zone, platform file picker, list of picked files |
| input-otp ✅ | P7 | Done 2026-10-04 — slots over one hidden field (paste / autofill / numeric keyboard) |
Expand Down Expand Up @@ -297,8 +297,9 @@ resizable, navbar, sidebar, date-range-picker and file-upload on both platforms
input-group ✅ and menubar ✅ (2026-10-11), then command ✅ (with a command palette) and
navigation-menu ✅, then data-table ✅ (client-side filter, sort, selection and pages;
2026-10-12), then chat ✅ and form ✅, then image-viewer ✅ and prev-next-nav ✅ (2026-10-13;
73 families, 104 CLI targets).
73 families, 104 CLI targets), then the charts ✅ (bar, line, area, pie / donut, radar,
radial; 80 families, 111 CLI targets).

**Backlog:** chart (P7). qr-code waits, like the icon switch, for NuGet package dependencies in the
**Backlog:** qr-code waits, like the icon switch, for NuGet package dependencies in the
CLI: it needs a QR encoder. Not in the native data-table yet: row actions and server-side
paging.
17 changes: 13 additions & 4 deletions docs/DEVELOPMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ main ← Phase 1 (PRs #1–#5), release 0.1.0-alpha.1 (PR #6), Phase 2 Avaloni
layout, navigation and pickers on both platforms (PR #14) merged
release 0.1.0-alpha.2 (PR #15), CI action bumps (PR #16), button-group,
input-group and menubar (PR #17), command and navigation-menu (PR #18),
data-table (PR #19), chat and form (PR #20) merged
└─ feat/image-viewer-prev-next ← image-viewer and prev-next-nav (active)
data-table (PR #19), chat and form (PR #20), image-viewer and prev-next-nav (PR #21)
merged
└─ feat/charts ← charts drawn natively, no library (active)
└─ feat/winui ← Phase 3 (conditional)
```

Expand All @@ -38,8 +39,8 @@ main ← Phase 1 (PRs #1–#5), release 0.1.0-alpha.1 (PR #6), Phase 2 Avaloni

| Branch | Base | Status | Purpose |
|--------|------|--------|---------|
| `main` | — | Phase 1, Phase 2 and PR #14 merged; released as `0.1.0-alpha.2`; PRs #17–#20 merged since | 97 registry entries (94 CLI targets, 63 families), each with MAUI and Avalonia content |
| `feat/image-viewer-prev-next` | `main` | **Active** | image-viewer and prev-next-nav for MAUI and Avalonia (107 registry entries, 104 CLI targets, 73 families) |
| `main` | — | Phase 1, Phase 2 and PR #14 merged; released as `0.1.0-alpha.2`; PRs #17–#21 merged since | 97 registry entries (94 CLI targets, 63 families), each with MAUI and Avalonia content |
| `feat/charts` | `main` | **Active** | chart core plus bar, line, area, pie / donut, radar and radial charts for MAUI and Avalonia (114 registry entries, 111 CLI targets, 80 families) |

---

Expand Down Expand Up @@ -539,6 +540,14 @@ Cross-desktop (Windows + macOS + Linux) from one XAML codebase.
- [x] image-viewer and prev-next-nav (`feat/image-viewer-prev-next`): ImageViewer is a
`ShellOverlayHost` whose trigger is its own thumbnail; zoom scales the image, no panning.
With these, the Blazor set is ported except the chart family and qr-code
- [x] Charts (`feat/charts`), drawn natively instead of wrapping a library (decided
2026-10-13: ShellUI on Blazor wraps ApexCharts; on native a library would need NuGet
dependencies in the CLI, would add SkiaSharp, and LiveCharts' Avalonia package targets
Avalonia 11). `chart` holds the series model, the `IChartCanvas` abstraction (lines,
polylines, polygons and text only; curves and arcs are turned into points), axes, legend,
tooltip and animation; bar-chart, line-chart, area-chart, pie-chart (with DonutChart),
radar-chart and radial-chart draw on it. `ShellToken.Chart1`–`Chart5` are new, from ShellUI's
`--chart-1`–`--chart-5`, so an older `shell` needs `shellui-native update shell`
- [ ] Replace the generated `Icon.cs` with the `ShellIcons.Maui` / `ShellIcons.Avalonia` packages
once they are on NuGet. Their names already match (`IconName`, `Icon`, `Kind` on Avalonia), so
components change little: token tinting binds the package's `Color` (MAUI) or `Foreground`
Expand Down
114 changes: 114 additions & 0 deletions examples/Avalonia.Demo/Components/UI/AreaChart.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Media;

namespace AvaloniaDemo.Components.UI;

// Area chart — a line per series with the space under it filled at 40%. Stacked, each
// series sits on the ones before it.
// <ui:AreaChart Labels="Jan, Feb, Mar, Apr, May, Jun" IsStacked="True" Height="260">
// <ui:ChartSeries Name="Desktop" Values="186, 305, 237, 73, 209, 214" />
// <ui:ChartSeries Name="Mobile" Values="80, 200, 120, 190, 130, 140" />
// </ui:AreaChart>
public class AreaChart : CartesianChart
{
// Curves the line through its points; false joins them with straight segments.
public static readonly StyledProperty<bool> IsSmoothProperty =
AvaloniaProperty.Register<AreaChart, bool>(nameof(IsSmooth), true);

// Stacks the series instead of overlapping them.
public static readonly StyledProperty<bool> IsStackedProperty =
AvaloniaProperty.Register<AreaChart, bool>(nameof(IsStacked));

public static readonly StyledProperty<double> FillOpacityProperty =
AvaloniaProperty.Register<AreaChart, double>(nameof(FillOpacity), 0.4);

public static readonly StyledProperty<double> StrokeWidthProperty =
AvaloniaProperty.Register<AreaChart, double>(nameof(StrokeWidth), 2d);

static AreaChart()
{
AffectsRender<AreaChart>(IsSmoothProperty, IsStackedProperty, FillOpacityProperty, StrokeWidthProperty);
}

public bool IsSmooth
{
get => GetValue(IsSmoothProperty);
set => SetValue(IsSmoothProperty, value);
}

public bool IsStacked
{
get => GetValue(IsStackedProperty);
set => SetValue(IsStackedProperty, value);
}

public double FillOpacity
{
get => GetValue(FillOpacityProperty);
set => SetValue(FillOpacityProperty, value);
}

public double StrokeWidth
{
get => GetValue(StrokeWidthProperty);
set => SetValue(StrokeWidthProperty, value);
}

protected override void DrawChart(IChartCanvas canvas, Rect area)
{
var count = CategoryCount;
double min = 0, max = 0;
for (var i = 0; i < count; i++)
{
var values = Series.Select(s => ValueAt(s, i)).ToList();
max = Math.Max(max, IsStacked ? values.Where(v => v > 0).Sum() : values.Max());
min = Math.Min(min, IsStacked ? values.Where(v => v < 0).Sum() : values.Min());
}

var frame = DrawAxes(canvas, area, min, max, bands: false);
var plot = frame.Plot;
var hovered = HoveredIndex;
if (hovered >= 0)
{
// The cursor: a hairline through the hovered category.
var x = Math.Round(frame.Category(hovered)) + 0.5;
canvas.Line(new Point(x, plot.Top), new Point(x, plot.Bottom), ShellTheme.Get(ShellToken.Border), 1);
}

// In a stack each series sits on the running total of the ones before it.
var stack = new double[count];
var dots = new List<(Point At, Color Color)>();
for (var s = 0; s < Series.Count; s++)
{
var color = SeriesColor(s);
var top = new List<Point>();
var bottom = new List<Point>();
for (var i = 0; i < count; i++)
{
var from = IsStacked ? stack[i] : 0;
var to = from + ValueAt(Series[s], i) * Progress;
if (IsStacked) stack[i] = to;
top.Add(new Point(frame.Category(i), frame.Value(to)));
bottom.Add(new Point(frame.Category(i), frame.Value(from)));
}

var upper = IsSmooth ? ChartMath.Smooth(top) : top;
var lower = IsSmooth ? ChartMath.Smooth(bottom) : bottom;
var shape = new List<Point>(upper);
shape.AddRange(Enumerable.Reverse(lower));
canvas.Polygon(shape, ChartColor.Fade(color, FillOpacity));
if (upper.Count > 1) canvas.Polyline(upper, color, StrokeWidth);
if (hovered >= 0 && hovered < top.Count) dots.Add((top[hovered], color));
}

foreach (var dot in dots)
{
canvas.Polygon(ChartMath.Circle(dot.At, StrokeWidth + 4), ShellTheme.Get(ShellToken.Background));
canvas.Polygon(ChartMath.Circle(dot.At, StrokeWidth + 2.5), dot.Color);
}
}
}
Loading
Loading