BarChart

Categorical comparison as bars, vertical or horizontal, with multiple series grouped, stacked, or normalized to 100%. Reach for `horizontal` whenever the category labels are words rather than dates.

html
<UiBarChart :data="..." :categoryKey="..." :series="..." :ariaLabel="..." />

Needs the charts module

This component ships in @jgastager/bundy-ui/charts, a separate module so projects that never draw a chart don't carry the paint classes these need. Nothing extra to install — just add the module entry. See Charts.


Data

required

data is one object per category with one field per series — the wide shape your API already returns.

  • Desktop
  • Mobile

data stays in the wide shape an API returns ({ month: "Jan", desktop: 120, mobile: 80 }); the component folds it into the long rows the marks want. Rows whose value isn't a finite number are dropped rather than zeroed, so a gap in the data reads as a gap.

The hover fade is a CSS transition over the library's own 250ms duration, so it costs nothing and honours prefers-reduced-motion. There's no animate prop and nothing tweens on a data change — the marks are plain SVG elements Vue patches directly.

Category key

required

categoryKey sets which field on each row names its category.

Series

required

series is field names, or { key, label, color } when a series needs a specific colour — a forecast in a lighter shade of the actual, a target in neutral.

  • Actual
  • Forecast
  • Target

Colors are the library's own semantic ones — primary, secondary, success, warning, error, info, pending, neutral — and any shade of them, primary-400 through primary-950, exactly as the bg-* utilities spell them. Set color per series (or per item) to pin one; leave it unset to take the next slot of the default sequence. neutral and secondary are never handed out automatically, so an unlabelled series never lands on them.

group-padding spaces the bars inside a group, and the gap is taken *out* of each bar's slot rather than added between them — so a group still spans exactly its category band however many series it holds. Keep it well below the gap between groups; once the two look equal, the eye stops seeing groups and sees one long row.

ARIA label

required

ariaLabel sets what the chart shows, in one sentence. Required — an unlabelled chart is an unreadable one.

ARIA description

ariaDescription is the detail the label shouldn't carry.

February is the strongest month at 305 visits and April the weakest at 73.

Orientation

orientation turns the bars on their side. horizontal is the better choice whenever the category labels are words rather than dates: they get a full line each instead of being crammed under a vertical bar or rotated.

orientation="horizontal" is the better default whenever categories are words — each label gets a full line of width instead of being rotated or thinned. Sort the rows yourself; a ranked list reads much faster than an alphabetical one.

Variant

variant decides how multiple series share a category. grouped keeps a shared baseline, so bars can be compared directly. stacked trades that for a total per category. normalized stacks to 100%, making it a share read where the total disappears.

grouped
  • Desktop
  • Mobile
stacked
  • Desktop
  • Mobile
normalized
  • Desktop
  • Mobile

variant is ignored for a single series, where there's nothing to group or stack. With several, grouped keeps a shared baseline for direct comparison, while stacked trades that away for a readable total.

value-labels places itself from the variant rather than taking a position. Grouped bars have a free end, so the number goes just past it and the plot reserves the room — a nice domain often ends exactly at the largest value, so without that the tallest bar's label would be clipped. Stacked and normalized segments have no free end, so the number goes inside, and one too small to hold it is dropped rather than overflowing its neighbours.

Radius

radius rounds each bar's corners. Drop it to zero for a histogram, where the bars are meant to read as adjacent bins rather than as separate objects.

Max thickness

maxThickness caps how thick a bar gets, so a handful of categories in a wide box don't become slabs.

max-thickness defaults to 48px. Without a cap, three categories in a wide container become three slabs — length is still the encoding, but it stops reading as one.

Group padding

groupPadding is the space between bars inside one group, as a fraction of the slot each gets. It only applies to grouped — stacked segments share a slot, so there's nothing to separate.

  • Desktop
  • Mobile
  • Desktop
  • Mobile

Value labels

valueLabels prints each bar's value on it. Grouped bars get the number just past their end; stacked and normalized ones get it inside the segment, and drop it when the segment is too small to hold it.

18401210760520310
1240820138096011801120152013401720158016401810
  • Desktop
  • Mobile

X label

xLabel titles the category axis — the horizontal one, or the vertical one once orientation is horizontal. It follows the categories, not the screen.

Y label

yLabel titles the value axis, which is where the unit belongs when the tick labels are bare numbers.

Value format

valueFormat formats the value-axis ticks and the tooltip together.

  • Actual
  • Forecast
  • Target

Category format

categoryFormat rewrites the category tick labels without touching your data.

Grid

grid are the rules from the value-axis ticks.

Tooltip

tooltip is the panel that follows the pointer.

