1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticaschartsvgzero-dependencybargroupedcomparisoncolumn

Grouped Bar Chart

Barras verticales agrupadas para comparar varias series entre categorías, con extremos redondeados.

Vista Previa en Vivo

Revenue by region

This year vs last year

  • 2025
  • 2026
$0k$200k$400k$600kNAEMEALATAMAPACANZ
View data
Category20252026
NA$420k$510k
EMEA$310k$365k
LATAM$120k$168k
APAC$260k$340k
ANZ$90k$112k

Uso

Copia y pega este código directamente en tu componente React:

tsx
import { GroupedBarChart } from 'react-component-kit-ui/charts';

<GroupedBarChart categories={['NA', 'EMEA', 'APAC']} series={[
    { name: '2025', data: [420, 310, 260] },
    { name: '2026', data: [510, 365, 340] },
  ]} prefix="$" suffix="k" />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/GroupedBarChart.tsx + GroupedBarChart.css:

GroupedBarChart.tsx
'use client';

import ChartFrame from './ChartFrame';
import { colorAt, scaleLinear, niceTicks, formatValue } from './chart-utils';
import './GroupedBarChart.css';

/**
 * GroupedBarChart — Side-by-side vertical bars for comparing several series
 * across categories. Bars have rounded data-ends anchored to the baseline.
 *
 * @param {string[]} categories - X-axis groups.
 * @param {Array<{name: string, data: number[]}>} series - Series per group (max 8).
 * @param {string} prefix - Value prefix.
 * @param {string} suffix - Value suffix.
 * @param {number} height - SVG height in viewBox units (width is 640).
 * @param {string[]} colors - Optional per-series colors.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
export interface GroupedBarChartProps {
  title?: string;
  subtitle?: string;
  categories?: string[];
  series?: Array<{ name: string; data: number[] }>;
  prefix?: string;
  suffix?: string;
  height?: number;
  colors?: string[];
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function GroupedBarChart({
  title = 'Revenue by region',
  subtitle = 'This year vs last year',
  categories = ['NA', 'EMEA', 'LATAM', 'APAC', 'ANZ'],
  series = [
    { name: '2025', data: [420, 310, 120, 260, 90] },
    { name: '2026', data: [510, 365, 168, 340, 112] },
  ],
  prefix = '$',
  suffix = 'k',
  height = 280,
  colors,
  variant = 'glass',
  size = 'md',
  className = '',
}: GroupedBarChartProps) {
  const W = 640;
  const H = height;
  const m = { t: 12, r: 12, b: 28, l: 48 };
  const ticks = niceTicks(0, Math.max(...series.flatMap((s) => s.data)), 4);
  const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
  const groupW = (W - m.l - m.r) / categories.length;
  const inner = groupW * 0.72;
  const gap = 2;
  const barW = (inner - gap * (series.length - 1)) / series.length;
  const R = Math.min(4, barW / 2);
  const fmt = (v: number) => formatValue(v, prefix, suffix);
  
  /** Bar with rounded top corners only (data-end), square at the baseline. */
  const barPath = (bx: number, top: number, w: number, base: number) => {
    const h = base - top;
    if (h <= 0) return '';
    const r = Math.min(R, h);
    return `M${bx},${base} V${top + r} Q${bx},${top} ${bx + r},${top} H${bx + w - r} Q${bx + w},${top} ${bx + w},${top + r} V${base} Z`;
  };
  return (
    <>
      <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className} legend={series.map((s, i) => ({ label: s.name, color: colorAt(i, colors) }))} table={{ headers: ['Category', ...series.map((s) => s.name)], rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i]))]) }}>
        <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: grouped bar chart`} data-rk-1a0e25="">
          {ticks.map((t, __i: number) => (
            <g key={__i} data-rk-1a0e25="">
              <line className={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} data-rk-1a0e25="" />
              <text className="cf-label" x={m.l - 8} y={y(t) + 4} textAnchor="end" data-rk-1a0e25="">{fmt(t)}</text>
            </g>
          ))}
          {categories.map((c, ci) => {
            const gx = m.l + ci * groupW + (groupW - inner) / 2;
            return (
              <g data-rk-1a0e25="">
                <text className="cf-label" x={m.l + ci * groupW + groupW / 2} y={H - 8} textAnchor="middle" data-rk-1a0e25="">{c}</text>
                {series.slice(0, 8).map((s, si) => {
                  const bx = gx + si * (barW + gap);
                  const v = s.data[ci] ?? 0;
                  return (
                    <path className="cf-mark cf-dim gb__bar" d={barPath(bx, y(v), barW, y(0))} fill={colorAt(si, colors)} tabIndex={0} role="img" aria-label={`${c}, ${s.name}: ${fmt(v)}`} data-tip-title={c} data-tip={`${colorAt(si, colors)}::${s.name}: ${fmt(v)}`} data-rk-1a0e25="" />
                  );
                })}
              </g>
            );
          })}
        </svg>
      </ChartFrame>
    </>
  );
}

export default GroupedBarChart;
GroupedBarChart.css
/* GroupedBarChart — ported from astro-component-kit (charts/GroupedBarChart.astro) */

.gb__bar:where([data-rk-1a0e25]) {
  transform-origin: bottom;
  transform-box: fill-box;
  animation: grouped-bar-chart-gb-grow 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes grouped-bar-chart-gb-grow {
  from { transform: scaleY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .gb__bar:where([data-rk-1a0e25]) { animation: none; }
}

Información Rápida

Categoría
Gráficos y Estadísticas
Nombre de archivo
GroupedBarChart.tsx
Dependencias
Ninguna — React + CSS puro
Etiquetas
chartsvgzero-dependencybargroupedcomparisoncolumn