Chart

Skia charts — line, area, bar, scatter, candlestick and pie — skinned with the theme's five-colour series ramp.

Open Source
A line

Installation

Chart ships with the library — install it once, then import:

import { Chart } from "@delacour/react-native-ui/chart";

Or copy the source into your project, to own and edit it:

bunx delacour@alpha add chart

Usage

app/index.tsx
import { Chart } from "@/components/ui/chart";
import { View } from "react-native";

const rows = [
  { month: "Jan", revenue: 186 },
  { month: "Feb", revenue: 305 },
  { month: "Mar", revenue: 237 },
  { month: "Apr", revenue: 273 },
  { month: "May", revenue: 209 },
];

export default function Example() {
  return (
    <View className="flex-1 justify-center p-4">
      <Chart config={{ revenue: { label: "Revenue" } }} data={rows} xKey="month">
        <Chart.Grid />
        <Chart.YAxis />
        <Chart.XAxis />
        <Chart.Line yKey="revenue" />
      </Chart>
    </View>
  );
}

Two roots. Chart is every cartesian plot — anything with an x and a y — and PieChart is the one that has neither. They share the palette, the legend and the font, and nothing else.

Line

Several series
Several series

config names each series, sets the draw order, and hands out the theme's ramp by position — so a mark names the data and never the paint. The shape is shadcn's, which means a chart config written for a web dashboard moves across as a copy rather than a translation.

const config = {
  desktop: { label: "Desktop" },                    // chart-1, by position
  mobile:  { label: "Mobile",  color: "chart-4" },  // an explicit ramp slot
  tablet:  { label: "Tablet",  color: "primary" },  // any token the theme emits
  other:   { label: "Other",   color: "#EC4899" },  // a literal, straight through
};

The ramp stops at five and cycles. That is not an arbitrary limit: --chart-1 through --chart-5 are what a shadcn palette declares, so a sixth token would leave every pasted theme drawing five themed series and one stranger. Past five, name the colours.

At most eight distinct token-valued colours resolve in one render, and the component throws by name past that. The ramp dedupes, so twenty series walking it cost five. Literal colours bypass the lookup entirely, so a chart of twenty literal-coloured series is fine.

Dates on the x axis
Dates on the x axis

A Date in the x field makes a time axis. The ticks land on calendar boundaries and print as 20 Jan, and the tooltip heading prints the same way.

Curves
Curves
<Chart curve="step" />
<Chart.Line curve="linear" yKey="visits" />

curve is set on the chart and overridden per mark. monotone, the default, cannot overshoot a point, which is the one a line of real data wants.

Area

An area under the line
An area under the line
<Chart config={config} data={rows} includeZero xKey="day">
  <Chart.Area yKey="visits" />
  <Chart.Line yKey="visits" />
</Chart>

Chart.Area fades the series' own colour out towards the baseline rather than painting it flat — a flat fill at any readable opacity competes with the line above it.

Stacked areas
Stacked areas
<Chart.Area stackId="devices" yKey="desktop" />
<Chart.Area stackId="devices" yKey="mobile" />
<Chart.Area stackId="devices" yKey="tablet" />

Areas naming the same stackId stand on one another: each is the band from the top of the series below to its own running total, so the topmost edge reads as the sum. The stack is built in data space, which is what lets the y domain cover the totals rather than the tallest single series.

Bar

Bars
Bars
<Chart config={{ orders: { label: "Orders" } }} data={rows} xKey="month">
  <Chart.Grid />
  <Chart.YAxis />
  <Chart.XAxis />
  <Chart.Bar yKey="orders" />
</Chart>

Placing a Chart.Bar pulls zero into the y domain on its own — a bar that does not start at zero is a lie about its length. The value end follows --radius at the chart's size, so a theme that squares its buttons squares its bars.

Grouped bars
Grouped bars
<Chart.Bar yKey="desktop" />
<Chart.Bar yKey="mobile" />

Two Chart.Bars that are siblings share each step between them. Nothing names the group — being placed together is what groups them. That is also why a Chart.Bar wrapped in a component of your own never groups: the root collects its direct children, and a wrapper hides the bar from it.

Stacked bars
Stacked bars
<Chart.Bar stackId="traffic" yKey="organic" />
<Chart.Bar stackId="traffic" yKey="paid" />
<Chart.Bar stackId="traffic" yKey="referral" />

Bars naming the same stackId stand on one another, first placed at the bottom, so the top of each column reads as the total. Only the outermost segment is rounded, so a column reads as one bar rather than a pile of pills.

One stack per chart. A second stackId, a stack beside a loose bar, or an area stack beside a bar stack all throw by name. Throwing beats drawing something that looks stacked and is not.

