BarChart
Compares lengths across categories. The mark that says how much, for data whose categories could be shuffled without losing anything.
import { BarChart } from 'neba';
<BarChart
label="Deploys per team"
categories={['Platform', 'Payments', 'Growth']}
series={[{ name: 'Deploys', data: [318, 264, 197] }]}
valueLabels="all"
/>;The data model is the one every chart shares: series, categories, and a null that means a gap rather than a zero. It is written out on the LineChart page.
A bar's length is its value, which is why its axis starts at zero and cannot be talked out of it: crop the scale and a bar twice as long stops meaning twice as much. Where the categories have a natural order and the shape of the change is the point, a LineChart is the better mark.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| series * | NebaChartSeries[] | — | The series. A colour is decided by a series' place in this array, so hiding one from the legend never repaints the survivors |
| categories | (string | number | Date)[] | — | The category axis' labels. Points may carry their own x instead |
| xAxis | NebaChartAxis | — | The category axis |
| yAxis | NebaChartAxis | — | The value axis |
| orientationshared | 'horizontal' | 'vertical' | 'vertical' | Which way the bars run. horizontal is right whenever the category names are words: a vertical chart gives each name the width of one bar |
| stacked | boolean | 'full' | false | Grouped bars answer "which series is bigger"; stacked bars answer "what is this total made of"; full asks about the mix |
| rounded | boolean | true | Cuts the corners off the data end of each bar. The baseline end stays square; a rounded foot makes the axis look scalloped |
| barSize | number | size (md는 24px) | How thick a bar may get, in pixels. Below the cap bars fill their share of the band; above it the leftover stays as air |
| valueLabels | 'none' | 'last' | 'extremes' | 'all' | 'none' | Which values are written on the bars. Eight bars with their numbers on them is a chart and a table at once; past a dozen it is neither |
| variantshared | 'solid' | 'outline' | 'text' | 'text' | Weight of the surface. A chart is a drawing rather than a sheet, so this defaults to text and a chart inside a Card draws no second edge. Use outline for one that stands alone |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Axis type, line weight, marker size, and the height, when none is given |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | The sheet's colour family. A series' colour does not come from here: the palette or series.color decides that |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| height | number | string | size | How tall the drawing is. The axis labels are drawn inside it, so a card sized to the chart is a card the chart fits in |
| label | string | locale's word | The chart's accessible name. Read out in place of the drawing, and the caption of the hidden data table under it. Without it the locale's generic word stands in, but what the chart is *of* can only be said here |
| format | Intl.NumberFormatOptions | — | How numbers are written everywhere they appear: the axis, the tooltip, the value labels, the table. Without it, past ten thousand they are compacted (12.4K) |
| locale | string | — | The language of the chart's own words and dates |
| legend | boolean | NebaChartLegend | series ≥ 2 | Shown automatically from two series up and left off below that: a legend with one swatch restates the title |
| tooltip | boolean | NebaChartTooltip | true | What the pointer uncovers. It never carries a value that is not also in the hidden table |
| empty | ReactNode | — | What to draw when there is nothing to draw |
Every native <div> attribute passes through, along with every Box prop. xAxis, yAxis, legend and tooltip take the same shapes they take on LineChart. See prop conventions for the shared axes.
Examples
orientation
horizontal is the right answer whenever the category names are words. A vertical chart gives each name the width of one bar; a horizontal one gives it a whole column, and the reading order runs the way a list does.
import { BarChart, Card, Grid, GridContainer } from 'neba';
const CATEGORIES = [
'Documentation',
'Onboarding flow',
'Billing portal',
'Search relevance',
'Mobile parity'
];
const VOTES = [412, 388, 301, 264, 190];
/**
* `horizontal` is the right answer whenever the category names are words. A
* vertical chart gives each name the width of one bar; a horizontal one gives
* it a whole column.
*/
export default function BarChartOrientation() {
return (
<GridContainer spacing={3} padded={false}>
<Grid span={{ xs: 12, md: 6 }}>
<Card title='orientation="vertical"' size="sm" className="h-full">
<BarChart
label="Feature votes, vertical"
size="sm"
height={200}
categories={CATEGORIES}
series={[{ name: 'Votes', data: VOTES }]}
/>
</Card>
</Grid>
<Grid span={{ xs: 12, md: 6 }}>
<Card title='orientation="horizontal"' size="sm" className="h-full">
<BarChart
label="Feature votes, horizontal"
size="sm"
height={200}
orientation="horizontal"
categories={CATEGORIES}
series={[{ name: 'Votes', data: VOTES }]}
valueLabels="all"
/>
</Card>
</Grid>
</GridContainer>
);
}stacked
Grouped bars answer "which series is bigger here". Stacked bars answer "what is this total made of". 'full' normalises every bar to the same length, which asks about the mix instead: the value axis becomes a percentage, and the tooltip keeps the original number.
They are different questions and a chart should be asked only one at a time.
import { BarChart, Card, Grid, GridContainer } from 'neba';
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
const SERIES = [
{ name: 'New', data: [180, 204, 176, 231, 248, 262] },
{ name: 'Renewed', data: [420, 438, 461, 452, 489, 514] },
{ name: 'Churned', data: [64, 71, 58, 82, 69, 55] }
];
export default function BarChartStacked() {
return (
<GridContainer spacing={3} padded={false}>
<Grid span={{ xs: 12, md: 4 }}>
<Card title="Grouped" subtitle="Which series is bigger" size="sm" className="h-full">
<BarChart
label="Subscriptions, grouped"
size="sm"
height={180}
categories={MONTHS}
series={SERIES}
legend={false}
/>
</Card>
</Grid>
<Grid span={{ xs: 12, md: 4 }}>
<Card title="stacked" subtitle="What the total is made of" size="sm" className="h-full">
<BarChart
label="Subscriptions, stacked"
size="sm"
height={180}
categories={MONTHS}
series={SERIES}
stacked
legend={false}
/>
</Card>
</Grid>
<Grid span={{ xs: 12, md: 4 }}>
<Card title='stacked="full"' subtitle="The mix" size="sm" className="h-full">
<BarChart
label="Subscriptions, as a share"
size="sm"
height={180}
categories={MONTHS}
series={SERIES}
stacked="full"
legend={false}
/>
</Card>
</Grid>
</GridContainer>
);
}Negative values
A bar that goes the other way grows down from the same zero the others grow up from: the baseline is drawn where zero is, not at the bottom of the plot.
A point's own color overrides its series' for one bar, which is how a single value is marked without spending a second series on it.
import { BarChart, Card } from 'neba';
/**
* A bar that goes the other way grows down from the same zero the others grow
* up from — the baseline is drawn where zero is, not at the bottom of the plot.
*
* A point may override its series' colour, which is what marks the two months
* that lost ground without spending a second series on them.
*/
export default function BarChartNegative() {
return (
<Card title="Net seat change" subtitle="Added minus removed" className="w-full">
<BarChart
label="Net seat change by month"
categories={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']}
valueLabels="all"
series={[
{
name: 'Net seats',
data: [124, 88, { y: -46, color: 'danger' }, 72, 140, { y: -18, color: 'danger' }, 206]
}
]}
/>
</Card>
);
}valueLabels · rounded · barSize
valueLabels="all" is defensible here in a way it is not on a line chart: eight bars with their numbers on them is a chart and a table at once. Past about a dozen it stops being either, and extremes (the series' own high and low) is the one to reach for.
rounded cuts the corners off the data end of each bar; the baseline end stays square. barSize caps the thickness in pixels: below the cap bars fill their share of the band, above it the leftover stays as air. density="compact" widens that share.
<BarChart valueLabels="extremes" rounded={false} barSize={12} density="compact" … />Accessibility
- The data is also rendered as a visually hidden table, captioned with
label. - The plot is focusable;
←/→(or↑/↓when horizontal) step the crosshair,Home/Endjump to the ends,Escapeclears it. - Touching bars are separated by a 2px gap of the surface colour rather than by a stroke, so nothing on the chart is ink that is not data.