BarChart
Categories compared by length, grouped or stacked.
Categories compared by length, drawn and animated on the UI thread. Composed the way every chart here is: the grid, each series, the axes and the readout are separate children, so a chart that wants no grid simply does not have one.
The baseline is zero and stays zero. A line may crop its axis to the range the data occupies, because its job is to show change; a bar’s job is to compare lengths, and a bar cropped at the bottom is a length that lies — twice as tall no longer means twice as much.
Installation
BarChart ships with the library — no separate install.
import { BarChart, Card, Text } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add bar-chartUsage
<BarChart data={revenue} xDataKey="month">
<BarChart.Grid />
<BarChart.Bar dataKey="revenue" />
<BarChart.XAxis />
<BarChart.Tooltip />
</BarChart>Composition
<BarChart>
<BarChart.Grid /> {/* lines across the value axis */}
<BarChart.Bar dataKey="…" /> {/* one per series */}
<BarChart.XAxis /> {/* category labels, under the bands */}
<BarChart.YAxis /> {/* value labels, down the side */}
<BarChart.Legend />
<BarChart.Tooltip /> {/* the drag, and the readout */}
</BarChart>BarChart.Grid— Lines across the value axis, so a bar can be read against a number and not only against the bar beside it.BarChart.Bar— One series. Every bar in it is a subpath of a single animated path, split in two so the band under the finger keeps full ink while the rest fade.BarChart.XAxis— Category labels under the bands. Real text, so they follow the theme’s font and the platform’s text scaling.BarChart.YAxis— Value labels down the side. The chart reserves a gutter for them, rather than drawing them over the plot.BarChart.Tooltip— The drag that selects a band, and the card that reports it.BarChart.Legend— A swatch and a name per series.
Examples
Two series side by side
Declare a Bar per series. They stand in one band, sharing the width barGap leaves — which is a fraction rather than a pixel gap, so the proportions hold at any width.
<BarChart data={revenue} xDataKey="month">
<BarChart.Grid />
<BarChart.Bar dataKey="revenue" />
<BarChart.Bar dataKey="costs" colorIndex={2} />
<BarChart.XAxis />
<BarChart.Legend labels={{ revenue: 'Revenue', costs: 'Costs' }} />
</BarChart>Stacked instead
The same two series read as a total rather than as a comparison. stackGap puts a hairline between the segments, which is what stops a stack of similar colours reading as one bar.
<BarChart data={revenue} xDataKey="month" stacked stackGap={2}>
<BarChart.Grid />
<BarChart.Bar dataKey="costs" colorIndex={2} />
<BarChart.Bar dataKey="revenue" />
<BarChart.XAxis />
</BarChart>Sideways
Category names longer than a band is wide want the horizontal orientation, where they run down the side and have room to be read. The value axis moves to the bottom with them.
<BarChart data={channels} xDataKey="name" orientation="horizontal" barGap={0.35}>
<BarChart.Grid />
<BarChart.Bar dataKey="sent" colorIndex={3} />
<BarChart.YAxis />
<BarChart.Tooltip />
</BarChart>Reading the active band above the chart
onActiveIndexChange fires when the band under the finger changes, not once per frame — so a readout in the card’s header costs a handful of re-renders across a whole drag.
const [active, setActive] = useState(null);
<Text size="sm" muted>
{active ? `${active.month} · £${active.revenue.toLocaleString()}` : 'Drag to read a month.'}
</Text>
<BarChart
data={revenue}
xDataKey="month"
onActiveIndexChange={(index, datum) => setActive(datum)}
>
<BarChart.Bar dataKey="revenue" />
<BarChart.Tooltip />
</BarChart>Bars that would round to nothing
minBarLength floors a non-zero bar at a length you can see. A value too small to draw still happened, and a bar of no height says it did not.
<BarChart data={errors} xDataKey="day" minBarLength={2}>
<BarChart.Bar dataKey="count" colorIndex={4} />
<BarChart.XAxis />
</BarChart>Versions
Grouped and stacked
Two series side by side, then the same two as a total — the same data answering two different questions.
<BarChart data={revenue} xDataKey="month" onActiveIndexChange={(i, d) => setActive(d)}>
<BarChart.Grid />
<BarChart.Bar dataKey="revenue" />
<BarChart.Bar dataKey="costs" colorIndex={2} />
<BarChart.XAxis />
<BarChart.Tooltip formatValue={(value) => `£${value.toLocaleString()}`} />
</BarChart>Sideways
Horizontal bars, for category names that need the room.
<BarChart data={channels} xDataKey="name" orientation="horizontal" barGap={0.35}>
<BarChart.Grid />
<BarChart.Bar dataKey="sent" colorIndex={3} />
<BarChart.YAxis />
<BarChart.Tooltip />
</BarChart>API Reference
BarChart
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
data | BarChartDatum[] | — | The rows. Each one is a band along the category axis. |
xDataKey | string | 'name' | Key holding the category label. Used by the axis and the readout. |
status | BarChartStatus | 'ready' | loading draws a row of flat placeholder bars and grows them into the real ones when it turns ready. One component throughout, rather than a spinner swapped for a chart — swapping loses the transition. |
aspectRatio | number | 2 | Width ÷ height. 2 is the wide card shape. |
animationDuration | number | 1100 | Milliseconds for the bars to grow in on mount. |
domainDuration | number | 500 | Milliseconds for the value axis to settle after the data changes. |
yDomain | [number, number] | — | Fix the value axis instead of deriving it. Note that the derived domain always includes zero, and a domain that does not is a bar chart whose lengths cannot be compared — pass this only when you mean it. |
orientation | BarChartOrientation | 'vertical' | vertical grows the bars upward; horizontal grows them rightward. |
stacked | boolean | false | Stack the series on each other instead of standing them side by side. |
barGap | number | 0.2 | Fraction of each band left empty, 0 to 1. A fraction rather than a pixel gap so the proportions hold at any width. |
barWidth | number | — | Fixed bar thickness in points. Derived from the band when omitted. |
stackGap | number | 0 | Points between the segments of a stack. |
cornerRadius | number | 4 | Corner radius on the growing end of a bar. |
minBarLength | number | 0 | Smallest length a non-zero bar is drawn at, in points. A value that rounds to nothing still happened, and a bar of zero height says it did not. |
fadedOpacity | number | 0.3 | Opacity of the bars that are not under the finger. |
onActiveIndexChange | (index: number, datum: BarChartDatum | null) => void | — | The band under the finger as it moves, and -1/null when it lifts. Fires when the index changes, not per frame. |
compact | boolean | false | Drop the axis padding, for a bar sparkline with no axis or readout. |
BarChart.Grid
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | — | How many lines to draw across the value axis. |
color | string | — | |
dashArray | string | — | |
opacity | number | — |
BarChart.Bar
| Prop | Type | Default | Description |
|---|---|---|---|
dataKey | string | — | Column in the data holding this series' values. |
color | string | — | Explicit colour. Defaults to the --color-chart-* token for colorIndex. |
colorIndex | SeriesColorIndex | — | Which of the five chart tokens to take. |
cornerRadius | number | 4 | Corner radius, overriding the chart's. |
BarChart.XAxis
| Prop | Type | Default | Description |
|---|---|---|---|
ticks | number | — | How many labels to show. The rest are dropped, evenly. |
format | (datum: BarChartDatum, index: number) => string | — | Turn a row into its label. Defaults to the value at xDataKey. |
className | string | — |
BarChart.YAxis
| Prop | Type | Default | Description |
|---|---|---|---|
ticks | number | — | How many labels to show along the value axis. |
format | (value: number) => string | — | Format a value for its label. Defaults to a compact number. |
className | string | — |
BarChart.Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
formatValue | (value: number, key: string) => string | — | Format one series' value. Defaults to a compact number. |
formatX | (datum: BarChartDatum) => string | — | Format the readout's heading from the row. Defaults to the value at xDataKey. |
className | string | — |
BarChart.Legend
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
labels | Record<string, string> | — | Prettier names for the series keys. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
Why the axis always reaches zero
A bar is read by its length, so the axis it is measured against has to start at nothing. Cropped at the bottom — the trick that makes a line chart's changes legible — a bar twice as tall no longer means twice as much, and the chart quietly exaggerates every difference in it. The derived domain therefore always includes zero. yDomain overrides it, and doing so is opting into a chart that misreads.
Bands, not points
A line has a point at each x; a bar owns a slice of width around it. barGap is the fraction of that slice left empty and barWidth caps the thickness, so a chart of six bars and a chart of sixty both stay proportional without either being told a pixel size.
What it costs to draw
Each series is two animated paths a frame — the band under the finger, and everything else — rather than one animated node per bar. Fifty bars is four animated props, not fifty, and the split is what lets the rest dim without giving every bar its own opacity.
The corners are drawn as a path rather than with rx, because a bar is rounded on the end it grows towards and square on the end it grows from. rx rounds all four corners or none, and a bar rounded at the axis reads as floating above it.
Reduced motion
The grow-in and the domain tween are both skipped, and the chart draws straight to its final shape.