Column Chart
Time-series vertical bars for activity, volume, and counts. Data-ink discipline (Tufte) — one accent, no gridlines, monospace numerics. Built-in source attribution and ASCII empty state.
Studio · Code, chart, settings
Three-panel workbench. Tweak any setting on the right — chart in the middle updates live and the code on the left regenerates ready to paste into your app.
import { ColumnChart } from "@/components/charts/column-chart";
const data = [142, 168, 187, 159, 203, 178, 215];
const labels = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
export function Example() {
return (
<ColumnChart
data={data}
labels={labels}
height={240}
gap={4}
header={{ title: "Active users", subtitle: "Last 7 days" }}
trend={0.184}
goal={{ value: 190, label: "Weekly target" }}
source="Brock Analytics, 2026"
exportable
exportFileName="active-users-7d"
/>
);
}| Label | Value |
|---|---|
| MON | 142 |
| TUE | 168 |
| WED | 187 |
| THU | 159 |
| FRI | 203 |
| SAT | 178 |
| SUN | 215 |
Installation
npx shadcn@latest add brockui.com/r/column-chartUsage
import { ColumnChart } from "@/components/charts/column-chart";
const data = [142, 168, 187, 159, 203, 178, 215];
const labels = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
export function Example() {
return (
<ColumnChart
data={data}
labels={labels}
height={220}
trend={0.184}
source="Brock Analytics, 2026"
/>
);
}Props
| Name | Type | Default | Description |
|---|---|---|---|
| data | number[] | DataPoint[] | — | Bar values. Two forms: number[] (with labels prop) or { label?, value, pattern?, color?, highlight?, note? }[] (object form — supports per-bar overrides for color, hatching, emphasis outline, and editorial annotation) |
| labels | string[] | undefined | X-axis labels (rendered in pixel font under bars + in hover tooltip). Only used when data is number[] |
| height | number | 200 | Chart height in pixels (Y-axis + bars area) |
| gap | number | 4 | Gap between bars in pixels. Auto-shrinks for dense datasets (60+ bars) |
| accent | string | var(--brock-accent) | Override the bar fill color (any CSS color or var). Defaults to Brock orange |
| barRadius | number | 0 | Top-corner radius in px. Common values: 0 (sharp), 2 (subtle), 6 (rounded) |
| header | { title?, subtitle? } | undefined | Title + subtitle block above the chart |
| xAxis | { title?, hideTicks? } | undefined | X-axis configuration (title below ticks, hide tick labels) |
| yAxis | { title?, min?, max?, hideTicks? } | undefined | Y-axis configuration (vertical title, custom min/max, hide tick labels) |
| numberFormat | { prefix?, suffix?, decimals?, locale?, notation?, style?, currency? } | undefined | Number formatter applied to Y-axis, tooltip, and data labels. Supports BCP-47 locale, Intl.NumberFormat notation ('compact' → 1.2K), style ('currency' / 'percent'), and ISO 4217 currency. Explicit formatValue/yAxisFormat win |
| dataLabels | { show?, format? } | undefined | Show value above each bar (Hack mono). Optional per-label formatter overrides numberFormat |
| pattern | 'solid' | 'hatched' | 'solid' | Default fill pattern for all bars. Per-point pattern on a data point wins over this. Hatched encodes historical/estimated/in-progress without spending a second color (Tufte) |
| hatchUntilIndex | number | undefined | Convenience: bars with index < N render hatched, the rest render solid. Classic historical-vs-projected encoding |
| hatchFromIndex | number | undefined | Mirror of hatchUntilIndex: bars with index >= N render hatched. Useful for forecast bands and 'last N hatched' patterns. Combinable with hatchUntilIndex (union) |
| patternStyle | 'diagonal' | 'diagonal-reverse' | 'dots' | 'vertical' | 'horizontal' | 'diagonal' | Visual style of hatched bars (chart-level). Per-bar pattern still controls whether a bar is hatched; this controls how each hatched bar looks. Use 'dots' for grayscale/print |
| scroll | 'none' | 'auto' | 'none' | Overflow behavior when bars don't fit. 'auto' enables horizontal scroll; Y-axis pins to the left while bars + X-axis scroll together |
| minBarWidth | number | 4 | Minimum px per bar. Used with scroll='auto' to decide chart min-width: N*minBarWidth + (N-1)*gap. Ignored when scroll='none' |
| bands | { from, to, label?, color? }[] | undefined | Plot bands — highlighted vertical zones over a range of bar indices. Editorial pattern ('Q3', 'deployment window'). Render behind bars at low opacity. Indices are clamped to the data range |
| trend | number | undefined | Decimal trend indicator e.g. 0.184 → ↗ +18.4%. Orange if positive, muted if negative |
| goal | { value, label? } | undefined | Dashed reference line at value. Goal is included in max scale so it stays visible above bars. KPI dashboard pattern |
| source | string | undefined | Attribution line rendered below the chart (FT pattern) |
| animation | { enabled?, duration? } | { enabled: true, duration: 400 } | Staggered bar-rise on mount. Disabled automatically when prefers-reduced-motion is set |
| loading | boolean | false | Loading state. With no data → full skeleton (dashed ghost bars + LOADING badge, ARIA role=status). With data → dim overlay on top of the chart for background refresh. Honors prefers-reduced-motion |
| error | Error | string | null | null | Terminal error state. Replaces the chart even when data is present (stale data next to an error is misleading). Accepts an Error, a string message, or null. ARIA role=alert |
| onRetry | () => void | undefined | Callback for the retry button in the default error state. The button is rendered only when this prop is provided |
| loadingLabel | string | 'Loading…' | Label rendered next to the LOADING badge and used as the ARIA label for the skeleton state. Override for localization |
| errorLabel | string | 'Error' | Label rendered above the error message and used as the ARIA label. Override for localization |
| retryLabel | string | 'Retry' | Label of the retry button in the default error state. Override for localization |
| loadingFallback | ReactNode | undefined | Full override of the default skeleton + overlay UI. Use for a custom-branded loading experience |
| errorFallback | ReactNode | (error: Error) => ReactNode | undefined | Full override of the default error UI. May be a React node or a function that receives the normalized Error |
| exportable | boolean | { png?, svg?, csv?, copy? } | false | Show the export toolbar (top-right). true = all 4 actions; object form = enable specific ones. Imperative ref methods always work regardless |
| exportFileName | string | (format) => string | 'chart' | Base file name for downloads. String for fixed, function for per-format. Right extension (.png/.svg/.csv) auto-appended |
| onExport | (format, artifact) => void | undefined | Fires after an export completes. Receives format ('png'|'svg'|'csv'|'copy') and the artifact (Blob for png/copy, string for svg/csv). Useful for analytics or custom share flows |
| ref | Ref<ColumnChartHandle> | — | Imperative API: { exportSVG, exportPNG, exportCSV, copyImage }. Each method accepts { fileName, download, width, height } (scale extra on PNG). Works even while the chart is in loading/error/empty state |
| description | string | auto-generated | Accessible description for screen readers (figcaption + table caption). Defaults to 'Column chart with N data points. Source: ...' |
| formatValue | (v: number) => string | toLocaleString | Format function for hover tooltip value. Wins over numberFormat |
| yAxisFormat | (v: number) => string | toLocaleString | Format function for Y-axis tick labels. Wins over numberFormat |
Accessibility
WCAG AA compliant. Keyboard navigable, screen-reader friendly, honors prefers-reduced-motion.
| Tab | Move focus into the chart (single tab stop) |
| ← → ↑ ↓ | Navigate between bars (roving tabindex) |
| Home | Jump to first bar |
| End | Jump to last bar |
- ·
<figure role="figure">wraps the chart with anaria-labelledbypointing to the figcaption - · Each bar uses
role="graphics-symbol"witharia-label="LABEL: value" - · A visually-hidden
<table class="sr-only">provides a tabular data summary (caption + rows for each data point) - · Trend indicator gets human-readable label (“Trend up 18.4 percent”) — arrows are aria-hidden
Design moves
- 1. Monospace Y-axis numbers + tabular-nums (Hack font) — values align by digit width.
- 2. Single
--brock-accent(orange) for all bars. No gradient, no glow, no per-bar color coding. - 3. No gridlines. Single 1px baseline at zero (Tufte data-ink).
- 4. Hover-tooltip with value in Hack mono + period in Departure Mono pixel-font badge.
- 5. Staggered entry animation (30ms cascade, scale-Y from baseline). Disabled on
prefers-reduced-motion. - 6. Built-in
sourceprop renders FT/Bloomberg-style attribution line below the chart. - 7. ASCII empty state (
▒▒▒ no data) when the dataset is empty — no separateisLoadingprop needed.
When to use
Column Chart fits time-series data where each bar is a discrete temporal bucket — hours, days, weeks, monthly buckets, agent calls per minute. Best for showing volume, count, or activity rhythm at a glance with sparse axes that don’t compete with the data.
When not to use
For continuous trends use Line Chart. For tiny embedded charts inside metric cards or prose use Sparkline. For categorical ranking with long labels (horizontal bars) use Bar Chart (coming soon). For composition over time use Stacked Column Chart (coming soon).
Inspired by
- · Financial Times Visual Journalism — sparse axes, source-line attribution, single-color bars
- · Stripe Annual Letters — inline numerics in editorial flow
- · The Pudding — interactive storytelling with restraint
- · Edward Tufte, The Visual Display of Quantitative Information — data-ink discipline