Hovering dims every bar except the one under the pointer to 0.7, rather than drawing a ring on it — a ring is a dot, which reads as a stray mark sitting on a bar. The dimming is per bar but the tooltip is per category: on a grouped chart one bar lights up while the panel still lists every series at that position. Hovering the gap between bars dims nothing rather than dimming everything.

Legend

legend is on by default from two series up, and off for one. Force it off when the surrounding heading already names what's plotted.

Legend position

legendPosition sets where the legend sits relative to the plot.

  • Desktop
  • Mobile

Legend align

legendAlign places the key horizontally — start lines it up with the plot's left edge, which is where the eye already is after reading the axis.

  • Desktop
  • Mobile
  • Desktop
  • Mobile
  • Desktop
  • Mobile

Legend orientation

legendOrientation flows the entries in a row or a column. vertical gives long labels their own line.

  • Desktop · 8,680 visits
  • Mobile · 7,630 visits

Guides

guides drops the axes and the margins they reserve, so the bars fill the box — the bar equivalent of a sparkline, for a stat tile or a table cell.

Deploys103

guides="false" drops the axes, ticks, labels and grid *and* the margins they reserve, so the marks fill the whole box. That plus a small height is the inline-sparkline shape — there's no separate component for it.

Height

height is the plot's height in pixels. Ignored once aspectRatio is set.

Aspect ratio

aspectRatio sizes the chart from its container's width instead of a fixed height.

  • Desktop
  • Mobile

Loading

loading is a skeleton at the chart's exact height. An empty data array gets the empty state instead — both hold the height, so a dashboard doesn't reflow as its panels resolve.

  • Desktop
  • Mobile
No data to display
  • Desktop
  • Mobile

@focus-change

The category under the pointer, or null once it leaves — for driving a readout or a linked row outside the chart.

Month:

  • Desktop
  • Mobile

API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
datarequired
Record<string, unknown>[]
One object per category, with one field per series.
categoryKeyrequired
string
Field holding each row's category.
seriesrequired
ChartSeriesInput[]
The series to draw. Order assigns default colors and, when stacked, the segment order.
ariaLabelrequired
string
What the chart shows, for screen readers. Required: an unlabelled chart is unreadable.
string
A longer accessible description, for detail the label shouldn't carry.
"vertical" | "horizontal"
'vertical'
Bar direction. `horizontal` is the better choice whenever the category labels are words rather than dates — they get a full line of width instead of being rotated.
"grouped" | "stacked" | "normalized"
'grouped'
How multiple series share a category. `grouped` keeps a shared baseline for direct comparison; `stacked` trades that for a readable total; `normalized` shows share. Ignored for a single series.
number
6
Corner radius on each bar, in pixels.
number
48
Caps how thick a bar gets, so few categories in a wide box don't become slabs.
number
0.16
Space between the bars inside one group, as a fraction of the slot each gets. Only applies to `grouped` — stacked bars share a slot rather than sitting beside each other. Keep it well below the gap the category band already leaves between groups: the moment the two read as equal, the eye stops seeing groups and starts seeing one long row of bars.
boolean
false
Prints each bar's value on it. Grouped bars get the number just past their end; stacked and normalized ones get it centred in their own segment, since a segment's end is the next one's start. A segment too small to hold its number is left unlabelled rather than overflowing into its neighbours. Worth using when the exact figures matter as much as the comparison — a summary tile, a report someone reads once. With many bars it's a lot of ink for what the value axis already says, and the tooltip covers the occasional lookup.
string
Title for the category axis.
string
Title for the value axis.
(value: number) => string
Formats value-axis ticks and tooltip values.
(value: string) => string
Formats category-axis tick labels.
boolean
true
Draws grid rules from the value-axis ticks.
boolean
true
Shows the tooltip on hover.
boolean
Shows the legend. Defaults to on for two or more series.
"top" | "bottom"
'bottom'
Where the legend sits relative to the plot.
"start" | "center" | "end"
'start'
Horizontal placement of the legend.
"horizontal" | "vertical"
'horizontal'
How the legend entries flow.
boolean
true
Draws the axes and reserves their margins.
number
320
Height of the plot in pixels. Ignored when `aspectRatio` is set.
number
Width-to-height ratio, sizing the chart from its container's width.
boolean
false
Replaces the plot with a skeleton of the same height.

Emits

Event
Payload
Description
focusChange
[category: string | null]
The category under the pointer, or `null` once it leaves.

Types

ts
interface ChartPlot {
    x: number;
    y: number;
    width: number;
    height: number;
    /** Right edge, since marks need it more often than the width. */
    right: number;
    /** Bottom edge, which is also the x axis' baseline. */
    bottom: number;
}
ts
type ChartSeriesInput = string | ChartSeries;