1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticas chartsvgzero-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 $600k NA EMEA LATAM APAC ANZ
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 página de Astro:

astro
---
import { GroupedBarChart } from 'astro-component-kit/charts';
---

<GroupedBarChart
  categories={['NA', 'EMEA', 'APAC']}
  series={[
    { name: '2025', data: [420, 310, 260] },
    { name: '2026', data: [510, 365, 340] },
  ]}
  prefix="$" suffix="k"
/>
--- import { GroupedBarChart } from 'astro-component-kit/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/lib/GroupedBarChart.astro:

astro
---
/**
 * 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).
 */
import ChartFrame from './ChartFrame.astro';
import { colorAt, scaleLinear, niceTicks, formatValue } from './chart-utils';

interface Props {
  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;
}

const {
  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 = '',
} = Astro.props;

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`;
};
---

<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`}>
    {ticks.map((t) => (
      <g>
        <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
        <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
      </g>
    ))}
    {categories.map((c, ci) => {
      const gx = m.l + ci * groupW + (groupW - inner) / 2;
      return (
        <g>
          <text class="cf-label" x={m.l + ci * groupW + groupW / 2} y={H - 8} text-anchor="middle">{c}</text>
          {series.slice(0, 8).map((s, si) => {
            const bx = gx + si * (barW + gap);
            const v = s.data[ci] ?? 0;
            return (
              <path
                class="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)}`}
              />
            );
          })}
        </g>
      );
    })}
  </svg>
</ChartFrame>

<style>
  .gb__bar {
    transform-origin: bottom;
    transform-box: fill-box;
    animation: gb-grow 0.7s cubic-bezier(0.4, 0, 0.2, 1) both;
  }
  @keyframes gb-grow {
    from { transform: scaleY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .gb__bar { animation: none; }
  }
</style>
--- /** * 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). */ import ChartFrame from './ChartFrame.astro'; import { colorAt, scaleLinear, niceTicks, formatValue } from './chart-utils'; interface Props { 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; } const { 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 = '', } = Astro.props; 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`; }; --- <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`}> {ticks.map((t) => ( <g> <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} /> <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text> </g> ))} {categories.map((c, ci) => { const gx = m.l + ci * groupW + (groupW - inner) / 2; return ( <g> <text class="cf-label" x={m.l + ci * groupW + groupW / 2} y={H - 8} text-anchor="middle">{c}</text> {series.slice(0, 8).map((s, si) => { const bx = gx + si * (barW + gap); const v = s.data[ci] ?? 0; return ( <path class="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)}`} /> ); })} </g> ); })} </svg> </ChartFrame> <style> .gb__bar { transform-origin: bottom; transform-box: fill-box; animation: gb-grow 0.7s cubic-bezier(0.4, 0, 0.2, 1) both; } @keyframes gb-grow { from { transform: scaleY(0); } } @media (prefers-reduced-motion: reduce) { .gb__bar { animation: none; } } </style>

Información Rápida

Categoría
Gráficos y Estadísticas
Nombre de archivo
GroupedBarChart.astro
Dependencias
apexcharts (peer)
Etiquetas
chartsvgzero-dependencybargroupedcomparisoncolumn