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-chart

Usage

<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

PropTypeDefaultDescription
classNamestring
dataBarChartDatum[]The rows. Each one is a band along the category axis.
xDataKeystring'name'Key holding the category label. Used by the axis and the readout.
statusBarChartStatus'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.
aspectRationumber2Width ÷ height. 2 is the wide card shape.
animationDurationnumber1100Milliseconds for the bars to grow in on mount.
domainDurationnumber500Milliseconds 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.
orientationBarChartOrientation'vertical'vertical grows the bars upward; horizontal grows them rightward.
stackedbooleanfalseStack the series on each other instead of standing them side by side.
barGapnumber0.2Fraction of each band left empty, 0 to 1. A fraction rather than a pixel gap so the proportions hold at any width.
barWidthnumberFixed bar thickness in points. Derived from the band when omitted.
stackGapnumber0Points between the segments of a stack.
cornerRadiusnumber4Corner radius on the growing end of a bar.
minBarLengthnumber0Smallest 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.
fadedOpacitynumber0.3Opacity of the bars that are not under the finger.
onActiveIndexChange(index: number, datum: BarChartDatum | null) => voidThe band under the finger as it moves, and -1/null when it lifts. Fires when the index changes, not per frame.
compactbooleanfalseDrop the axis padding, for a bar sparkline with no axis or readout.

BarChart.Grid

PropTypeDefaultDescription
rowsnumberHow many lines to draw across the value axis.
colorstring
dashArraystring
opacitynumber

BarChart.Bar

PropTypeDefaultDescription
dataKeystringColumn in the data holding this series' values.
colorstringExplicit colour. Defaults to the --color-chart-* token for colorIndex.
colorIndexSeriesColorIndexWhich of the five chart tokens to take.
cornerRadiusnumber4Corner radius, overriding the chart's.

BarChart.XAxis

PropTypeDefaultDescription
ticksnumberHow many labels to show. The rest are dropped, evenly.
format(datum: BarChartDatum, index: number) => stringTurn a row into its label. Defaults to the value at xDataKey.
classNamestring

BarChart.YAxis

PropTypeDefaultDescription
ticksnumberHow many labels to show along the value axis.
format(value: number) => stringFormat a value for its label. Defaults to a compact number.
classNamestring

BarChart.Tooltip

PropTypeDefaultDescription
formatValue(value: number, key: string) => stringFormat one series' value. Defaults to a compact number.
formatX(datum: BarChartDatum) => stringFormat the readout's heading from the row. Defaults to the value at xDataKey.
classNamestring

BarChart.Legend

PropTypeDefaultDescription
classNamestring
labelsRecord<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.

On this page