UI Verify
Blog
4 min read

Make charts and canvas UIs testable

Visual testing charts and canvas UIs is hard because they redraw constantly and render live data. Freeze the data, the clock, and the fonts, and the bugs we hit doing it.

Igor LuchenkovIgor LuchenkovBuilding UI Verify
chartscanvasdatavizdeterminismflake detectionhow-to
The same Recharts area chart captured four times: 0.2 seconds after render only the first few days are drawn (3,614 pixels off), at 0.6 seconds about two thirds (1,327 pixels off), at 1.0 second nearly all (286 pixels off), and the settled chart.
On this page

Charts and canvas UIs are the hardest visual surface there is, because they redraw many times a second, animate in on mount, and render live or non-deterministic data. A naive screenshot catches a different frame every run, so the diff flakes even when nothing changed. Dataviz is also the one surface even change-tolerant teams refuse to ship broken, and most of them still verify it by hand. I have chased three separate chart flakes in our own captures, so this post is part how-to and part confession.

Why do teams leave charts out of their visual tests?

Picture the hard case: a data viewer for scientific instruments, a canvas chart library wrapped in React, custom zoom math on top, a graph that updates many times a second, zoomable from seconds out to a year. The rest of that app tests fine with Playwright. Only the plots get excluded, because writing those tests takes longer than checking the plots by hand before every release, so that is what happens. The data cannot be mocked past a point either; a realistic fixture would be megabytes. And even orgs that tolerate UI regressions almost everywhere do not tolerate them in charts, where any regression is a serious problem.

Both are the same shape: the surface people trust least to automate is the one they are least willing to ship wrong. That is a bad combination to leave to a human glance.

Why does a chart vary between two identical runs?

The entry animation is mid-tween when you capture: a Recharts area chart with its 1.5-second entry animation was still 3,614 device pixels away from its final frame 0.2 seconds after render. The data is a live feed, so the bars are a different height. A tooltip follows a cursor that moved. A time axis is anchored to now, so the labels shift every minute. None of that is a real change, but a pixel diff counts all of it, and you learn to ignore the tool. The fix is to remove the variation before you snapshot, not to loosen the threshold until it stops complaining.

Two chart flakes I have actually debugged

  • The tick label that moved two pixels. One Recharts x-axis label shifted by one or two pixels between runs while the line and grid were byte-identical. It took a week to name. Recharts measures text once and caches the width module-wide, keyed by text and font but not by whether the web font had finished loading, so on a cold render it caches fallback-font metrics. The fix was in our capturer, loading fonts eagerly before first paint: 183 correct out of 200 runs became 200 out of 200. A first attempt to fix it on the app side, a blocking font loader, hung on a mocked font and was reverted.
  • The chart we still do not capture on the Playwright path. Our own project overview has a Recharts area chart that sizes its SVG from a runtime measurement of its container, and that measurement varies sub-pixel between renders. We chose not to mask it in the product component, so that page is deliberately excluded from the Playwright suite. The same chart, rendered as a story from a fixture at a fixed size, captures cleanly.
The tick-label flake, reproduced on a small Recharts page. Rendering before the web font loaded moved only the last label, by one pixel, and the diff found nothing else.
The builds-per-day chart on our usage page, captured as a story. The series is a fixture, the size is fixed, the animation is off, so it diffs only when the chart really changed.

What the tool neutralizes, and what is yours

Source of variationHandled by UI VerifyYours to fix
Clock and now-anchored axesFrozen
Entry and tween animationsFrozen via reduced motion
Math.random jitterSeeded
Web font loadingLoaded before first paint
Container sizeRender at a fixed width and height
The data series itselfFeed a fixed fixture
The tool neutralizes the environment; you neutralize the data and the geometry.

UI Verify pins the clock, freezes animations through prefers-reduced-motion, seeds Math.random, inlines fonts, and runs the network sealed, then auto-ignores the residual flake; see deterministic captures and flake detection. What it cannot do is invent a stable data series or decide how big your chart should be. If your chart reads a live feed, the tool has no way to know what the numbers should be.

The recipe

Fixed inputs, fixed size, animation off
new Chart(ctx, {
  type: "line",
  data: fixtureSeries,          // never a live feed
  options: { animation: false, responsive: false },
});
// and render the canvas at an explicit width and height
  • Feed a fixed data series from a fixture, never a live feed. If the real data is megabytes, capture a representative slice; you are testing the drawing, not the dataset.
  • Turn the chart library's own animation flag off, so the first painted frame is also the final one.
  • Render at a fixed width and height, not a fluid box that reflows to its container.
  • Anchor any time axis to a fixed date, not now.
  • For a canvas, wait for the draw to settle before capturing; a canvas paints asynchronously and an early screenshot catches a half-drawn frame.
One flag decides whether the capture time matters. With the entry animation off, a screenshot at 0.2 seconds already matches the settled chart.

Once the data, the clock, and the size are pinned, a chart becomes as testable as a button. The same holds for a raw canvas game board, a seeded network graph, or a WebGL scene: fix the inputs, wait for the frame, capture. The general version of this discipline is in fix flaky Playwright screenshot tests. The payoff is that dataviz stops being the thing you squint at by hand on every release: a deterministic chart capture diffs only when the chart really changed, and the AI judge reads the PR to decide whether that change was meant.

Stop eyeballing your charts

Freeze the data and the clock, and let deterministic captures turn a flaky canvas into a diff you can trust. Start free with 10,000 snapshots a month (50,000 for open-source projects).

Start for free

No credit card required.

ShareXLinkedIn
Related skill

Set up visual testing

From no visual tests to a green check on every pull request.