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
This year vs last year
- 2025
- 2026
View data
| Category | 2025 | 2026 |
|---|---|---|
| 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