{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-bar",
  "title": "Chart Bar",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A bar chart with upright columns or flat bars, grouped, stacked or expanded to 100%, value labels, one color per category, a highlight, negative coloring and sorting, in a frame with a table view, patterns and states.",
  "dependencies": [
    "recharts@^3"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/chart.json",
    "https://ui.ballmac.com/r/chart-frame.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/chart-bar.tsx",
      "content": "// Ballmac UI: Chart Bar. https://ui.ballmac.com/components/chart-bar\n\"use client\"\n\nimport * as React from \"react\"\nimport { Bar, BarChart, CartesianGrid, Cell, LabelList, ReferenceLine, XAxis, YAxis } from \"recharts\"\n\nimport { ChartTooltip, ChartTooltipContent } from \"@/components/ballmac/chart\"\nimport { ChartFrame, describeChart, domainTicks, niceScale, type ChartFrameProps, type ChartRow, type ChartSeries } from \"@/components/ballmac/chart-frame\"\nimport { useLocale, useMessages } from \"@/lib/ballmac/i18n\"\nimport { cn } from \"@/lib/utils\"\n\ntype ChartBarProps = Omit<ChartFrameProps, \"children\" | \"summaryMode\" | \"skeleton\" | \"table\"> & {\n  /** \"columns\" stand upright with categories along the bottom. \"bars\" lie flat with categories down the side, which reads better for long names. */\n  layout?: \"columns\" | \"bars\"\n  /** \"stacked\" piles the series up; \"expanded\" shows each one's share of the total. */\n  stack?: \"none\" | \"stacked\" | \"expanded\"\n  /** Corner radius of the bars, in pixels. */\n  radius?: number\n  /** Write each bar's value on it. For one series or grouped bars. */\n  labels?: boolean\n  /** With one series, \"category\" colors every bar differently and the legend lists the categories. */\n  colorBy?: \"series\" | \"category\"\n  /** A category to emphasize: the others fade. */\n  highlight?: string\n  /** With one series, color bars below zero differently. */\n  negativeColor?: string\n  /** Sort the categories by the first series. */\n  sort?: \"none\" | \"ascending\" | \"descending\"\n  /** Space between bars. */\n  gap?: \"tight\" | \"default\" | \"wide\"\n  grid?: \"value\" | \"both\" | \"none\"\n  /** Show the value axis. Hide it when `labels` already write the numbers. */\n  valueAxis?: boolean\n  /** Writes a tick on the value axis. Defaults to a compact number. */\n  axisFormatter?: (value: number) => string\n  domain?: [number | \"auto\", number | \"auto\"]\n  referenceLines?: { value: number; label: string }[]\n  tooltip?: \"dot\" | \"line\" | \"dashed\" | \"none\"\n}\n\nconst GAP = { tight: 0.08, default: 0.2, wide: 0.45 } as const\n\n/**\n * A bar chart in a ChartFrame: upright columns or flat bars, grouped, stacked or expanded to 100%, value labels, one color per category,\n * a highlighted category, positive and negative coloring, sorting, reference lines and everything the frame adds: a Table view, patterns,\n * a toggling legend and loading, empty and error states.\n */\nfunction ChartBar({\n  data,\n  xKey,\n  series,\n  layout = \"columns\",\n  stack = \"none\",\n  radius = 6,\n  labels = false,\n  colorBy = \"series\",\n  highlight,\n  negativeColor,\n  sort = \"none\",\n  gap = \"default\",\n  grid = \"value\",\n  valueAxis = true,\n  axisFormatter,\n  domain,\n  referenceLines,\n  tooltip = \"dot\",\n  summary,\n  categoryFormatter,\n  valueFormatter,\n  categoryLabel,\n  chartClassName,\n  ...frame\n}: ChartBarProps) {\n  const msg = useMessages()\n  const locale = useLocale()\n  const nf = React.useMemo(() => new Intl.NumberFormat(locale), [locale])\n  const flat = layout === \"bars\"\n  const expanded = stack === \"expanded\"\n  const byCategory = colorBy === \"category\" && series.length === 1\n  const measure = series[0]\n\n  const rows = React.useMemo<ChartRow[]>(() => {\n    if (sort === \"none\" || !measure) return data\n    const sign = sort === \"ascending\" ? 1 : -1\n    return [...data].sort((a, b) => sign * (((a[measure.key] as number) ?? 0) - ((b[measure.key] as number) ?? 0)))\n  }, [data, sort, measure])\n\n  // With colorBy=\"category\" the legend and the colors belong to the categories, not to the one series.\n  const items: ChartSeries[] = byCategory ? rows.map((r) => ({ key: String(r[xKey]), label: categoryFormatter ? categoryFormatter(r[xKey]) : String(r[xKey]), color: typeof r.color === \"string\" ? r.color : undefined })) : series\n  const number = (value: unknown) => (typeof value === \"number\" ? value : 0)\n\n  const table = byCategory && measure\n    ? {\n        columns: [\n          { key: xKey, label: categoryLabel ?? msg(\"chart-bar.category\", \"Category\") },\n          { key: measure.key, label: measure.label, numeric: true, format: (v: unknown) => (valueFormatter ? valueFormatter(number(v), measure.key) : nf.format(number(v))) },\n        ],\n        rows,\n      }\n    : undefined\n\n  return (\n    <ChartFrame\n      data={rows}\n      xKey={xKey}\n      series={items}\n      table={table}\n      summary={summary ?? (byCategory && measure ? describeChart({ data: rows, xKey, series: [measure], mode: \"ranking\", format: (v, k) => (valueFormatter ? valueFormatter(v, k) : nf.format(v)), formatCategory: (v) => (categoryFormatter ? categoryFormatter(v) : String(v)), msg }) : undefined)}\n      summaryMode={sort === \"none\" && !byCategory ? \"trend\" : \"ranking\"}\n      skeleton=\"bars\"\n      valueFormatter={valueFormatter}\n      categoryFormatter={categoryFormatter}\n      categoryLabel={categoryLabel}\n      chartClassName={cn(chartClassName)}\n      {...frame}\n    >\n      {(chart) => {\n        const compact = new Intl.NumberFormat(chart.locale, { notation: \"compact\", maximumFractionDigits: 1 })\n        const tick = (v: number) => (expanded ? `${Math.round(v * 100)}%` : axisFormatter ? axisFormatter(v) : compact.format(v))\n        const visibleSeries = byCategory ? series : series.filter((s) => !chart.isHidden(s.key))\n        const lastVisible = visibleSeries.at(-1)?.key\n        const scale = expanded || domain || !valueAxis ? undefined : niceScale({ data: rows, keys: visibleSeries.map((s) => s.key), stacked: stack === \"stacked\" })\n        const shown = byCategory ? rows.filter((r) => !chart.isHidden(String(r[xKey]))) : rows\n        const barRadius = (key: string): [number, number, number, number] => {\n          const r = radius\n          if (stack !== \"none\" && key !== lastVisible) return [0, 0, 0, 0]\n          return flat ? [0, r, r, 0] : [r, r, 0, 0]\n        }\n        return (\n          <BarChart\n            data={shown}\n            layout={flat ? \"vertical\" : \"horizontal\"}\n            stackOffset={expanded ? \"expand\" : \"none\"}\n            barCategoryGap={`${GAP[gap] * 100}%`}\n            margin={chart.margin({ top: labels && !flat ? 20 : 8, right: flat && labels ? 44 : 8, left: 0, bottom: 0 })}\n            accessibilityLayer\n          >\n            <defs>{chart.defs}</defs>\n            <CartesianGrid horizontal={flat ? grid === \"both\" : grid !== \"none\"} vertical={flat ? grid !== \"none\" : grid === \"both\"} />\n            {flat ? (\n              <>\n                <YAxis dataKey={xKey} type=\"category\" tickLine={false} axisLine={false} tickMargin={8} width=\"auto\" orientation={chart.rtl ? \"right\" : \"left\"} tickFormatter={(v: unknown) => chart.formatCategory(v)} />\n                <XAxis type=\"number\" hide={!valueAxis} reversed={chart.rtl} tickLine={false} axisLine={false} tickMargin={8} domain={expanded ? [0, 1] : (domain ?? scale?.domain)} ticks={expanded ? undefined : (scale?.ticks ?? domainTicks(domain))} tickFormatter={tick} />\n              </>\n            ) : (\n              <>\n                <XAxis dataKey={xKey} type=\"category\" tickLine={false} axisLine={false} tickMargin={8} reversed={chart.rtl} tickFormatter={(v: unknown) => chart.formatCategory(v)} />\n                <YAxis hide={!valueAxis} type=\"number\" width=\"auto\" tickLine={false} axisLine={false} tickMargin={8} orientation={chart.rtl ? \"right\" : \"left\"} domain={expanded ? [0, 1] : (domain ?? scale?.domain)} ticks={expanded ? undefined : (scale?.ticks ?? domainTicks(domain))} tickFormatter={tick} />\n              </>\n            )}\n            {referenceLines?.map((r) => (flat ? <ReferenceLine key={r.label} x={r.value} stroke=\"var(--muted-foreground)\" strokeDasharray=\"4 4\" label={{ value: r.label, position: \"top\", fill: \"var(--muted-foreground)\", fontSize: 11 }} /> : <ReferenceLine key={r.label} y={r.value} stroke=\"var(--muted-foreground)\" strokeDasharray=\"4 4\" label={{ value: r.label, position: chart.rtl ? \"insideTopLeft\" : \"insideTopRight\", fill: \"var(--muted-foreground)\", fontSize: 11 }} />))}\n            {tooltip !== \"none\" ? <ChartTooltip cursor={{ fill: \"var(--muted)\", opacity: 0.5 }} content={<ChartTooltipContent indicator={tooltip} hideLabel={flat && false} nameKey={byCategory ? undefined : undefined} />} /> : null}\n            {(byCategory ? [measure!] : series).map((s) => (\n              <Bar\n                key={s.key}\n                dataKey={s.key}\n                hide={!byCategory && chart.isHidden(s.key)}\n                stackId={stack === \"none\" ? undefined : \"stack\"}\n                fill={byCategory ? chart.color(s.key) : chart.fill(s.key)}\n                stroke={chart.patterns ? chart.color(s.key) : undefined}\n                strokeWidth={chart.patterns ? 1.5 : 0}\n                radius={barRadius(s.key)}\n                isAnimationActive={chart.animate}\n                animationDuration={600}\n                maxBarSize={72}\n              >\n                {byCategory || highlight || negativeColor\n                  ? shown.map((row) => {\n                      const key = String(row[xKey])\n                      const value = number(row[s.key])\n                      const base = byCategory ? chart.fill(key) : negativeColor && value < 0 ? negativeColor : chart.fill(s.key)\n                      return <Cell key={key} fill={base} stroke={chart.patterns && byCategory ? chart.color(key) : undefined} strokeWidth={chart.patterns ? 1.5 : 0} fillOpacity={highlight && key !== highlight ? 0.35 : 1} />\n                    })\n                  : null}\n                {labels && stack === \"none\" ? (\n                  <LabelList dataKey={s.key} position={flat ? \"right\" : \"top\"} offset={6} className=\"fill-foreground text-xs\" formatter={(v: unknown) => (typeof v === \"number\" ? chart.format(v, s.key) : \"\")} />\n                ) : null}\n              </Bar>\n            ))}\n          </BarChart>\n        )\n      }}\n    </ChartFrame>\n  )\n}\n\nexport { ChartBar, type ChartBarProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/chart-bar.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "chart",
      "bar",
      "column",
      "stacked",
      "ranking",
      "comparison",
      "recharts",
      "graph"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "ai": {
      "summary": "<ChartBar title description data xKey series={[{ key, label, color? }]} />. layout: columns | bars (flat, for long names). stack: none | stacked | expanded. Also labels, colorBy ('category' for one color per bar), highlight, negativeColor, sort, gap, valueAxis, axisFormatter, referenceLines, legend, patterns, state and every ChartFrame prop.",
      "whenToUse": [
        "Comparing values across categories",
        "Rankings (layout='bars', sort='descending')",
        "Parts of a whole across groups (stack)"
      ],
      "whenNotToUse": [
        "A trend over time with many points (use chart-area or chart-line)",
        "A single share of a whole (use chart-pie or chart-radial)"
      ],
      "composesWith": [
        "chart-frame",
        "stat-card",
        "segmented-control"
      ],
      "a11y": [
        {
          "keys": "Arrow keys",
          "action": "Move between bars (accessibilityLayer)"
        },
        {
          "keys": "Tab, Enter / Space",
          "action": "Legend buttons switch series or categories; the Chart / Table switch shows the numbers as a table"
        }
      ],
      "customization": [
        "layout, stack, radius and gap",
        "highlight to emphasize one category",
        "negativeColor for gains and losses",
        "labels with valueAxis={false} for a cleaner look"
      ]
    },
    "examples": [
      "chart-bar-demo",
      "chart-bar-horizontal",
      "chart-bar-stacked",
      "chart-bar-expanded",
      "chart-bar-labels",
      "chart-bar-categories",
      "chart-bar-negative",
      "chart-bar-ranked",
      "chart-bar-patterns"
    ],
    "url": "https://ui.ballmac.com/components/chart-bar"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}