Zum Inhalt springen
UI Kit
Accent

Balkendiagramm für React

Säulen und Balken mit gerundetem Datenende, 2 px Abstand in Flächenfarbe, Werten wo sie passen, Tooltips und Pfeiltasten.

Eine von 7 Components in Charts

4 Variants

Kategorien auf einen Blick vergleichen

Ein Balkendiagramm vergleicht Mengen über Kategorien, und Längen liest das Auge genauer als jede andere Form. Dieses zeichnet Säulen und Balken mit runden Enden und einer kleinen Lücke zwischen gestapelten Teilen, setzt Werte dorthin, wo sie passen, und geht mit den Pfeiltasten von Balken zu Balken.

„Einfach“ zeigt einen Wert je Kategorie, der Standard für Rangfolgen und Monatssummen. „Gruppiert“ stellt zwei oder drei Reihen nebeneinander, etwa Plan A gegen Plan B je Monat. „Gestapelt“ zeigt die Anteile jeder Summe, „Waagerecht“ legt die Balken quer für lange Namen wie Produkte oder Länder.

Lass die Werteachse bei null beginnen, ein Balken ab 80 lügt über seine Länge. Sortiere nach Wert, außer die Kategorien haben eine eigene Ordnung wie Monate. Nutze den Akzent für den Balken, auf den es ankommt, und einen ruhigen Ton für den Rest, wenn du eine Aussage treffen willst.

Variants

Einfach

Eine Reihe, Werte auf den Säulen, der stärkste Tag behält den Akzent.

Gruppiert

Zwei Jahre nebeneinander je Quartal, das laufende im Akzent.

Gestapelt

Drei Kanäle gestapelt zur Monatssumme, getrennt durch eine feine Lücke in Flächenfarbe.

Waagerecht

Lange Kategorien links, Balken nach Größe sortiert, der Wert an jeder Spitze.

Details

Was Balkendiagramm mitbringt, aus Registry und Quellcode gelesen.

Kategorie
Charts
Bereich
Elements
Variants
4
Version
1.0.0 · seit 1.0.0
Datei
components/ui/bar-chart.tsx
Bringt mit
utilskitchart
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle BarChartProps aus components/ui/bar-chart.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von ChartA11y an.

  • dataPflicht
    Datum[]

    Rows, one per category, e.g. { month: '2026-03', online: 21400, store: 9800 }

  • xKeyPflicht
    string

    Key of the category. "2026-03" and "2026-03-14" are read as dates and formatted per language.

  • seriesPflicht
    ChartSeries[]

    One bar per series and category, in slot order (accent, grey, two accent tones)

  • formatoptional
    Intl.NumberFormatOptions

    Value format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }

  • xFormatoptional
    Intl.DateTimeFormatOptions
  • tooltipXFormatoptional
    Intl.DateTimeFormatOptions
  • stackedoptional
    boolean

    Several series side by side (default) or stacked to a total

  • orientationoptional
    'vertical' | 'horizontal'

    vertical columns (default) or horizontal bars with the labels on the left

  • valueLabelsoptional
    boolean

    Value at the bar end. Single series and stacked totals, only where it fits.

  • highlightoptional
    string

    Single series: the category (x value) that keeps the accent, the others step back

  • heightoptional
    number

    Vertical: plot height in px, axis labels included

  • rowHeightoptional
    number

    Horizontal: height of one row in px

  • legendoptional
    boolean

    Legend above the plot. Default: for two series and more.

  • xLabeloptional
    string

    Header of the first table column

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.

Bewegung

  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
  • Ohne Animationen, sie steht sofort ruhig da.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 144 Zeilen
components/ui/bar-chart.tsx
'use client';

import { round } from '@/lib/kit';
import { CartesianFrame, barPath, numberFormatter, rampColor, seriesColor, seriesOpacity, textWidth, useIntlLocale, type ChartA11y, type ChartSeries, type Datum } from '@/components/ui/chart';

export interface BarChartProps extends ChartA11y {
  /** Rows, one per category, e.g. { month: '2026-03', online: 21400, store: 9800 } */
  data: Datum[];
  /** Key of the category. "2026-03" and "2026-03-14" are read as dates and formatted per language. */
  xKey: string;
  /** One bar per series and category, in slot order (accent, grey, two accent tones) */
  series: ChartSeries[];
  /** Value format, e.g. { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 } */
  format?: Intl.NumberFormatOptions;
  xFormat?: Intl.DateTimeFormatOptions;
  tooltipXFormat?: Intl.DateTimeFormatOptions;
  /** Several series side by side (default) or stacked to a total */
  stacked?: boolean;
  /** vertical columns (default) or horizontal bars with the labels on the left */
  orientation?: 'vertical' | 'horizontal';
  /** Value at the bar end. Single series and stacked totals, only where it fits. */
  valueLabels?: boolean;
  /** Single series: the category (x value) that keeps the accent, the others step back */
  highlight?: string;
  /** Vertical: plot height in px, axis labels included */
  height?: number;
  /** Horizontal: height of one row in px */
  rowHeight?: number;
  /** Legend above the plot. Default: for two series and more. */
  legend?: boolean;
  /** Header of the first table column */
  xLabel?: string;
  className?: string;
}

const GAP = 2;
const MAX = 24;

/**
 * Columns or bars: simple, grouped, stacked and horizontal.
 *   <BarChart data={rows} xKey="month" series={[{ key: 'online', label: 'Online' }, { key: 'store', label: 'Store' }]} stacked />
 */
export function BarChart({ data, xKey, series, format, xFormat, tooltipXFormat, stacked = false, orientation = 'vertical', valueLabels, highlight, height = 260, rowHeight = 40, legend, xLabel, label, summary, labels, animate, className }: BarChartProps) {
  const intl = useIntlLocale();
  const fmt = numberFormatter(intl, format);
  const horizontal = orientation === 'horizontal';
  const num = (d: Datum, k: string) => (typeof d[k] === 'number' ? (d[k] as number) : 0);
  const totals = data.map((d) => series.reduce((a, s) => a + Math.max(0, num(d, s.key)), 0));
  const all = data.flatMap((d) => series.map((s) => num(d, s.key)));
  const extent: [number, number] = stacked ? [0, Math.max(0, ...totals)] : [Math.min(0, ...all), Math.max(0, ...all)];
  const single = series.length === 1;
  const tipValue = (i: number) => (stacked ? totals[i] : single ? num(data[i], series[0].key) : null);
  const labelW = valueLabels ? Math.max(0, ...data.map((_, i) => { const v = tipValue(i); return v === null ? 0 : textWidth(fmt(v), 12); })) + 10 : 0;
  const plotHeight = horizontal ? data.length * rowHeight + 30 : height;
  const dim = (i: number, si: number) => (highlight !== undefined && single && String(data[i][xKey]) !== highlight ? rampColor(1) : seriesColor(si, series[si].color));

  return (
    <CartesianFrame
      data={data}
      xKey={xKey}
      series={series}
      format={format}
      xFormat={xFormat}
      tooltipXFormat={tooltipXFormat}
      height={plotHeight}
      scale="band"
      horizontal={horizontal}
      extent={extent}
      legend={legend}
      legendShape="rect"
      padRight={horizontal ? labelW : 0}
      padTop={!horizontal && valueLabels ? 12 : 0}
      total={stacked}
      xLabel={xLabel}
      label={label}
      summary={summary}
      labels={labels}
      animate={animate}
      className={className}
      marks={({ cat, band, val, animated, n, active, focus }) => {
        const zero = val(0);
        const thick = stacked || single ? Math.min(MAX, band * (horizontal ? 0.56 : 0.6)) : Math.min(MAX, (band * 0.72 - GAP * (series.length - 1)) / series.length);
        const groupW = stacked || single ? thick : thick * series.length + GAP * (series.length - 1);
        const delay = (i: number) => (animated ? ({ '--kitch-d': `${round((i / Math.max(1, n)) * 420, 0)}ms` } as React.CSSProperties) : undefined);
        return data.map((d, i) => {
          const c = cat(i);
          const start = round(c - groupW / 2);
          const faded = active !== null && active !== i;
          let marks: React.ReactNode;
          if (stacked) {
            let acc = 0;
            const visible = series.map((s, si) => ({ s, si, v: Math.max(0, num(d, s.key)) })).filter((x) => x.v > 0);
            marks = visible.map(({ s, si, v }, k) => {
              const a = val(acc);
              acc += v;
              const b = val(acc);
              const isTop = k === visible.length - 1;
              // The 2px surface gap sits on top of every segment but the last
              const len = Math.abs(b - a) - (isTop ? 0 : GAP);
              if (len <= 0) return null;
              const path = horizontal ? barPath(a, start, len, thick, isTop ? 4 : 0, 'right') : barPath(start, b + (isTop ? 0 : GAP), thick, len, isTop ? 4 : 0, 'up');
              return <path key={s.key} d={path} fill={seriesColor(si, s.color)} className="kitch-series" opacity={seriesOpacity(si, focus)} />;
            });
          } else {
            marks = series.map((s, si) => {
              const v = num(d, s.key);
              const p = val(v);
              const off = round(start + si * (thick + GAP));
              const len = Math.abs(p - zero);
              if (len < 0.5) return null;
              const path = horizontal ? barPath(Math.min(p, zero), off, len, thick, 4, v >= 0 ? 'right' : 'left') : barPath(off, Math.min(p, zero), thick, len, 4, v >= 0 ? 'up' : 'down');
              return <path key={s.key} d={path} fill={dim(i, si)} className="kitch-series" opacity={seriesOpacity(si, focus)} />;
            });
          }
          const tip = tipValue(i);
          const text = valueLabels && tip !== null ? fmt(tip) : null;
          const fits = text && (horizontal || textWidth(text, 11) <= band - 2);
          const highlighted = highlight !== undefined && String(d[xKey]) === highlight;
          return (
            <g key={i} className="transition-opacity duration-200 motion-reduce:transition-none" opacity={faded ? 0.55 : 1}>
              <g className={animated ? (horizontal ? 'kitch-grow-x' : tip !== null && tip < 0 ? 'kitch-grow-y-down' : 'kitch-grow-y') : undefined} style={delay(i)}>
                {marks}
              </g>
              {fits && (
                <text
                  x={horizontal ? val(tip!) + 8 : c}
                  y={horizontal ? c : val(Math.max(0, tip!)) - 7}
                  dy={horizontal ? '0.34em' : undefined}
                  textAnchor={horizontal ? 'start' : 'middle'}
                  className={`tabular-nums ${horizontal ? 'text-[12px]' : 'text-[11px]'} ${animated ? 'kitch-rise' : ''}`}
                  fontWeight={highlighted ? 600 : 500}
                  fill={highlighted || horizontal ? 'var(--foreground)' : 'var(--muted-foreground)'}
                  style={animated ? ({ '--kitch-d': `${round(380 + (i / Math.max(1, n)) * 420, 0)}ms` } as React.CSSProperties) : undefined}
                >
                  {text}
                </text>
              )}
            </g>
          );
        });
      }}
    />
  );
}

Häufige Fragen

Wie installiere ich Balkendiagramm?

Mit aiw add @ai-website/bar-chart. Der Befehl legt die Datei components/ui/bar-chart.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind chart. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Balkendiagramm?

4 Varianten, Einfach, Gruppiert, Gestapelt und Waagerecht.

Kann ein KI-Assistent Balkendiagramm einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Balkendiagramm mit aiw_components und installieren sie mit aiw_add @ai-website/bar-chart.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert