Skip to content

Commit ef7c4dd

Browse files
fix(reconciler): templated ListView/GridView stale-closure refresh + feat(charting): rich Element labels with Canvas anchors (#160)
* fix(reconciler): templated ListView/GridView refresh realized containers on parent re-render; handle GridViewItem in refresh path Two related correctness bugs in the templated list-view update path. 1. Stale viewBuilder closures on parent re-render `UpdateTemplatedListView` / `UpdateTemplatedGridView` skipped refreshing realized containers when the items reference was unchanged: if (o.ItemCount != n.ItemCount) lv.ItemsSource = …; else if (!n.SameItemsAs(o)) // ReferenceEquals(Items, x.Items) RefreshRealizedContainers(…); // skipped when items unchanged The `SameItemsAs` shortcut assumed `viewBuilder` was a pure function of `(item, index)`. In practice viewBuilder is a closure that legitimately captures outer state — `count` from `UseState`, theme tokens, mode flags, etc. With the shortcut in place, the surrounding controls (header, sibling elements) refreshed on parent re-render but list rows stayed frozen at their first-render closure values. Repro: a list whose viewBuilder reads an outer `count`. Header text shows `count: 6`; rows show `[count: 0]` indefinitely. Fix: drop the shortcut. Always call `RefreshRealizedContainers` when the item count is unchanged. Per-item cost is bounded by the leaf-level `ShallowEquals` fast-path inside `Update`, so unchanged subtrees still short-circuit cheaply. Removed the now-unused `SameItemsAs` abstract + three implementations and their unit-test coverage. 2. GridView containers were silently dropped from refresh `RefreshRealizedContainers` cast `ContainerFromIndex(i) as ListViewItem`. For `GridView`, the container is a `GridViewItem`, so the cast returned `null` and every iteration `continue`d — refresh was a no-op for GridView. Both `ListViewItem` and `GridViewItem` derive from `SelectorItem`; cast to that instead so both controls share the refresh path. This bug never surfaced because (a) item-count changes hit the `ItemsSource =` reset path that bypasses `RefreshRealizedContainers` entirely, and (b) the now-removed `SameItemsAs` shortcut prevented the cast from being exercised on most renders. Discovered by the new GridView fixture below. ## Tests - New selftest fixtures (Reactor.AppTests.Host/SelfTest/Fixtures/ TemplatedListHighlightTests.cs): - TemplatedListView_ViewBuilderClosureRefreshes — proves rows reflect a closure-captured `count` after a sibling state change with stable items reference. - TemplatedGridView_ViewBuilderClosureRefreshes — same contract for GridView, also exercises the SelectorItem cast fix. - Removed three unit-test assertions on the now-deleted `SameItemsAs` method (MoreCoverageTests2.cs). - Full suite: 6797 unit, 652 selftest, all passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(charting): rich Element labels for axis ticks and pie slices; Canvas anchor support Adds three new chart customization APIs that accept a Reactor Element instead of a string, plus the Canvas anchor primitive they require. ## New chart APIs - ChartElement<T>.XTickLabelView(Func<double, Element>) — replace the built-in numeric X-axis tick label with a caller-supplied Element. Anchored horizontally centered on the tick mark. - ChartElement<T>.YTickLabelView(Func<double, Element>) — same for the Y axis. Right-anchored to the axis edge, vertically centered on the tick. - PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>) — replace the built-in text slice label. Centered on the slice centroid; the original string LabelAccessor remains the source of truth for accessibility, so screen-reader summaries keep working. PieSliceLayout exposes Index, Value, Fraction, CentroidX/Y, StartAngle/ EndAngle (radians, clockwise from 12 o'clock), Inner/OuterRadius, and the resolved palette Color, so label authors can render shape/color/percent without re-deriving slice geometry. All Element labels are rendered with IsHitTestVisible=false and AccessibilityView.Raw so they don't pollute the UIA tree — the chart's existing IChartAccessibilityData remains the canonical accessible representation. D3Axes() picks up two new optional parameters (xTickLabel/yTickLabel) that swap the built-in TextBlock for the supplied factory; behavior is unchanged when both are null. ## Canvas anchor positioning Rich labels need to position centered on a tick or centroid without knowing their rendered size at construction time. The Canvas attached property gained two anchor knobs: - Canvas(left, top, anchorX, anchorY) — anchorX/Y are 0..1 fractions of the element's rendered size. 0,0 = top-left (legacy), 0.5,0.5 = centered on (left, top), 1,1 = bottom-right. - CenterAt(x, y) — sugar for Canvas(x, y, 0.5, 0.5). Implementation: ApplyCanvasPosition() in Reconciler.cs falls back to plain Canvas.SetLeft/SetTop when anchor is (0,0) — zero overhead for existing callsites. When an anchor is set, per-FE state is held in a ConditionalWeakTable, the element subscribes to Loaded + SizeChanged once, and Canvas.Left/Top are recomputed as `target - anchor * ActualWidth/Height` on every layout pass. Updates that swap the anchor reuse the existing subscriptions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skills): add reactor-charts skill covering the chart DSL and the new *View extension points skills/charts.md is targeted at AI agents helping with chart customization. It covers the four chart factories (LineChart, BarChart, AreaChart, PieChart) briefly, then concentrates on what isn't obvious from the public guide: when the string-label APIs aren't enough and the user needs to reach for PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>) ChartElement<T>.XTickLabelView(Func<double, Element>) ChartElement<T>.YTickLabelView(Func<double, Element>) added in this same PR. Documents the PieSliceLayout fields, the auto-applied defensive defaults (IsHitTestVisible=false, AccessibilityView.Raw), and the a11y rule that callers MUST keep the string LabelAccessor / DataLabel set so the chart's IChartAccessibilityData stays the canonical UIA description even when the visual is replaced. Also briefly cross-references the underlying Canvas anchor primitive (.CenterAt, .Canvas(left, top, anchorX, anchorY)). Format mirrors the existing skills (perf-tips.md, input.md, etc.) — concise, scannable, with a "when to reach for it / when not to" framing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skills): expand charts skill with chart-choice, D3, label, and a11y guidance; link from SKILL.md skills/charts.md now covers, in order: (1) choosing the right chart for the data, (2) the DSL and *View extension points, (3) D3 scales/axes pitfalls, (4) direct labeling vs legend vs tooltip, (5) a11y beyond the framework defaults, (6) the most-cited visualization mistakes to refuse to ship. Drawn from the standard visualization literature — Cleveland & McGill's perceptual ranking (1984), Tufte's data-ink ratio and chartjunk, Few's practitioner playbook, Cairo's How Charts Lie pitfalls chapter, Wilke's Fundamentals of Data Visualization, and the W3C WAI / Tenon accessibility guidance for charts. Each rule is a one-line heuristic an AI agent can scan quickly. The table at the top maps "use when / avoid when" for the four chart types so the agent has an immediate answer when a user asks for the wrong chart. Specifically called out: - Pie chart guard rails (4 conjoint conditions; default to sorted bar). - Truncated baselines on bar charts (always anchor at zero). - Dual y-axes (don't; use small multiples or index to 100). - Color-as-sole-channel (WCAG 1.4.1; pair with shape/pattern/label). - Direct labeling beats legends when feasible — exactly what the new *View methods enable, so the skill ties the technique to the framework feature. SKILL.md sub-skills table gains a charts row so the root skill's index points agents at this. Packaging is automatic — release.yml's "Assemble skill kit" step does Copy-Item -Recurse skills $stage, so any new skills/*.md is included in reactor-skill-kit-<version>.zip without further changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(skills): cover donut, TreeChart, ForceGraph in charts skill The previous version of skills/charts.md only listed the four "primary" chart factories (Line/Bar/Area/Pie) and missed three that ship in the framework: - Donut: actually a parameter on PieChart (`.InnerRadius(r)` for r > 0), not a separate factory. Worth calling out because the inner-hole label pattern is a common ask and `InnerRadius` is non-obvious from the factory signature. - TreeChart (`Charts.Tree.cs`): top-level factory for hierarchical data — org charts, file trees, taxonomies. Has its own ChartElement, fluent surface, and accessibility data. - ForceGraph (`Charts.Tree.cs`): top-level factory for relationship networks. Same shape. Also added a paragraph noting the D3 layout primitives that aren't yet wrapped in a factory — Sankey, Treemap, Cluster, Stratify — so the agent knows where to look when a user asks for one of these and can correctly suggest "compose from D3 primitives, or file a factory request" rather than claim it doesn't exist. Frontmatter description and SKILL.md table row updated to mention the broader catalog so the chart-shape question routes here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(devtools,docs): unbreak preview-capture pipeline after security hardening Two regressions from #102 (security hardening) compounded to wedge `mur docs compile` for any topic with screenshots — Phase 3 timed out waiting for the capture port and the app silently exited. PreviewCaptureServer.Start: the TASK-026 TOCTOU fix kept a TcpListener bound on the loopback port until HttpListener.Start() succeeded. The kernel only lets one socket own the port, so HttpListener.Start() threw SocketException; the exception escaped OnLaunched, was logged to NullLogger.Instance (the default _logger before any logger is wired), and the process exited. Stop the placeholder TcpListener before binding HttpListener — the resulting Stop->Start TOCTOU is microseconds wide on loopback and not a meaningful local-attack surface. PreviewCaptureServer.ServeFrame: the TASK-025 idle-stop optimization queued Start() and Stop() on the dispatcher for every /frame request. A serial-polling client (the docs CLI) toggled _activeReaders 0->1->0 fast enough that Start and Stop landed on the dispatcher back-to-back and cancelled out before any tick fired — the timer never produced a single frame. Drop the idle-stop; keep lazy-start. The CPU cost of a 5-10 fps PrintWindow loop on an idle preview window is negligible, and the alternative is a request-overlap dance the docs pipeline can't do. ScreenshotCapture (CLI): the bearer-token plumbing added on the server side (TASK-018) was never wired into the docs-pipeline client. Read CAPTURE_TOKEN= from app stdout alongside CAPTURE_PORT= and send Authorization: Bearer ... on every /preview and /frame request. Also poll /frame until a non-empty body lands (capture timer starts lazily, so the first call returns 204) and warm the timer before the manifest loop so the first screenshot doesn't pay the startup latency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gallery): disable horizontal scroll on sample detail page Without HorizontalScrollBarVisibility=Disabled, the no-wrap source-code TextBlock propagates its full unwrapped width up through both ScrollViewers and inflates the page-level StackPanel past the visible viewport. That stretched the chart-card Border with it, and HAlign=Center then placed each sample's chart at the middle of the inflated width — visually far off to the right of the window. ScrollMode.Disabled only suppresses the gesture; the visibility flag is what clamps measure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(charting): cover the new Element-label extension points Adds a "Custom Label Elements" section to the charting guide, framed as an advanced opt-in: most charts only need the string label APIs (AxisLabel, LabelAccessor, DataLabel); reach for *View when plain text isn't enough — icon-plus-text ticks, multi-line labels with mixed typography, or rendering a slice's percent inside the slice itself. Two focused subsections with their own snippets: - PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>) — PieLabelViewDemo renders the slice percentage inside the slice. The string LabelAccessor is still passed so screen readers describe the slice; the chart's IChartAccessibilityData stays canonical even when the visual is replaced. - ChartElement<T>.XTickLabelView(Func<double, Element>) + YTickLabelView — AxisTickViewDemo renders month-name X-axis labels with a "month" caption per tick. Closing paragraph documents the auto-applied defensive defaults (IsHitTestVisible=false, AccessibilityView=Raw) so callers don't have to repeat them, and reminds them that the string label is required. Generated docs/guide/charting.md and screenshots via `mur docs compile --topic charting`. Only commits the three new images (pie-label-view, axis-tick-view, accessible-chart); the other six charting screenshots regenerate byte-different but visually identical on every capture (PrintWindow + JPEG re-encode noise) and are reverted to keep history clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: address PR #160 CR feedback — palette consistency, OnMount chaining, virtualized refresh, chart unit tests Five CR concerns fixed; two declined. Pre-OSS so we keep moving — no compat shims kept just to placate the reviewer. ## Fixed 1. **OnMount clobbering on caller-supplied chart labels** (Charts.cs:608, D3Charts.cs:395, D3Charts.cs:411). The chart code wrapped author-supplied `Element`s in `.OnMount(static fe => fe.IsHitTestVisible = false)` to stamp the defensive-default. ElementModifiers stores a single `OnMountAction`, so plain `.OnMount(…)` overwrote any mount hook the caller already had. New `OnMountAdd` extension composes: existing action runs first, then the new one. All three sites now use OnMountAdd. 2. **Palette inconsistency between rendered slices and PieSliceLayout.Color** (Charts.cs RenderLabelViews + D3Charts.cs D3Pie). The label path resolved `_colorPalette ?? D3Color.Category10`, but D3Pie hard-coded `D3Charts.Palette` (also Category10). When a caller used SetColors(...), the labels saw the custom palette while the slices kept the static one, so PieSliceLayout.Color (which label authors lean on) didn't match what was actually rendered. D3Pie now accepts an optional palette parameter and PieChartElement passes the same palette it gave RenderLabelViews. 3. **SetColors(empty) stored an empty palette** (Charts.cs:467). Caller could `chart.SetColors()` (no args) and every downstream consumer would mod-by-zero on `palette[i % palette.Count]`. Empty input now clears the override so the default palette applies; documented why we don't store "no colors" as a meaningful state. 4. **RefreshRealizedContainers was O(ItemCount) on virtualized lists** (Reconciler.Update.cs:2510). The previous loop did `for i in 0..ItemCount: ContainerFromIndex(i)`, which on a 10k-item virtualized ListView meant 10k cross-WinRT lookups per parent re-render and discarded most as null. Now iterates `ItemsPanelRoot.Children` directly — that IS the realized set, so cost is O(realized) ≈ viewport size. Container index is resolved via `IndexFromContainer` instead of driving the loop with a guessed index. Snapshots Children before the inner Update calls (which can mount new controls and would throw on live-collection enumeration). 5. **No unit-test coverage for the new *View APIs / PieSliceLayout / SetColors** (Charts.cs:142, 491, 603 — three duplicate review comments). Added six tests in ChartsBuilderTests.cs covering builder fluency for `XTickLabelView` / `YTickLabelView` / `LabelView`, `PieSliceLayout` field surface, and the empty-palette behavior. Visual end-to-end coverage stays in the selftest fixtures (Charts construct WinRT brushes during ToElement, which can't run on the unit-test thread). ## Declined - **Restore SameItemsAs as an obsolete shim** — the framework isn't shipped yet, the protected/abstract method has no external derivers, and an Obsolete shim would just be dead weight. Hard-removed. - **AnchorX/AnchorY look like a no-op** — stale review. ApplyCanvasPosition reads them and is wired into both Mount (Reconciler.Mount.cs:1025) and Update (Reconciler.Update.cs:1202). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * samples(charting): two demos for the new *View extension points - WeeklyForecastSample: 14-day temperature forecast where both axis tick labels are Reactor Elements via XTickLabelView / YTickLabelView. Each X tick is a stacked '№ / day' label; each Y tick pairs the temperature with a color swatch that warms with the value. - BrowserSharePieSample: PieChart.LabelView demo — slice labels render the browser logo + percentage centered on each slice's centroid. Both samples ride alongside the existing chart gallery entries and register through SampleRegistry. GalleryHelpers.GallerySample.IconName becomes virtual so the new samples can override it where needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reconciler,devtools): structural AccessibilityModifiers compare; sprite-leak in highlight overlay Two fixes that surfaced together while investigating "the entire chart lights up as modified" when only one descendant property changed in step-08 (PieChart with LabelView rendering Component<ListItem,ItemProps>). ## 1. ModifiersEqual: AccessibilityModifiers must compare by value (#167 follow-up) `Element.ModifiersEqual` was checking the `Accessibility` slot via `ReferenceEquals`. Every fluent helper (`.AccessibilityView`, `.LiveRegion`, `.ItemStatus`, `.HelpText`, …) routes through `ModifyA11y`, which allocates a fresh `AccessibilityModifiers` per call — so the reference is never equal across renders even when the values are identical. Cascaded into: - the Update fast-path skip failing for any element with an accessibility modifier (charts, axis ticks, slice labels), and - the reconcile-highlight overlay flagging those elements as "modified" every render even though `OwnPropsEqual` correctly returned true (Components/Func/Memo are pure wrappers). Fixed by comparing the record structurally (it's all scalar/string fields). Added 5 regression tests in `ReconcilerCorrectnessTests`. ## 2. ReconcileHighlightOverlay: sprite cleanup leak The fade animation cleanup in `Show()` removed sprites whose `Opacity <= 0.001f`. But Composition keyframe animations drive the *rendered* value without writing back to the static property — `sprite.Opacity` keeps reading 0.17 even after the fade completes. The predicate was effectively always false, so sprites accumulated in `_container.Children` for the life of the session. Two consequences: - Memory leak (one container, growing without bound). - Visual stacking: at 80ms flush cadence and 600ms fade, ~7 sprite generations stacked at the same target position. Repeated 17%-yellow composition over the same pixel approaches saturated yellow film over the slice color underneath, which read as the chart "fading". Fixed by tracking each batch's sprites in a per-flush `List<SpriteVisual>` and removing exactly those when `batch.Completed` fires — no introspection of an unreliable property. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6d9fd00 commit ef7c4dd

32 files changed

Lines changed: 1359 additions & 89 deletions

SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ re-renders automatically. No XAML. No data binding. No ViewModels.
2525
| [`skills/navigation.md`](skills/navigation.md) | Multi-page apps, sidebar/tab navigation, routes, deep linking, page transitions, caching. `UseNavigation`, `NavigationHost`, `NavigationView`, `TabView`. |
2626
| [`skills/forms.md`](skills/forms.md) | Data-entry screens, validation, masked/formatted input. `UseValidationContext`, `FormField`, `MaskEngine`, `InputFormatter`. |
2727
| [`skills/input.md`](skills/input.md) | Gestures, pointer events, drag-and-drop, focus management. `OnPan`, `OnPinch`, `OnRotate`, `OnDragStarting`, `UseElementFocus`. |
28+
| [`skills/charts.md`](skills/charts.md) | Data visualization — choosing a chart type (incl. donut, `TreeChart`, `ForceGraph`), the chart DSL, the `LabelView` / `XTickLabelView` / `YTickLabelView` extension points for icon-plus-text and rich labels, plus the visualization-best-practices rules to refuse to break. |
2829
| [`skills/dsl-reference.md`](skills/dsl-reference.md) | Look up signatures — every factory, modifier, and enum in the DSL. |
2930

3031
## Project Setup

docs/_pipeline/apps/charting/App.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,73 @@ public override Element Render()
193193
}
194194
// </snippet:dynamic-data>
195195

