react-native-pure-chart
v2.0.2
Published
Charts built from nothing but <View> and <Text>. Zero dependencies, no native modules, no SVG, no Skia.
Readme
react-native-pure-chart
Charts built from nothing but
<View>and<Text>. Zero dependencies. No native modules. No SVG, no Skia.
react-native-pure-chart draws every axis, line, bar and pie slice with plain
React Native core components — rotated Views with transformOrigin, circular
clipping, and the Animated API. Nothing to link, nothing to compile.
Why pure?
npm installand you're done. Nopod install, no config plugin, no native linking — works in Expo Go, bare RN, and react-native-web.- Survives every React Native upgrade. Only core components are used, so there is no native module to break on the next RN release.
- New Architecture native. Built for RN 0.76+ / Fabric; animations run on the native driver (transform/opacity only).
Quick start
import { LineChart } from 'react-native-pure-chart';
<LineChart data={[30, 200, 170, 250, 10]} />;That's a full chart: width fills the parent, nice-number ticks, compact
1.2K-style labels, and a 500 ms entrance animation — all defaults.
Installation
npm install react-native-pure-chart
# or
yarn add react-native-pure-chartRequires React Native 0.73+ (for transformOrigin). No other setup.
Gallery
Every image below is a real screenshot from example/ on an iOS simulator —
the code under each one is what produced it.
Line charts
Multi-series + legend
<LineChart data={revenue} height={200} />Smooth curve + area fill
<LineChart data={traffic} curve="monotone" area />Step curve, thick, no dots
<LineChart
data={traffic}
curve="step"
strokeWidth={3}
showDataPoints={false}
/>null values break the line
<LineChart data={[50, null, null, 90, 40, 70, 20, 60]} />Custom value labels
<LineChart
data={traffic}
curve="monotone"
renderValueLabel={(e) => <Pill value={e.value} />}
/>Built-in tooltip + guide line
// tap, or drive it yourself
<LineChart data={revenue} selectedIndex={3} />Bar charts
Grouped
<BarChart data={platforms} barRadius={5} />Stacked
<BarChart data={platforms} stacked barRadius={5} />Horizontal
<BarChart data={monthly} horizontal barRadius={5} />Negative values
<BarChart data={[12, -8, 25, -15, 30, 5]} barRadius={5} />Selection dims the rest
<BarChart data={platforms} selectedIndex={2} />Stacked + horizontal + dark
<BarChart data={platforms} stacked horizontal theme="dark" />Pie & donut
Pie + legend
<PieChart data={budget} size={200} />Donut + center label
<PieChart
data={budget}
innerRadius="60%"
renderCenterLabel={() => <Total />}
/>On-slice labels + padAngle
<PieChart data={budget} padAngle={2} sliceLabel="percentage" />Dark donut
<PieChart data={budget} innerRadius="60%" theme="dark" />Dark mode
One prop. Pass useColorScheme() straight through and every axis, grid line,
label and tooltip flips with it.
import { useColorScheme } from 'react-native';
const scheme = useColorScheme();
<LineChart data={revenue} theme={scheme === 'dark' ? 'dark' : 'light'} />;API
Runtime exports: LineChart, BarChart, PieChart, DEFAULT_PALETTE,
formatCompact. Every prop and payload type is exported too
(LineChartProps, PressEvent, ChartTheme, …).
Data
type ChartData =
| readonly (number | null | DataPoint)[] // one series
| readonly Series[]; // many
interface DataPoint {
value: number | null; // null = missing
label?: string; // x-axis label
color?: string; // per-point override
extra?: unknown; // returned untouched in press events
}
interface Series {
data: readonly (number | null | DataPoint)[];
name?: string; // legend + tooltip
color?: string; // defaults to palette[seriesIndex]
}Numbers, nulls and DataPoint objects can be mixed freely in one array.
Pie charts take PieSliceDatum[] — { value, label?, color?, extra? }.
Colors
Nothing is hard-coded. Colors come from four levels, most specific first:
// 1. the whole chart — replaces the auto-assigned palette
<BarChart data={data} palette={['#FF6B6B', '#4ECDC4', '#FFD93D']} />
// 2. one series / one pie slice
<LineChart data={[{ name: 'Sales', data: [30, 90, 50], color: '#FF6B6B' }]} />
<PieChart data={[{ value: 50, label: 'Marketing', color: '#FF6B6B' }]} />
// 3. one bar — highlight a single value
<BarChart data={[30, { value: 90, color: '#FF6B6B' }, 50]} />
// 4. line dots, independently of the line
<LineChart data={data} showDataPoints={{ color: '#111827', radius: 5 }} />The palette cycles when there are more series than colors, so it never runs out. Extend the built-in one instead of retyping it:
import { DEFAULT_PALETTE } from 'react-native-pure-chart';
<BarChart data={data} palette={['#FF6B6B', ...DEFAULT_PALETTE]} />;Two limits worth knowing: a per-point color applies to grouped and
single-series bars only — stacked segments take the series color — and line
segments always use the series color, so a single point can't recolor the line
it sits on. Axis, grid, label and tooltip colors are not part of the palette;
they come from theme.
LineChart
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| data | ChartData | — | Numbers, {value, label} objects, or an array of series |
| strokeWidth | number | 2 | Line thickness (no upper limit) |
| curve | 'linear' \| 'step' \| 'monotone' | 'linear' | 'monotone' draws a smooth curve that never overshoots the data |
| area | boolean \| {opacity} | false | Fill between the line and the zero baseline (default opacity 0.15) |
| showDataPoints | boolean \| {radius, color} | true | Point markers. Default radius max(3, strokeWidth * 1.5) |
| missingValues | 'break' \| 'interpolate' \| 'zero' | 'break' | How null values are treated |
| spacing | number | auto | Exact px between points. Auto mode fills the width, but never squeezes below 40px — the chart grows and scrolls instead |
| scrollable | boolean | true | Horizontal scroll when content overflows |
| initialScroll | 'start' \| 'end' | 'start' | Initial scroll position |
| legend | boolean \| LegendOptions | auto | Shown when there are 2+ series and at least one is named |
| tooltip | boolean \| TooltipOptions | true | Built-in tooltip + selection guide |
| onPointPress | (e: PressEvent) => void | — | Tap on the chart (nearest point wins) |
| selectedIndex / onSelectionChange | number \| null | — | Controlled selection |
| renderDataPoint | (e: PressEvent) => ReactNode | — | Replace point markers |
| renderValueLabel | (e: PressEvent) => ReactNode | — | Always-visible label above points |
| xAxis / yAxis | see Axes | — | Ticks, grid, formatters |
BarChart
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| data | ChartData | — | Same shapes as LineChart |
| barWidth | number | auto | Bar width. Auto mode fills the width, but grows and scrolls instead of rendering sliver bars for many categories |
| barRadius | number | 4 | Corner radius on the value end |
| barGap | number | 2 | Gap between bars in a group |
| groupGap | number | auto | Gap between categories |
| stacked | boolean | false | Stack multi-series values (positives up, negatives down) |
| horizontal | boolean | false | Horizontal bars (yAxis still configures the value axis) |
| legend | boolean \| LegendOptions | auto | Same rule as LineChart |
| scrollable / initialScroll | | true / 'start' | Vertical bars only — horizontal bars always fit their height |
| tooltip | boolean \| TooltipOptions | true | Selection also dims the other categories |
| onPointPress / selectedIndex / onSelectionChange | | | Same as LineChart |
| renderValueLabel | (e: PressEvent) => ReactNode | — | Label above each bar |
| xAxis / yAxis | see Axes | — | yAxis is always the value axis |
Negative values are supported — bars grow down from the zero baseline, and stacks split at it.
PieChart
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| data | PieSliceDatum[] | — | {value, label?, color?, extra?} per slice |
| size | number | 200 | Diameter in px |
| innerRadius | number \| '60%' | 0 | Donut hole — px, or a % of the radius |
| holeColor | string | theme background | Hole fill (opaque — see Limitations) |
| startAngle | number | 0 | Degrees clockwise from 12 o'clock |
| padAngle | number | 0 | Gap between slices in degrees |
| sliceLabel | 'none' \| 'label' \| 'value' \| 'percentage' \| (e) => string | 'none' | On-slice labels |
| legend | boolean \| LegendOptions | true | Lists every labeled slice |
| focusOnPress | boolean | true | Tapped slice slides outward |
| onSlicePress | (e: SlicePressEvent) => void | — | Tap on a slice |
| selectedIndex / onSelectionChange | number \| null | — | Controlled selection |
| renderCenterLabel | (selected: SlicePressEvent \| null) => ReactNode | — | Donut center content |
PieChart sizes itself with size — it does not take width, height or
padding.
Common props
Every chart accepts:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| width | number | parent width | Measured with onLayout when omitted |
| height | number | 220 | Plot height (pies use size) |
| padding | number \| {top, right, bottom, left} | {top: 12, right: 12, bottom: 0, left: 0} | Inner padding of the plot area |
| palette | string[] | built-in 10 colors | Auto-assigned series/slice colors — exported as DEFAULT_PALETTE |
| theme | 'light' \| 'dark' \| ChartTheme | 'light' | Pass useColorScheme() output directly |
| animate | boolean \| AnimationConfig | true | {duration, delay, easing, type: 'grow' \| 'fade' \| 'none'} |
| style, testID, accessibilityLabel | | | Standard RN props (a11y label auto-generated) |
The default entrance (type: 'grow') is chart-aware: lines draw left to
right, bars grow from the baseline with a stagger, pies sweep clockwise.
One Animated.Value drives the whole chart on the native driver.
Axes
yAxis configures the value axis (including on horizontal bars), xAxis the
category axis.
| YAxisOptions | Type | Default |
| --- | --- | --- |
| show | boolean | true |
| tickCount | number | 5 |
| min / max | number | nice-number bounds from the data |
| formatLabel | (value, index) => string | formatCompact |
| showGridLines | boolean | true |
| gridLineStyle | 'solid' \| 'dashed' | 'dashed' |
| showAxisLine | boolean | false |
| position | 'left' \| 'right' | 'left' (ignored by horizontal bars) |
| labelStyle | TextStyle | — |
| XAxisOptions | Type | Default |
| --- | --- | --- |
| show | boolean | true (auto-hidden when no point has a label) |
| interval | number \| 'auto' | 'auto' — thins labels to fit the width |
| formatLabel | (label, index) => string | identity |
| showAxisLine | boolean | true |
| showGridLines | boolean | false |
| labelStyle | TextStyle | — |
Theme, legend, tooltip, events
interface ChartTheme {
gridColor?: string;
axisColor?: string;
labelColor?: string;
tooltipBackgroundColor?: string;
tooltipTextColor?: string;
/** Only used as the donut hole fill — charts never paint their background. */
backgroundColor?: string;
}
interface LegendOptions {
position?: 'top' | 'bottom'; // default 'bottom'
labelStyle?: TextStyle;
}
interface TooltipOptions {
render?: (e: PressEvent) => ReactNode; // replace it entirely
formatValue?: (value: number) => string; // default formatCompact
dismissOnTapOutside?: boolean; // default true
}
interface PressEvent {
point: DataPoint;
value: number | null;
label?: string;
index: number; // index within the series
seriesIndex: number;
seriesName?: string;
position: { x: number; y: number }; // px, relative to the chart
}
interface SlicePressEvent {
slice: PieSliceDatum;
index: number;
value: number;
percentage: number; // 0..1
}Guides
Handling missing data
null values are first-class citizens in line charts:
'break'(default) — the line is interrupted, like recharts'connectNulls={false}. An isolated point still gets its dot.'interpolate'— gaps are bridged linearly; synthetic points get no markers'zero'— nulls are treated as 0
Bars always render null as an empty slot (the category label remains).
Controlled selection
Omit selectedIndex and the chart manages selection itself. Pass it and you
own it — useful for syncing a chart with an external list:
const [selected, setSelected] = useState<number | null>(null);
<BarChart
data={data}
selectedIndex={selected}
onSelectionChange={setSelected}
onPointPress={(e) => console.log(e.seriesName, e.label, e.value)}
/>;Custom tooltips
<BarChart
data={data}
tooltip={{
render: (e) => (
<View style={styles.tip}>
<Text>
{e.seriesName}: {e.value}
</Text>
</View>
),
}}
/>Axis formatting
formatCompact (exported) is the default y-axis formatter: 1234 → '1.2K',
2500000 → '2.5M'. Compose it for currencies or percentages:
import { formatCompact } from 'react-native-pure-chart';
<LineChart
data={revenue}
yAxis={{ tickCount: 4, formatLabel: (v) => `$${formatCompact(v)}` }}
xAxis={{ interval: 2 }}
/>;Many points
Auto sizing never crams points closer than 40px (or bars thinner than 12px).
Past that the chart grows wider than its viewport and scrolls horizontally,
with the y-axis labels staying put. Set scrollable={false} to squeeze
everything into the width instead, or initialScroll="end" to open on the
most recent data.
Limitations
Honesty section — the price of the zero-dependency concept:
- Smooth curves cost Views.
curve="monotone"approximates the curve with short line segments (budgeted at ~400 per series). For very dense series prefer'linear'. - Donut holes are opaque. The hole is a colored circle overlay, so donuts
don't work on top of images/gradients. Set
holeColorto match your background. - Large datasets: each point/segment is a
View. A few hundred points are fine; for thousands of points use an SVG- or Skia-based library instead.
Migrating from 0.x
Version 2 is a complete rewrite with no backward compatibility. The old
<PureChart type="..." /> component is gone — import charts directly:
| 0.x | 2.x |
| --- | --- |
| <PureChart type="line" /> | <LineChart /> |
| <PureChart type="bar" /> | <BarChart /> |
| <PureChart type="pie" /> | <PieChart /> |
| data={[{x, y}]} | data={[{value, label}]} |
| data={[{seriesName, data, color}]} | data={[{name, data, color}]} |
| gap | spacing |
| lineThickness (max 10) | strokeWidth (unlimited) |
| numberOfYAxisGuideLine | yAxis={{ tickCount }} |
| yAxisSymbol | yAxis={{ formatLabel }} |
| showXAxisLabel / showYAxisLabel | xAxis={{ show }} / yAxis={{ show }} |
| showEvenNumberXaxisLabel | xAxis={{ interval }} |
| initialScrollPosition + initialScrollTimeOut | initialScroll: 'start' \| 'end' |
| onPress(index) | onPointPress(event) — full event object |
| customValueRenderer | renderValueLabel |
| defaultColumnWidth / defaultColumnMargin | barWidth / groupGap |
| primaryColor / colors | Series.color / palette |
Development
npm install # library dev deps
npm test # unit tests for the core + component tests
npm run typecheck
cd example && npm install
npm start # gallery app against the local ../src (live editing)
npm run start:npm # gallery app against the published npm packageThe example/ app is a gallery of every chart variant, including artifact
stress tests (1° pie slices, high-contrast neighbors, missing data). It runs
in two modes: npm start resolves the library from ../src for live
development, while npm run start:npm resolves the version installed from
npm — use it to smoke-test a release exactly as consumers get it.
example/screens/ShotScreen.tsx is the reproducible source of every image in
docs/ — flip SHOT_MODE in example/App.tsx, pick a group, screenshot.
License
MIT
