Skip to content

Charts: honour prefers-reduced-motion across every Recharts series - #14

Draft
aurph wants to merge 1 commit into
mainfrom
charts-reduced-motion
Draft

Charts: honour prefers-reduced-motion across every Recharts series#14
aurph wants to merge 1 commit into
mainfrom
charts-reduced-motion

Conversation

@aurph

@aurph aurph commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Two bugs, one cause

Recharts animates every series by default and never consults the user's motion preference. All 27 series on the site grew from zero regardless of what the visitor asked their OS for. That is the accessibility bug.

The second effect is subtler and is why the compute-page charts could never be verified. Recharts builds series geometry inside react-smooth, which only emits the actual <path> on its first requestAnimationFrame tick. A chart that never receives a frame renders structurally complete but visually empty — the <g class="recharts-bar-rectangle"> groups are all present with nothing inside:

<g class="recharts-layer recharts-bar-rectangles">
  <g class="recharts-layer recharts-bar-rectangle"></g>   <!-- no path -->
  <g class="recharts-layer recharts-bar-rectangle"></g>   <!-- no path -->

Nothing errors. Nothing logs. The chart just looks blank. Any visitor whose rAF is throttled hits the same thing.

Change

  • chart-theme.ts exports prefersReducedMotion and seriesAnimation, following the existing axisProps/gridProps convention of a plain spreadable const.
  • Every Recharts <Bar>, <Line>, <Area>, <Pie>, <Radar> spreads it — 27 series across 9 files.
  • @visx deliberately excluded. PriceHistoryChart's <Area> comes from @visx/shape, a pure path generator with no animation and no such prop. Passing it would be meaningless, so the guard test skips files that don't import from recharts.

The guard test

The failure mode is a silently blank chart, not an error, so a new chart added without the prop would regress this invisibly. chart-motion.test.ts walks client/src and fails listing any offender. It found 9 series I missed on the first pass (PortfolioOverlay, TiltOverview) plus correctly flagged the visx false positive, which is what scoped the rule.

Verification

Run with chrome --force-prefers-reduced-motion, the Compute Frontier operator bars render real geometry, and the widths match the API to within rounding:

operator bar px API MW bar ratio data ratio
Meta 132 16,245 1.00 1.00
Amazon (AWS) 88 11,043 0.67 0.68
Google 83 10,460 0.63 0.64
OpenAI / Oracle 71 8,902 0.54 0.55

This closes the "charts unverifiable in headless" gap I flagged on #12/#13 — they were correct all along, just unprovable.

  • npm test 303 pass, 0 fail
  • npx tsc --noEmit clean
  • npm run build passes

Independent of #12 and #13; touches no files either of them touch.

Recharts animates every series by default and never consults the user's
motion preference, so all 27 series on the site grew from zero regardless
of what the visitor asked their OS for.

The same default has a second effect that is easy to miss. Recharts builds
series geometry inside react-smooth, which only emits the actual <path> on
its first requestAnimationFrame tick. A chart that never receives a frame
renders structurally complete but visually empty: the
<g class="recharts-bar-rectangle"> groups are all there with nothing inside
them. Nothing errors and nothing logs. That is what made these charts
impossible to verify in a headless browser, and it is the same failure any
visitor hits if rAF is throttled.

- chart-theme.ts exports prefersReducedMotion and seriesAnimation, matching
  the existing axisProps/gridProps convention of a plain spreadable const.
- Every Recharts <Bar>, <Line>, <Area>, <Pie> and <Radar> spreads it: 27
  series across 9 files.
- @visx series are deliberately excluded. PriceHistoryChart's <Area> comes
  from @visx/shape, which is a pure path generator with no animation and no
  such prop; passing it would be meaningless.

The guard test walks client/src and fails if any Recharts series is added
without the prop, because the failure mode is a silently blank chart rather
than an error.

Verified with Chrome --force-prefers-reduced-motion: the Compute Frontier
operator bars now render real geometry, and their widths match the API to
within rounding (132/88/83/71 px against 16245/11043/10460/8902 MW, ratios
1.00/0.67/0.63/0.54 vs 1.00/0.68/0.64/0.55).

303 tests pass, tsc clean, client build passes.
@aurph

aurph commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Verified against current main (e887487) in a clean worktree:

Confirmed the second effect you describe: on the merged branch the compute-frontier bar and timeline charts render with visible geometry in a headless Chrome screenshot. The source-walking test is the valuable part here, since it fails on any future series added without the prop rather than letting the chart go silently blank.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant