component — charts

Bar Chart

High-level bar chart wrapper over the Chart primitive supporting grouped, stacked, and percent layouts with an ink series palette and an interactive legend. Requires the recharts package.

  • chart
  • bar-chart
  • recharts
  • data
  • graph
  • visualization

05 examples

Grouped

Two ink 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.

Legend placement

`legendProps` positions the legend — it is a recharts `Legend` underneath, so `verticalAlign` and `align` are the two axes of that decision, and `ChartLegendContent` reads both to line itself up.

source

bar-chart.tsx

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 ink 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,

  overlays,
  ...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}
            displayEdgeLabelsOnly={displayEdgeLabelsOnly}
            intervalType={intervalType}
            {...xAxisProps}
          />
          <YAxis
            hide={hideYAxis}
            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}

          {overlays}
        </BarChartPrimitive>
      )}
    </Chart>
  )
}