Charts
Bar Chart
High-level bar chart wrapper over the Chart primitive supporting grouped, stacked, and percent layouts with a teal-led quebi palette and an interactive legend. Requires the recharts package.
chartbar-chartrechartsdatagraphvisualization
Grouped
Two teal-led series rendered side by side. Click a legend item to focus a single series.
Stacked
The same series stacked into a single bar per category.
Percent (100%)
A stacked-to-100% layout that shows each series as a share of the total.
Single series, no legend
A minimal bar chart with one series, the legend hidden, and grid lines removed.
Source
Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import { type ComponentProps, startTransition, useMemo } from "react"
import { Bar, BarChart as BarChartPrimitive } from "recharts"
import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"
import {
type BaseChartProps,
CartesianGrid,
Chart,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
constructCategoryColors,
DEFAULT_COLORS,
getColorValue,
valueToPercent,
XAxis,
YAxis,
} from "@/components/chart"
/**
* BarChart — quebi design system
*
* A high-level wrapper around the `Chart` primitive that renders grouped,
* stacked, or percent (100%) bar charts from a `config` + `data` pair. Bars
* use the teal-led quebi series palette, with an interactive legend that
* focuses a single series on click. Pass `children` to fully control the bars.
*
* Requires the `recharts` npm package as a peer dependency.
*/
export interface BarChartProps<TValue extends ValueType, TName extends NameType>
extends BaseChartProps<TValue, TName> {
barCategoryGap?: number
barRadius?: number
barGap?: number
barSize?: number
barProps?: Partial<React.ComponentProps<typeof Bar>>
chartProps?: Omit<ComponentProps<typeof BarChartPrimitive>, "data" | "stackOffset">
}
export function BarChart<TValue extends ValueType, TName extends NameType>({
data = [],
dataKey,
colors = DEFAULT_COLORS,
type = "default",
config,
children,
layout = "horizontal",
// Components
tooltip = true,
tooltipProps,
legend = true,
legendProps,
intervalType = "equidistantPreserveStart",
barCategoryGap = 5,
barGap,
barSize,
barRadius,
barProps,
valueFormatter = (value: number) => value.toString(),
// XAxis
displayEdgeLabelsOnly = false,
xAxisProps,
hideXAxis = false,
// YAxis
yAxisProps,
hideYAxis = false,
hideGridLines = false,
chartProps,
...props
}: BarChartProps<TValue, TName>) {
const configKeys = useMemo(() => Object.keys(config), [config])
const categoryColors = useMemo(
() => constructCategoryColors(configKeys, colors),
[configKeys, colors],
)
const configEntries = useMemo(() => Object.entries(config), [config])
const stacked = type === "stacked" || type === "percent"
const defaultBarRadius = stacked ? undefined : 4
return (
<Chart config={config} data={data} dataKey={dataKey} layout={layout} {...props}>
{({ onLegendSelect, selectedLegend }) => (
<BarChartPrimitive
onClick={() => {
onLegendSelect(null)
}}
data={data}
margin={{
bottom: 0,
left: 5,
right: 0,
top: 5,
}}
layout={layout === "radial" ? "horizontal" : layout}
barGap={barGap}
barSize={barSize}
barCategoryGap={barCategoryGap}
stackOffset={type === "percent" ? "expand" : stacked ? "sign" : undefined}
{...chartProps}
>
{!hideGridLines && <CartesianGrid strokeDasharray="4 4" />}
<XAxis
hide={hideXAxis}
className="**:[text]:fill-quebi-fg-muted"
displayEdgeLabelsOnly={displayEdgeLabelsOnly}
intervalType={intervalType}
{...xAxisProps}
/>
<YAxis
hide={hideYAxis}
className="**:[text]:fill-quebi-fg-muted"
tickFormatter={type === "percent" ? valueToPercent : valueFormatter}
{...yAxisProps}
/>
{legend && (
<ChartLegend
content={typeof legend === "boolean" ? <ChartLegendContent /> : legend}
{...legendProps}
/>
)}
{tooltip && (
<ChartTooltip
content={
typeof tooltip === "boolean" ? <ChartTooltipContent accessibilityLayer /> : tooltip
}
{...tooltipProps}
/>
)}
{!children
? configEntries.map(([category, values]) => {
const color = getColorValue(values.color || categoryColors.get(category))
const strokeOpacity = selectedLegend && selectedLegend !== category ? 0.2 : 0
const fillOpacity = selectedLegend && selectedLegend !== category ? 0.1 : 1
return (
<Bar
key={category}
name={category}
dataKey={category}
stroke={color}
strokeWidth={1}
stackId={stacked ? "stack" : undefined}
onClick={(_item, _number, event) => {
event.stopPropagation()
startTransition(() => {
onLegendSelect(category)
})
}}
radius={barRadius ?? defaultBarRadius}
strokeOpacity={strokeOpacity}
fillOpacity={fillOpacity}
fill={color}
{...barProps}
/>
)
})
: children}
</BarChartPrimitive>
)}
</Chart>
)
}