196+
/// <summary>
197+
/// Demonstrates <see cref="PieChartElement{T}.LabelView"/> — replace the built-in
198+
/// text slice label with any Element. The string LabelAccessor is still passed so
199+
/// screen readers describe the slice correctly.
200+
/// </summary>
201+
class PieLabelViewDemo : Component
202+
{
203+
public override Element Render()
204+
{
205+
var data = new CategoryData[]
206+
{
207+
new("Engineering", 42), new("Sales", 25),
208+
new("Marketing", 18), new("Support", 15)
209+
};
210+
211+
return VStack(12,
212+
SubHeading("Pie LabelView"),
213+
// <snippet:pie-label-view>
214+
// Percent rendered inside the slice. The string label accessor
215+
// is still passed so screen readers describe the slice.
216+
PieChart(data, d => d.Value, d => d.Name)
217+
.Title("Team Distribution")
218+
.Width(300).Height(300)
219+
.InnerRadius(50).PadAngle(0.02)
220+
.LabelView((d, layout) =>
221+
TextBlock($"{layout.Fraction:P0}")
222+
.FontSize(12).Bold().Foreground("White"))
223+
// </snippet:pie-label-view>
224+
).Padding(24);
225+
}
226+
}
227+
228+
/// <summary>
229+
/// Demonstrates <see cref="ChartElement{T}.XTickLabelView"/> — replace the numeric
230+
/// X-axis tick label with any Element.
231+
/// </summary>
232+
class AxisTickViewDemo : Component
233+
{
234+
public override Element Render()
235+
{
236+
var data = new SalesPoint[]
237+
{
238+
new(1, 120), new(2, 180), new(3, 150),
239+
new(4, 220), new(5, 310), new(6, 280)
240+
};
241+
242+
string[] months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun" };
243+
244+
return VStack(12,
245+
SubHeading("Axis XTickLabelView"),
246+
// <snippet:axis-tick-view>
247+
// X axis ticks: render month name plus a caption per tick.
248+
LineChart(data, d => d.Month, d => d.Revenue)
249+
.Title("Revenue by Month")
250+
.SeriesName("Revenue")
251+
.Width(600).Height(220)
252+
.Stroke("#0078D4").StrokeWidth(2.5)
253+
.ShowGrid(true).ShowAxes(true)
254+
.XTickLabelView(t => VStack(2,
255+
TextBlock(months[Math.Clamp((int)t - 1, 0, months.Length - 1)])
256+
.FontSize(11).SemiBold(),
257+
TextBlock("month").FontSize(8).Opacity(0.6)))
258+
// </snippet:axis-tick-view>
259+
).Padding(24);
260+
}
261+
}
262+
196263
// <snippet:accessible-chart>
197264
/// <summary>
198265
/// Canonical accessible chart pattern — demonstrates all recommended accessibility
@@ -251,6 +318,8 @@ public override Element Render()
251318
Component<PieChartDemo>(),
252319
Component<CombinedChartDemo>(),
253320
Component<DynamicDataDemo>(),
321+
Component<PieLabelViewDemo>(),
322+
Component<AxisTickViewDemo>(),
254323
Component<AccessibleChartDemo>()
255324
).Padding(24)
256325
);

