/docsfor

Migrating from Recharts

Map a Recharts dashboard onto diagrammatic forms, sizing, axes, tooltips, color, and legends.

A working map from a Recharts dashboard to diagrammatic, written from the parts that actually differ.

Two differences drive everything else. A recharts chart is a client component that measures its container and renders in pixel space; a diagrammatic chart is a server component that renders in pixel space at a stated width, so size is a host decision rather than a measurement and only a fluid box needs the client. And a recharts tooltip receives the row; a diagrammatic tooltip receives an address into the data the host already has.

Forms

rechartsdiagrammatic
AreaChart with one AreaArea data
AreaChart with several AreaArea series
AreaChart with stackIdStackedArea
LineChartLine data or Line series
BarChartColumn, or Bar for a ranked horizontal breakdown
BarChart layout="vertical"Bar
BarChart with stackIdStackedBar, plus normalize for percent
several Bar side by sideGroupedBar
PieChart with innerRadiusPie inner center centerLabel, plus legend={false} for a DOM legend
TreemapTreemap root
ScatterChart with ZAxisScatter points[].size
Scatter with a Cell per categoryScatter points[].series
RadarChartRadar
FunnelChartFunnel
a hand-rolled hour by weekday gridHeatmap mark="dot"
a hand-rolled contribution calendarHeatmap mark="calendar"
a hand-rolled composition barSplitBar items block
a hand-rolled label truncationnothing: labels are cut to their margin
ResponsiveContainerwidth and height, or Fit when the width is fluid
CartesianGrid plus YAxis tickCountyTicks={4}
XAxis ticks={...}labels, one per tick you want printed
XAxis tick={{ fontSize: 12 }}fontSize={12} on the chart, every role follows
Tooltip contentRoot plus Tooltip, looking the row up by datum.index
Legendautomatic, or legend={false} plus the DOM Legend
isAnimationActive={false}nothing to turn off

Sizing

<ResponsiveContainer width="100%" height={180}>
  <AreaChart data={rows}>…</AreaChart>
</ResponsiveContainer>

becomes, when the card width is known:

<Area data={values} labels={ticks} width={420} height={180} yTicks={4} />

and when it is not:

<Fit>
  {({ width }) => (
    <Area data={values} labels={ticks} width={width} height={180} yTicks={4} />
  )}
</Fit>

Both render in CSS pixels, so the tick labels are 11px whatever the width, and a fontSize on the chart is the one knob when the host's small text is another size. A figure given width alone takes its height from aspect; a figure given both ignores aspect. Without any of them a figure is 640 wide and shrinks, whole, inside a narrower host.

The chart text inherits the page font, so the font-family a recharts host set through a wrapper class is simply gone.

Axes

A recharts axis draws itself and formats its own ticks. Here the value axis is opt-in and the category axis is a list of strings.

// recharts
<YAxis tickFormatter={fmtCompact} tickCount={4} />
<XAxis dataKey="date" ticks={pickTicks(rows, r => r.date)} tickFormatter={fmtDay} />

// diagrammatic
<Area yTicks={4} format={fmtCompact} labels={pickTicks(rows, 5).map(fmtDay)} />

labels are spread evenly across the plotted span, so pass as many as you want printed rather than one per point. The widest printed tick sizes the value gutter, so YAxis width has no counterpart. yTicks on log paper counts in decades; decades() helpers in the host can be deleted.

Tooltips

The tooltip surface stays yours. What changes is where the values come from: the mark carries data-i and data-series, and the host reads its own row.

"use client";
import { Root, Tooltip } from "diagrammatic/interactive";

<Root>
  <Area series={series} labels={ticks} width={width} yTicks={4} />
  <Tooltip>
    {({ datum }) => {
      const row = rows[datum.index ?? 0];
      if (!row) return null;
      return (
        <div className="bg-popover ring-border rounded-lg px-2.5 py-2 shadow-md ring-1">
          <div className="font-mono text-xs">{fmtDay(row.date)}</div>
          {series.map((s) => (
            <Row key={s.name} label={s.name} value={format(row[s.name])} />
          ))}
        </div>
      );
    }}
  </Tooltip>
</Root>;

datum.series names the hovered series when the form has several, datum.index2 addresses the second dimension of a grid, and getSeriesColor(datum.element, name) reads the rendered color for a swatch. A recharts payload mapper that formatted values inline becomes one lookup plus the formatting the host already owns.

Click and selection ride the same delegation: onMarkClick replaces a Bar onClick, and highlight replaces a selected-index style prop.

<Root
  onMarkClick={(datum) => router.push(rows[datum.index ?? 0].href)}
  highlight={selected == null ? null : { index: selected }}
>

Color

A shadcn/ui host imports the shipped bridge once:

import "diagrammatic/shadcn.css";

It maps --dg-c1 through --dg-c5 to --chart-1 through --chart-5, ink to --foreground, muted to --muted-foreground, grid to --border, and surface to --background. Any other system writes the same lines:

[data-dg],
[data-dg-legend] {
  --dg-c1: var(--chart-1);
  --dg-c2: var(--chart-2);
  --dg-c3: var(--chart-3);
  --dg-c4: var(--chart-4);
  --dg-c5: var(--chart-5);
  --dg-grid: var(--color-border);
  --dg-muted: var(--color-muted-foreground);
}

A <Cell fill={chartColor(i)} /> per datum disappears: color follows the series or the category (points[].series on a scatter, categorical on a bar) and comes from the palette. The palette is seven categories; a host with an eighth ranks and folds the tail into Other. The ChartConfig that shadcn's wrapper needs to name and color each key becomes nothing: names come from the data and colors from the tokens.

Legends

A recharts Legend is HTML positioned over the plot. Here the built-in legend is drawn inside the figure, and the DOM Legend is the counterpart on the page's own type:

import { Area, Legend } from "diagrammatic";

<Area series={series} legend={false} width={width} />
<Legend names={series.map((s) => s.name)} className="mt-2 text-xs text-muted-foreground" />

It is a plain list, server-safe, colored through the same tokens as the marks.

Maps

react-simple-maps plus a world-atlas fetch plus a country-name table becomes WorldMap from diagrammatic/world, which carries Natural Earth 110m outlines as static path data and addresses countries by ISO 3166-1 alpha-2. Pan and zoom, which ZoomableGroup provided, is Zoom from diagrammatic/interactive.

<Root>
  <Zoom max={8}>
    {(view) => (
      <WorldMap
        values={{ US: 412, DE: 96, JP: 74 }}
        view={view}
        title="Sign-ins by country"
      />
    )}
  </Zoom>
  <Tooltip>{({ datum }) => <CountryTip iso={datum.series} />}</Tooltip>
</Root>

The function child is what keeps borders and country labels at their drawn weight while the map magnifies; a node child would scale them with the picture.

The name-to-ISO mapping is the host's migration, once, on the data rather than on every render.

What does not come across

  • Dual value axes. Two units on one frame is two figures.
  • A fill per datum. Category color comes from series or categorical; an arbitrary color per row stays out, and a token override is how a host recolors.
  • Animation props. The charts render once, on the server.
  • A wrapper class that restyles chart internals ([&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground). The tokens are the surface, and the type is already on the page's scale.