microsoft/reactor-charts
Reactor charts and data visualization — choosing a chart type (line, bar, area, donut, `TreeChart`, `ForceGraph`), the chart DSL, `LabelView` / `XTickLabelView` / `YTickLabelView` for icon-plus-text and rich labels, and the visualization-best-practices rules. Use when adding charts or designing visualizations.
npx skills add https://github.com/microsoft/microsoft-ui-reactor --skill reactor-charts
The chart surface lives in Reactor.D3.Charts (full guide:
docs/guide/charting.md). This skill is the AI-agent quick reference —
chart-choice rules, the new *View extension points, and the a11y/accuracy
rails you should keep on by default.
Most "my chart looks bad" problems are wrong-chart problems, not styling
problems. Cleveland & McGill's perceptual ranking (1984) — accuracy of
graphical judgment, best to worst:
> position on a common scale > position on non-aligned scales > length /
> direction / angle > area > volume > shading / color saturation
That ordering is why bars and dots win and why pies lose. Translate it into
rules of thumb:
| Chart | Use when | Avoid when |
|---|---|---|
| Bar | Discrete categories, time buckets (Q1..Q4), ranked comparison. Default when in doubt. | (Use a different chart if the data is continuous, e.g. dense time-series — line is clearer.) |
| Line | Continuous data, many x-values, trends/slopes, autocorrelated series (prices, sensor readings). | Few categorical observations — use bar. |
| Area | Cumulative magnitude or parts-of-whole over time. | Series cross frequently — overlap occludes; switch to line. |
| Pie | Parts-of-a-whole and ≤5 slices and large differences and proportions land near recognizable fractions (½, ¼, ⅓). | Anything else. Ranking, change-over-time, negative values, precise comparison — use a sorted bar chart. |
| Donut (PieChart with InnerRadius > 0) | Same conditions as pie. The inner hole gives you a place for a center label (total, current selection) — useful when the chart sits inside a dashboard. | Same as pie. The hole doesn't fix pie's ranking weakness. |
| Tree (TreeChart) | Hierarchical data: org charts, file/folder structures, taxonomies, decision trees. Visualizes parent→child links. | Wide hierarchies (>~30 nodes per level) — the tree gets unreadable; consider a treemap (use the D3.Layout.Treemap primitive — no top-level factory yet). |
| Force graph (ForceGraph) | Relationship networks: dependency graphs, social graphs, knowledge graphs. Shows which nodes cluster. | Hierarchical data — use TreeChart (an explicit layout always reads better when one exists). Static print/snapshot output — force layouts converge differently each run. |
When the user asks for a pie chart, push back unless all four pie conditions
hold. A sorted bar chart usually wins. (Few/Tufte say "almost never pie";
Cairo/Wilke allow the narrow case above. Default to "no" and concede when
the conditions are met.)
Charts not yet exposed as top-level factories: Sankey, Treemap, Cluster
dendrograms, partition/sunburst, stratified hierarchies. The math primitives
exist in src/Reactor.Advanced/Charting/D3/Layout/ (Sankey.cs, Treemap.cs,
Cluster.cs, Stratify.cs, TreeLayout.cs) and can be composed with
D3Canvas + the d3 shape generators. If a user asks for one of these and
isn't comfortable assembling D3 primitives directly, suggest filing a
factory request rather than hand-rolling — the resulting chart will lack
the shared accessibility surface.
LineChart (data, x, y) // continuous line
BarChart (data, x, y) // vertical bars (always anchored at zero — see pitfalls)
AreaChart (data, x, y) // filled area
PieChart (data, value) // slices summing to 100%
PieChart (data, value) // donut variant: chain .InnerRadius(40+)
.InnerRadius(60)
TreeChart (root, children) // hierarchical (org chart, file tree, taxonomy)
ForceGraph(nodes, links) // network graph (dependencies, relationships)
Common fluent knobs (chain-call any subset):
.Title("Monthly Revenue")
.SeriesName("Revenue")
.Units(xUnits: "months", yUnits: "USD")
.AxisLabel(ChartAxisType.X, "Month")
.AxisLabel(ChartAxisType.Y, "Revenue (USD)")
.Width(600).Height(250)
.Stroke("#0078D4").StrokeWidth(2.5)
.Fill("#50C878") // bars / area / pie slice baseline
.ShowGrid(true).ShowAxes(true)
.DataLabel((point, idx) => $"{point.Revenue:C}") // string label per point
.Palette(ChartPalette.Categorical) // Tier 1 curated palette (a11y-scanned)
.ChartBackground("#1E1E1E") // declare active bg → scopes A11Y_CHART_011
AxisLabel text and Title are reserved for *labeling the chart*, not
labeling individual data points or ticks — for those, see the next two
sections.
Charts compose D3-style scales and generators internally. When you reach
under the DSL hood (or hand-roll Canvas drawings), follow these:
LinearScale from [min, max] to [plotLeft, plotRight] is the boundary
between "data" and "pixels". Mixing them is the #1 source of "off by 5px"
bugs.
log for exponential, time for dates, ordinal/band for categories. Linear-
on-exponential hides patterns; log-on-linear inflates them.
.nice() quantitative domains. Axis bounds should round tohuman-friendly numbers (0/100/200, not 13.7/187.4). Pair with .ticks(n)
as a *suggestion*, not a hard count — D3 picks readable intervals.
*Viewelements need gutter. The Bostock margin convention
({top, right, bottom, left}) is the safe default; if you customize tick
labels (next section), measure the longest one and grow the margin.
selection, formatting, alignment, label rotation. Custom tick code is
where charts go to die.
*ViewThree cases the string-label APIs (DataLabel, AxisLabel, LabelAccessor)
can't handle:
weights per fragment.
button, anything Reactor can build).
For these, reach for the *View extensions. They take a render delegate and
substitute its returned Element for the built-in TextBlock at the same
anchor position:
// Pie slice: replace text label with arbitrary element rendered at the slice centroid
PieChart(data, d => d.Value)
.LabelView((d, layout) => HStack(
FontIcon(IconForCategory(d.Category)).FontSize(12),
TextBlock($"{layout.Fraction:P0}").FontWeight(FontWeights.SemiBold)));
// Axis tick: replace numeric tick label with arbitrary element
LineChart(data, x, y)
.XTickLabelView(t => VStack(
TextBlock(MonthName((int)t)).FontSize(11),
TextBlock("month").FontSize(8).Foreground(Theme.SecondaryText)))
.YTickLabelView(t => TextBlock($"${t:N0}").FontFamily("Cascadia Mono"));
PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>)The render delegate receives:
T data — the slice's source data item.PieSliceLayout layout — a readonly record struct with everything youneed to render a label that knows its slice geometry:
Index, Value, Fraction (0..1 of total)CentroidX, CentroidY (absolute canvas coords — already applied viaCenterAt, you don't need to apply them yourself)
StartAngle, EndAngle (radians, clockwise from 12 o'clock — d3 semantics)InnerRadius, OuterRadiusColor — the resolved D3Color from the chart's palette, so your labelcan echo the slice color without a separate lookup.
The returned element is auto-anchored at the centroid via CenterAt. You do
not need a known size at construction time; the reconciler recomputes
position after layout from ActualWidth/ActualHeight.
ChartElement<T>.XTickLabelView(Func<double, Element>) and YTickLabelViewSame pattern for axis ticks. The delegate receives the tick's domain value
(double); X labels are anchored horizontally centered on the tick mark, Y
labels right-anchored to the axis edge and vertically centered.
A label-placement decision tree (Tufte, Cleveland — direct labeling raises
the data-ink ratio and avoids legend ping-pong):
slices on the chart, label bars above the bar. LabelView /
*TickLabelView exist precisely so you can do this without re-templating
the whole chart. Eyes don't ping-pong from line to legend; print works;
screen readers work.
tightly-packed lines, repeating palette across small multiples. Treat it
as a fallback, not a default.
never *instead of* — direct labels or a legend. Tooltips fail print,
screen readers, mobile-tap accuracy, and keyboard navigation.
outside leader lines or get rolled into "Other". Don't label tiny slices
inline — they overlap.
overlapping line endpoints. Label collisions there are worse than a
legend; switch to legend.
*View internally)The *View methods are built on Canvas's anchor extensions
(CanvasExtensions.cs). You'll rarely call them directly, but if you need to
position arbitrary content on a Reactor Canvas without knowing its size at
build time (overlay markers, custom callouts), use these:
.Canvas(left, top, anchorX, anchorY) // 0..1 fractions of rendered size
.CenterAt(x, y) // sugar for anchor (0.5, 0.5)
The reconciler subscribes once to Loaded + SizeChanged per anchored element
and recomputes Canvas.Left/Top as target − anchor × ActualWidth/Height.
Zero-anchor (0, 0) is the legacy fast path with no subscription overhead.
Charts implement IChartAccessibilityData, which exposes axis ranges, units,
point values, and (for pie) slice descriptors via UIA. Don't disable that.
Beyond that, your responsibilities:
pattern, dash style, or — best — a direct text label. Useful for series
identification when the user can't distinguish two of your colors.
ChartPalette.Categorical (Reactor'scurated set; Okabe-Ito-style). Avoid red/green pairings. Avoid rainbow
for ordinal data — use a sequential ramp (Viridis-style) instead.
for text labels. The default chart palette meets this against the
framework's surface tokens; if you override with .Stroke("#…") /
.Fill("#…"), check contrast against Theme.SurfaceBackground.
.Palette(ChartPalette.Categorical) (Tier 1, curated) and .SeriesColors(…)
(Tier 3, your own colors) are both contrast-checked by the a11y scanner
(A11Y_CHART_011). .RawColors(…) (Tier 4) is an escape hatch that bypasses
the check and itself trips the A11Y_CHART_012 info. A pie chart's low-level
.SetColors(…) colors are the slices it actually draws, so they are
scanner-visible too — validated as a Tier-3 custom palette. When a pie sets
both .SetColors(…) and .Palette(…), the rendered .SetColors(…) colors are
the ones scanned.
A11Y_CHART_011 is theme-agnosticby default, so it can only flag a custom color against *either* fixed
light/dark background as an info. Call .ChartBackground("#1E1E1E") (also
takes a D3Color or Windows.UI.Color) to scope the check to the single
background the chart actually renders on — the finding becomes an actionable
warning with a real, contrast-hardened fix, and palettes that are fine on
their actual background stop being flagged.
for screen-reader-only users — a hidden, expandable data table next to
the chart is the pattern Tenon and the W3C WAI accessibility guidance
recommend. Build it from the same data the chart consumed.
focus should announce category + value. The IChartAccessibilityData
surface drives this; if you turn the chart Interactive(false) you lose
it — keep it on unless there's a reason.
*View defaults. Custom labels are non-interactive by default: thechart force-applies AccessibilityView=Raw to the **entire realized
subtree** (not just the outer wrapper) and clears IsTabStop on every inner
control, so no inner peer surfaces to UIA and nothing inside the label joins
the tab order. Prefer keeping the string LabelAccessor (PieChart) or
DataLabel (line/bar/area) set — those feed UIA. When the visual lives
entirely in the *View, pass name: (a Func<T,string> for pie /
Func<double,string> for ticks) so the accessible name matches the visible
label instead of falling back to Slice {n}. Set interactive: true only
when the label has focusable children that must stay reachable — then you
own its accessibility. Signatures:
LabelView(render, name: null, interactive: false),
XTickLabelView(render, name: null, interactive: false),
YTickLabelView(render, name: null, interactive: false).
The most-cited visualization mistakes (Tufte, Few, Cairo, Wilke). If a user
asks for one, push back with the alternative.
truncated baseline visually looks like 300%. Cairo's *How Charts Lie*
spends a chapter on this. Line charts may truncate because shape, not
height, conveys meaning — bars never.
small multiples or normalize both series to an index (e.g. `(value /
baseline) × 100`) so they share a single axis.
"chartjunk." 3D pies are actively misleading — angle distortion changes
the visual proportion of slices.
"Other" or split into small multiples.
variable has order (low/medium/high, age buckets, sentiment), use a
sequential ramp. A categorical palette implies "different kind", not
"different magnitude".
imprecise. A sorted bar chart shows it directly.
*View (and when not to)Reach for it when:
side legend (direct labeling, see above).
fonts/colors the built-in ChartAxis style doesn't match.
Skip it when a string works:
DataLabel((d, i) => d.Value.ToString("C")).Fmt(t)handles short numbers cleanly).
if you need it, file an issue rather than dropping to *View for a
one-property override.
docs/guide/charting.md — full user-facing chart guide.src/Reactor.Advanced/Charting/Charts.cs — ChartElement<T> / PieChartElement<T>fluent API. *View methods sit near the bottom of each class.
src/Reactor.Advanced/Charting/Charts.Tree.cs — TreeChart and ForceGraphfactories + their elements.
src/Reactor.Advanced/Charting/D3Charts.cs — d3 primitives (D3Pie, D3Axes,D3Grid, D3Canvas). D3Axes is where the optional xTickLabel /
yTickLabel delegates plug in.
src/Reactor.Advanced/Charting/D3/Layout/ — composable layout algorithms(Sankey.cs, Treemap.cs, Cluster.cs, Stratify.cs, TreeLayout.cs).
No top-level factory wrapping these yet; they're the building blocks for
one when the time comes.
src/Reactor/Elements/CanvasExtensions.cs — CenterAt and the anchoroverload of Canvas.
External (read these once if charting is new to you):
d3-scale, d3-axis, d3-shape — even when working through Reactor's wrappers.Take microsoft/reactor-charts from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.