| name | reactor-charts |
|---|---|
| description | Reactor charting skill. Covers (1) choosing the right chart for the data, (2) the DSL surface — LineChart, BarChart, AreaChart, PieChart (donut via InnerRadius), TreeChart, ForceGraph — plus the *View extension points that let axis ticks and pie slices render any Reactor Element, (3) D3 scales/axes pitfalls, (4) labeling and a11y rules, and (5) the most-cited visualization mistakes to refuse to ship. Read when a user asks for charts — even if they didn't ask for advice on which chart to use, the answer to "should I use a pie chart for this?" is almost always "no, use a sorted bar chart." |
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/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) // pie color paletteAxisLabel 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:
- Scales map domain → range — never compute pixel positions by hand. A
LinearScalefrom[min, max]to[plotLeft, plotRight]is the boundary between "data" and "pixels". Mixing them is the #1 source of "off by 5px" bugs. - Pick the right scale type for the data. Linear for quantitative, log for exponential, time for dates, ordinal/band for categories. Linear- on-exponential hides patterns; log-on-linear inflates them.
- Always
.nice()quantitative domains. Axis bounds should round to human-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. - Reserve margins. Long y-axis labels and multi-line tick
*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. - Don't reinvent the axis. The built-in axis generator handles tick selection, formatting, alignment, label rotation. Custom tick code is where charts go to die.
Three cases the string-label APIs (DataLabel, AxisLabel, LabelAccessor)
can't handle:
- The label needs an icon next to or inside the text.
- The label needs multi-line text, wrapping, or different colors / weights per fragment.
- The label needs to render a mini sub-tree (badge, sparkline, mini 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"));The render delegate receives:
T data— the slice's source data item.PieSliceLayout layout— areadonly record structwith everything you need 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 resolvedD3Colorfrom the chart's palette, so your label can 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.
Same 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):
- Direct labels first. Label series at the end of each line, label pie
slices on the chart, label bars above the bar.
LabelView/*TickLabelViewexist 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. - Use a legend only when direct labeling would collide — many series, tightly-packed lines, repeating palette across small multiples. Treat it as a fallback, not a default.
- Use a tooltip for precise values on dense data, in addition to — never instead of — direct labels or a legend. Tooltips fail print, screen readers, mobile-tap accuracy, and keyboard navigation.
- Pie slices: large slices get inside-labels; small slices (<~5%) need outside leader lines or get rolled into "Other". Don't label tiny slices inline — they overlap.
- Backfire cases: dense scatterplots, tightly-packed bars, many overlapping line endpoints. Label collisions there are worse than a legend; switch to legend.
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:
- Color is never the sole channel (WCAG 1.4.1). Pair color with shape, pattern, dash style, or — best — a direct text label. Useful for series identification when the user can't distinguish two of your colors.
- Color-blind-safe palettes. Use
ChartPalette.Categorical(Reactor's curated set; Okabe-Ito-style). Avoid red/green pairings. Avoid rainbow for ordinal data — use a sequential ramp (Viridis-style) instead. - Contrast. WCAG 1.4.11 — 3:1 for non-text essential graphics, 4.5:1
for text labels. The default chart palette meets this against the
framework's surface tokens; if you override with
.Stroke("#…")/.Fill("#…"), check contrast againstTheme.SurfaceBackground. - Screen-reader fallback. UIA structured data is good but not enough 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.
- Keyboard nav. Data points should be focusable in reading order, and
focus should announce category + value. The
IChartAccessibilityDatasurface drives this; if you turn the chartInteractive(false)you lose it — keep it on unless there's a reason. *Viewdefaults. Custom labels are auto-stamped withIsHitTestVisible=falseandAccessibilityView=Rawso they don't duplicate the structured UIA description. Always keep the stringLabelAccessor(PieChart) orDataLabel(line/bar/area) set — those feed UIA. Custom visuals augment the visual; they don't replace the accessible description.
The most-cited visualization mistakes (Tufte, Few, Cairo, Wilke). If a user asks for one, push back with the alternative.
- Truncated bar baselines. Bars MUST start at zero. A 3% gap with a 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.
- Dual y-axes. Fabricates correlation between unrelated series. Use two
small multiples or normalize both series to an index (e.g.
(value / baseline) × 100) so they share a single axis. - 3D / exploded pies, drop shadows, gradients on bars. Tufte's "chartjunk." 3D pies are actively misleading — angle distortion changes the visual proportion of slices.
- Too many series. >5–7 lines, >7 pie slices. Group the tail into "Other" or split into small multiples.
- Rainbow / unordered categorical palettes on ordinal data. When the variable has order (low/medium/high, age buckets, sentiment), use a sequential ramp. A categorical palette implies "different kind", not "different magnitude".
- Pie chart for ranking. Pies hide rank — angle judgments are imprecise. A sorted bar chart shows it directly.
Reach for it when:
- You need an icon-plus-text axis tick or slice label.
- You need to render the slice percent in the slice itself instead of a side legend (direct labeling, see above).
- You're embedding a chart in a dashboard whose typography contract demands
fonts/colors the built-in
ChartAxisstyle doesn't match.
Skip it when a string works:
- Plain numeric formatting →
DataLabel((d, i) => d.Value.ToString("C")). - Custom number-to-string for ticks → built-in tick formatting (
Fmt(t)handles short numbers cleanly). - Just changing color/font of a built-in label — that's not exposed yet;
if you need it, file an issue rather than dropping to
*Viewfor a one-property override.
docs/guide/charting.md— full user-facing chart guide.src/Reactor/Charting/Charts.cs—ChartElement<T>/PieChartElement<T>fluent API.*Viewmethods sit near the bottom of each class.src/Reactor/Charting/Charts.Tree.cs—TreeChartandForceGraphfactories + their elements.src/Reactor/Charting/D3Charts.cs— d3 primitives (D3Pie,D3Axes,D3Grid,D3Canvas).D3Axesis where the optionalxTickLabel/yTickLabeldelegates plug in.src/Reactor/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—CenterAtand the anchor overload ofCanvas.
External (read these once if charting is new to you):
- Cleveland & McGill (1984), Graphical Perception — the perceptual ranking.
- Edward Tufte, The Visual Display of Quantitative Information — data-ink ratio, chartjunk.
- Stephen Few, Show Me the Numbers — practitioner playbook for business charts.
- Claus Wilke, Fundamentals of Data Visualization — free online; the modern reference.
- Alberto Cairo, How Charts Lie — the pitfalls chapter is required reading.
- d3js.org docs on
d3-scale,d3-axis,d3-shape— even when working through Reactor's wrappers.