Horizontal bars
Horizontal bars
<Chart config={config} data={rows} orientation="horizontal" xKey="browser">
  <Chart.Grid axis="x" />
  <Chart.YAxis />
  <Chart.XAxis />
  <Chart.Bar yKey="visitors" />
</Chart>

orientation="horizontal" swaps the axis roles: the categories run up the left and the bars grow rightward from them. Chart.YAxis and Chart.XAxis keep their names — each labels the axis it sits beside, whichever field that axis now carries.

Bars below zero
Bars below zero

A negative value hangs from the baseline instead of standing on it, and its rounded end is the one furthest from zero. The baseline sits where zero falls in the domain, so a chart of mixed signs draws it through the middle of the plot.

Labels and square ends
Labels and square ends
<Chart.Bar labels={(value) => `$${value}k`} yKey="revenue" />
<Chart.Bar rounded={false} yKey="revenue" />

labels prints each value against its bar, in the axis colour and the axis font. A grouped or stacked chart draws no labels whatever is passed — there is no room between the bars for them to read. rounded={false} squares the value end; a number is an exact radius in points.

Press to read a column
<Chart.Bar yKey="desktop" />
<Chart.Bar yKey="mobile" />
<Chart.Tooltip.X />
<Chart.Tooltip />

Over bars, Chart.Tooltip.X is a translucent band one step wide rather than a rule, so it picks out the whole column the readout names. The band always snaps — one that glided between columns would highlight half of two bars. Pass band={false} to get the hairline back, or band on a line chart to get the wash.

Scatter

Scatter
Scatter
<Chart config={config} data={rows} includeZero xKey="minutes">
  <Chart.Grid axis="both" />
  <Chart.YAxis />
  <Chart.XAxis />
  <Chart.Scatter radius={5} yKey="free" />
  <Chart.Scatter radius={5} yKey="pro" />
</Chart>

One dot per row per series, on a numeric x. A whole series is one Skia path, so a hundred points is one node. A gap in the data is the same dot at radius zero, which is what lets a point come and go without the series snapping.

Candlestick

Candlestick
Candlestick
<Chart config={{ close: { label: "Close" } }} data={rows} xKey="at">
  <Chart.Grid />
  <Chart.YAxis />
  <Chart.XAxis />
  <Chart.Candlestick />
  <Chart.Tooltip.X />
  <Chart.Tooltip />
</Chart>

Open, high, low and close per row, read from fields of those names unless the props say otherwise. A candle that closed up is chart-1, one that closed down is chart-2, and a flat one is muted-foreground — the top of the same ramp every other chart wears, so a pasted palette recolours them with it.

The config names the close field alone, so the legend and the readout name one price rather than four; the tooltip prints all four, swatched by sentiment.

Pie and donut

A pie
A pie
<PieChart data={rows} nameKey="browser" valueKey="visitors">
  <PieChart.Slice />
  <PieChart.Legend />
</PieChart>

A second root, not a part of Chart. Its categories are its rows: the first row is chart-1, the second chart-2, and no config is needed at all. Pass one to relabel or recolour a row by its name. A row with a negative, missing or unreadable value is dropped before anything is drawn.

A hairline in the chart's background runs between neighbouring slices. Two ramp colours side by side with no gap read as one shape with a stripe.

A donut
A donut
<PieChart data={rows} innerRadius={0.62} nameKey="plan" valueKey="seats">
  <PieChart.Slice />
  <PieChart.Center label="Seats" value="1,125" />
  <PieChart.Legend />
</PieChart>

innerRadius is the hole as a fraction of the radius. PieChart.Center puts a headline figure in it — a React Native view, so it carries the type scale and takes a className. It takes no touches, so a tap through it still lands on a slice.

Labels on the slices
Labels on the slices
<PieChart.Label />
<PieChart.Label format="value" />
<PieChart.Label format={(slice) => slice.label} />

PieChart.Label prints on every slice wide enough to hold one, in the chart's background colour so it reads on any slice of the ramp. The default is the share as a percentage. A slice under twelve degrees gets none — a label wider than its wedge lands on the neighbours.

Tap to read a slice
<PieChart data={rows} nameKey="source" valueKey="sessions">
  <PieChart.Slice />
  <PieChart.Tooltip />
</PieChart>

A pie has no x to scrub along, so PieChart.Tooltip answers a tap. The tapped slice keeps its colour and the rest dim; tapping outside clears it. The readout sits just past the slice's outer edge on its bisector, where a callout line would point.

Placing a tooltip is what makes the chart take taps at all. Without one, an onSelect or a controlled selectedIndex, a pie takes no gesture and scrolls with its parent like a picture.

Grid, axes, tooltip and legend

Grid and axes
Grid and axes
<Chart.Grid />              {/* y ticks, painted with `border` */}
<Chart.Grid axis="both" />
<Chart.YAxis />
<Chart.XAxis />

The grid paints with border and the labels with muted-foreground, so both follow a pasted palette without a chart-specific token existing at all.

Press to read a value

Hold and drag. The readout follows the finger on the UI thread; its text updates only when the nearest point changes.

<Chart config={config} data={rows} xKey="month">
  <Chart.Line yKey="desktop" />
  <Chart.Tooltip.X />
  <Chart.Tooltip.Dot />
  <Chart.Tooltip />
</Chart>

Chart.Tooltip.X, .Y and .Dot mark the position on the plot itself — a vertical rule, a horizontal one, and a dot on each series. They are siblings of Chart.Tooltip, not children: the readout is a React Native view over the canvas and these are drawn inside it, so one cannot contain the other.

All three sit on the nearest datum by default, which is what the readout names. Pass glide to follow the finger continuously instead, and expect the marks to describe a different position from the number beside them. A dot skips a series that is only a bar — a bar has no curve to sit on, and the band already picks out its column.

Holding first is deliberate — a chart inside a scrolling list must not steal the scroll. Pass scrubConfig={{ behaviour: "claim" }} on a chart that owns its width, and a horizontal drag scrubs while a vertical one still scrolls.

A legend
A legend
<Chart.Legend />
<Chart.Legend keys={["ios"]} />
<PieChart.Legend />

The same legend serves both roots. Under a pie, one swatch per row.

Colours
Colours

A colour on a config entry is a token or a literal. A colour on a mark is a literal: the canvas is a second reconciler with no theme provider above it, so a token given to a mark has nothing to resolve it. Name tokens in the config.

Nothing to draw
Nothing to draw
A series that never moves
A series that never moves

An empty series and a single point are both real states, and neither is allowed to crash: with no rows the domain falls back to [0, 1], so the axis still has something to say. A series whose every value is the same draws flat through the middle rather than being stretched to fill the height — expanding a zero-width domain would invent a spread the data does not have.

Inside a card
Inside a card

Sizes

Sizes
Sizes
<Chart size="sm" />
<Chart size="md" />
<Chart size="lg" />
<PieChart size="sm" />

A height token rather than an aspect ratio, so two charts at different widths still line up on a dashboard. The axis font, the tick count and the bar radius step with it. A pie takes the same height, and its diameter is the shorter side of the frame.

API Reference

Part
Chart.GridA hairline rule at each tick. Draws first, under every mark
Chart.LineA stroked line through one series
Chart.AreaThe region under one series, fading towards the baseline. stackId stacks it
Chart.BarOne bar per datum. Siblings group; a shared stackId stacks
Chart.ScatterOne dot per datum
Chart.CandlestickOpen-high-low-close candles, coloured by sentiment
Chart.XAxisTick labels below the plot
Chart.YAxisTick labels beside the plot
Chart.TooltipA floating readout that follows the scrub
Chart.Tooltip.DotA dot on each series at the scrubbed point
Chart.Tooltip.XA vertical rule through the scrubbed position — a band, over bars
Chart.Tooltip.YA horizontal rule at one series' value
Chart.LegendA swatch and a label per series
PieChart.SliceEvery slice, with a hairline between them
PieChart.LabelA percentage, a value or a name on every slice wide enough
PieChart.CenterA headline figure and a caption in the hole of a donut
PieChart.TooltipA readout for the tapped slice
PieChart.LegendA swatch and a label per slice

A part must be a direct child of its root: the root sorts them by identity to decide what is drawn into the canvas, what floats over it and what sits under it. An array from .map() is fine; a mark wrapped in your own component is not.

Chart

Prop

Type

Chart.Line

Prop

Type

Chart.Area

Prop

Type

Chart.Bar

Prop

Type

Chart.Scatter

Prop

Type

Chart.Candlestick

Prop

Type

Chart.Grid

Prop

Type

Chart.XAxis

Prop

Type

Chart.YAxis

Prop

Type

Chart.Tooltip

Prop

Type

Chart.Tooltip.Dot

Prop

Type

Chart.Tooltip.X

Prop

Type

Chart.Tooltip.Y

Prop

Type

Chart.Legend

Prop

Type

PieChart

Prop

Type

PieChart.Slice

Prop

Type

PieChart.Label

Prop

Type

PieChart.Center

Prop

Type

PieChart.Tooltip

Prop

Type

The drawing is @delacour/react-native-charts, a headless Skia engine with no tokens and no className of its own, documented under its own tab. It installs alongside this component and is where to go for a chart this one does not skin — a render prop, a custom mark, or the scale maths on its own.

Zooming and brushing are not here yet. The scrub and the pie's tap are the only gestures a chart takes today.

On this page