Skip to main content

Mantle Chart SVG Renderer Migration Guide

BarChart, LineChart, and AreaChart paint into an <svg> by default. The <canvas> with data-slot="bar-chart-canvas" (line-chart-canvas, area-chart-canvas) is gone from the default chart, and an <svg> with data-slot="bar-chart-svg" (line-chart-svg, area-chart-svg) takes its place. The marks, the axes, the hover layers, the tooltip, the description, and the data table are the same. ScatterPlot did not change, and still paints a <canvas>.

TL;DR: Find every selector and every test that reads a chart's <canvas> (canvas, [data-slot="bar-chart-canvas"], getContext("2d"), getImageData). Point a selector at [data-slot="bar-chart-svg"] instead. A test that reads canvas pixels reads the SVG's paths instead. Change nothing else.

Codemod

Codemod: manual: the change breaks CSS selectors and test queries, not JSX, and a rule cannot tell which chart a test reads pixels from.


You are an agent migrating a codebase. Do this:

  1. Find each CSS selector, querySelector, and testing-library query that names a chart canvas: [data-slot="bar-chart-canvas"], [data-slot="line-chart-canvas"], [data-slot="area-chart-canvas"], [data-slot$="-chart-canvas"], or a bare canvas inside a BarChart.Root, LineChart.Root, or AreaChart.Root. Replace -chart-canvas with -chart-svg, and a bare canvas with svg[data-slot$="-chart-svg"]. Leave a scatter-plot-canvas selector alone.
  2. Find each test that waits for a chart to paint through its canvas, such as canvas.width > 0. Wait for a path in the SVG instead: container.querySelector('[data-slot="bar-chart-svg"] path[d]').
  3. Find each test that reads a chart's pixels through getContext("2d") or getImageData. Read the SVG instead: each series paints one <path>, its d attribute holds the geometry, and its style.fill or style.stroke is the series color, such as var(--color-chart-1). A browser test that must compare pixels takes a screenshot of the plot.
  4. After editing, run the project's formatter, linter, type checker, and tests.

Why SVG? On a list of 100 cards with one small chart each, the SVG charts used less main-thread time than the canvas charts on mount, hover, resize, theme switch, and a data change, and they hold no bitmap memory. An SVG paints mantle's color tokens as CSS, so a theme switch repaints no chart. The SVG draws the same geometry as the canvas did: a browser test paints each scene both ways and compares the pixels.


Before → after

A selector on the chart surface

/* BEFORE */.agent-card [data-slot="bar-chart-canvas"] {	opacity: 0.8;} /* AFTER */.agent-card [data-slot="bar-chart-svg"] {	opacity: 0.8;}

A test that waits for the paint

// BEFOREconst canvas = await waitFor(() => {	const element = container.querySelector("canvas");	expect(element).not.toBeNull();	return element;}); // AFTERawait waitFor(() => {	expect(container.querySelector('[data-slot="bar-chart-svg"] path[d]')).not.toBeNull();});

A test that reads canvas pixels

// BEFOREconst context = canvas.getContext("2d");const pixel = context?.getImageData(x, y, 1, 1).data;expect(pixel?.[3]).toBe(255); // AFTERconst bars = container.querySelector<SVGPathElement>('[data-slot="bar-chart-svg"] path[d]');expect(bars?.style.fill).toBe("var(--color-chart-1)");

Edge cases & gotchas

  • happy-dom lays out nothing, so no chart paints its marks in a happy-dom test. Assert the slots, roles, and text a happy-dom test can see, and read paint in a browser test.
  • A consumer stylesheet that set color or a --color-chart-N override on the Root still works: the SVG reads both from the Root's cascade.

How to verify

Run the project's tests. Search the code for -chart-canvas and for getContext("2d") near a chart. Each match left must belong to a ScatterPlot.