docs/_pipeline/apps/charting/doc-manifest.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ screenshots:
3535
component: DynamicDataDemo
3636
region: client
3737
format: png
38+
- id: pie-label-view
39+
description: "Pie chart with percent rendered inside each slice via LabelView"
40+
component: PieLabelViewDemo
41+
region: client
42+
format: png
43+
- id: axis-tick-view
44+
description: "Line chart with month-name axis ticks via XTickLabelView"
45+
component: AxisTickViewDemo
46+
region: client
47+
format: png
3848
- id: accessible-chart
3949
description: "Accessible chart with screen-reader annotations and keyboard navigation"
4050
component: AccessibleChartDemo

docs/_pipeline/templates/charting.md.dt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,52 @@ diffs the old and new element trees and patches only what changed:
123123
For high-frequency updates (60fps streaming), use `OnReady` to get a
124124
handle that exposes the underlying `Canvas` for direct manipulation.
125125

126+
## Custom Label Elements
127+
128+
Most charts only need the string-based label APIs (`AxisLabel`,
129+
`LabelAccessor`, `DataLabel`). Reach for the `*View` extensions only when
130+
plain text isn't enough — for icon-plus-text ticks, multi-line labels with
131+
mixed typography, or rendering a slice's percent inside the slice itself.
132+
133+
### Pie slice labels
134+
135+
`LabelView` replaces the built-in text label on each pie slice. The delegate
136+
receives the slice's data item plus a `PieSliceLayout` describing its
137+
geometry, and returns any Element:
138+
139+
```csharp snippet="charting/pie-label-view"
140+
```
141+
142+
![Pie chart with the percent rendered inside each slice](screenshot://charting/pie-label-view)
143+
144+
`PieSliceLayout` exposes the slice's `Index`, `Value`, `Fraction`,
145+
`CentroidX`/`CentroidY`, `StartAngle`/`EndAngle`, `InnerRadius`/`OuterRadius`,
146+
and the resolved palette `Color` so a label can echo slice geometry without
147+
recomputing it.
148+
149+
### Axis tick labels
150+
151+
`XTickLabelView` and `YTickLabelView` replace the numeric tick labels with
152+
any Element. Each delegate receives the tick's domain value:
153+
154+
```csharp snippet="charting/axis-tick-view"
155+
```
156+
157+
![Line chart with month-name axis ticks](screenshot://charting/axis-tick-view)
158+
159+
| Method | Purpose |
160+
|--------|---------|
161+
| `PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>)` | Replace the built-in slice text with any Element, anchored on the slice centroid |
162+
| `ChartElement<T>.XTickLabelView(Func<double, Element>)` | Replace the X-axis tick label, horizontally centered on the tick |
163+
| `ChartElement<T>.YTickLabelView(Func<double, Element>)` | Replace the Y-axis tick label, right-anchored to the axis edge |
164+
165+
The element you return is auto-anchored — you don't need a known size at
166+
construction time, and the chart re-positions on layout. It is also rendered
167+
non-interactive and hidden from the UIA tree, so the chart's structured
168+
accessibility description (see below) stays canonical. Always keep the
169+
string `LabelAccessor` (pie) or `DataLabel` (line/bar/area) set when you use
170+
a `*View` override; that's what screen readers read.
171+
126172
## Chart Accessibility
127173

128174
Charts are fully accessible out of the box. Add `.Title()` and

docs/guide/charting.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,72 @@ class DynamicDataDemo : Component
272272
For high-frequency updates (60fps streaming), use `OnReady` to get a
273273
handle that exposes the underlying `Canvas` for direct manipulation.
274274

275+
## Custom Label Elements
276+
277+
Most charts only need the string-based label APIs (`AxisLabel`,
278+
`LabelAccessor`, `DataLabel`). Reach for the `*View` extensions only when
279+
plain text isn't enough — for icon-plus-text ticks, multi-line labels with
280+
mixed typography, or rendering a slice's percent inside the slice itself.
281+
282+
### Pie slice labels
283+
284+
`LabelView` replaces the built-in text label on each pie slice. The delegate
285+
receives the slice's data item plus a `PieSliceLayout` describing its
286+
geometry, and returns any Element:
287+
288+
```csharp
289+
// Percent rendered inside the slice. The string label accessor
290+
// is still passed so screen readers describe the slice.
291+
PieChart(data, d => d.Value, d => d.Name)
292+
.Title("Team Distribution")
293+
.Width(300).Height(300)
294+
.InnerRadius(50).PadAngle(0.02)
295+
.LabelView((d, layout) =>
296+
TextBlock($"{layout.Fraction:P0}")
297+
.FontSize(12).Bold().Foreground("White"))
298+
```
299+
300+
![Pie chart with the percent rendered inside each slice](images/charting/pie-label-view.png)
301+
302+
`PieSliceLayout` exposes the slice's `Index`, `Value`, `Fraction`,
303+
`CentroidX`/`CentroidY`, `StartAngle`/`EndAngle`, `InnerRadius`/`OuterRadius`,
304+
and the resolved palette `Color` so a label can echo slice geometry without
305+
recomputing it.
306+
307+
### Axis tick labels
308+
309+
`XTickLabelView` and `YTickLabelView` replace the numeric tick labels with
310+
any Element. Each delegate receives the tick's domain value:
311+
312+
```csharp
313+
// X axis ticks: render month name plus a caption per tick.
314+
LineChart(data, d => d.Month, d => d.Revenue)
315+
.Title("Revenue by Month")
316+
.SeriesName("Revenue")
317+
.Width(600).Height(220)
318+
.Stroke("#0078D4").StrokeWidth(2.5)
319+
.ShowGrid(true).ShowAxes(true)
320+
.XTickLabelView(t => VStack(2,
321+
TextBlock(months[Math.Clamp((int)t - 1, 0, months.Length - 1)])
322+
.FontSize(11).SemiBold(),
323+
TextBlock("month").FontSize(8).Opacity(0.6)))
324+
```
325+
326+
![Line chart with month-name axis ticks](images/charting/axis-tick-view.png)
327+
328+
| Method | Purpose |
329+
|--------|---------|
330+
| `PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>)` | Replace the built-in slice text with any Element, anchored on the slice centroid |
331+
| `ChartElement<T>.XTickLabelView(Func<double, Element>)` | Replace the X-axis tick label, horizontally centered on the tick |
332+
| `ChartElement<T>.YTickLabelView(Func<double, Element>)` | Replace the Y-axis tick label, right-anchored to the axis edge |
333+
334+
The element you return is auto-anchored — you don't need a known size at
335+
construction time, and the chart re-positions on layout. It is also rendered
336+
non-interactive and hidden from the UIA tree, so the chart's structured
337+
accessibility description (see below) stays canonical. Always keep the
338+
string `LabelAccessor` (pie) or `DataLabel` (line/bar/area) set when you use
339+
a `*View` override; that's what screen readers read.
340+
275341
## Chart Accessibility
276342

277343
Charts are fully accessible out of the box. Add `.Title()` and
65.3 KB
Loading
48.9 KB
Loading
99.2 KB
Loading

samples/ReactorCharting.Gallery/GalleryHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ public abstract class GallerySample
1818
public abstract Element Render();
1919

2020
/// <summary>SVG icon filename (without extension), derived from class name.</summary>
21-
public string IconName => GetType().Name.Replace("Sample", "");
21+
public virtual string IconName => GetType().Name.Replace("Sample", "");
2222
}
Lines changed: 6 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)