1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticas chartsvgzero-dependencybarstackedcompositionhorizontal

Stacked Bar Chart

Barras apiladas verticales u horizontales con modo 100% y separación de 2px entre segmentos.

Vista Previa en Vivo

Tickets by status

Per sprint

  • Done
  • In review
  • Blocked
0 20 40 60 80 S18 S19 S20 S21 S22 S23
View data
CategoryDoneIn reviewBlockedTotal
S184212559
S19489764
S205114368
S214611663
S225810472
S23638273

Uso

Copia y pega este código directamente en tu página de Astro:

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

<StackedBarChart
  categories={['S1', 'S2', 'S3']}
  series={[
    { name: 'Done', data: [42, 48, 51] },
    { name: 'Blocked', data: [5, 7, 3] },
  ]}
  horizontal={false}
  normalized={false}
/>
--- import { StackedBarChart } from 'astro-component-kit/charts'; --- <StackedBarChart categories={['S1', 'S2', 'S3']} series={[ { name: 'Done', data: [42, 48, 51] }, { name: 'Blocked', data: [5, 7, 3] }, ]} horizontal={false} normalized={false} />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/lib/StackedBarChart.astro:

astro
---
/**
 * StackedBarChart — Vertical or horizontal stacked bars, with an optional 100%
 * mode. Segments are separated by a 2px surface gap instead of borders.
 *
 * @param {string[]} categories - Bar labels.
 * @param {Array<{name: string, data: number[]}>} series - Stack layers (max 8).
 * @param {boolean} horizontal - Draw bars horizontally.
 * @param {boolean} normalized - Show each bar as a 100% share.
 * @param {string} prefix - Value prefix.
 * @param {string} suffix - Value suffix.
 * @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[] }>;
  horizontal?: boolean;
  normalized?: boolean;
  prefix?: string;
  suffix?: string;
  height?: number;
  colors?: string[];
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  title = 'Tickets by status',
  subtitle = 'Per sprint',
  categories = ['S18', 'S19', 'S20', 'S21', 'S22', 'S23'],
  series = [
    { name: 'Done', data: [42, 48, 51, 46, 58, 63] },
    { name: 'In review', data: [12, 9, 14, 11, 10, 8] },
    { name: 'Blocked', data: [5, 7, 3, 6, 4, 2] },
  ],
  horizontal = false,
  normalized = false,
  prefix = '',
  suffix = '',
  height = 280,
  colors,
  variant = 'glass',
  size = 'md',
  className = '',
} = Astro.props;

const W = 640;
const H = horizontal ? Math.max(height, categories.length * 40 + 40) : height;
const m = horizontal ? { t: 8, r: 16, b: 24, l: 72 } : { t: 12, r: 12, b: 28, l: 44 };
const totals = categories.map((_, i) => series.reduce((a, s) => a + (s.data[i] ?? 0), 0));
const ticks = normalized ? [0, 25, 50, 75, 100] : niceTicks(0, Math.max(...totals), 4);
const maxT = ticks[ticks.length - 1];
const v = horizontal ? scaleLinear(0, maxT, m.l, W - m.r) : scaleLinear(0, maxT, H - m.b, m.t);
const band = horizontal ? (H - m.t - m.b) / categories.length : (W - m.l - m.r) / categories.length;
const thick = band * 0.6;
const GAP = 2;
const fmt = (n: number) => formatValue(n, prefix, suffix);
const fmtAxis = (n: number) => (normalized ? `${n}%` : fmt(n));

const bars = categories.map((c, ci) => {
  let acc = 0;
  const segs = series.slice(0, 8).map((s, si) => {
    const raw = s.data[ci] ?? 0;
    const val = normalized ? (raw / (totals[ci] || 1)) * 100 : raw;
    const start = acc;
    acc += val;
    return { name: s.name, raw, val, start, end: acc, color: colorAt(si, colors) };
  });
  return { c, ci, segs };
});
---

<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), 'Total'], rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i])), fmt(totals[i])]) }}
>
  <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: ${normalized ? '100% ' : ''}stacked bar chart`}>
    {ticks.map((t) => horizontal ? (
      <g>
        <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={v(t)} x2={v(t)} y1={m.t} y2={H - m.b} />
        <text class="cf-label" x={v(t)} y={H - 6} text-anchor="middle">{fmtAxis(t)}</text>
      </g>
    ) : (
      <g>
        <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={v(t)} y2={v(t)} />
        <text class="cf-label" x={m.l - 8} y={v(t) + 4} text-anchor="end">{fmtAxis(t)}</text>
      </g>
    ))}

    {bars.map(({ c, ci, segs }) => {
      const pos = (horizontal ? m.t : m.l) + ci * band + (band - thick) / 2;
      return (
        <g>
          {horizontal
            ? <text class="cf-label" x={m.l - 8} y={pos + thick / 2 + 4} text-anchor="end">{c}</text>
            : <text class="cf-label" x={pos + thick / 2} y={H - 8} text-anchor="middle">{c}</text>}
          {segs.map((s, si) => {
            const a = v(s.start);
            const b = v(s.end);
            const isLast = si === segs.length - 1;
            const len = Math.abs(b - a) - (isLast ? 0 : GAP);
            if (!(len > 0)) return null;
            const rect = horizontal
              ? { x: a, y: pos, width: len, height: thick }
              : { x: pos, y: b + (isLast ? 0 : GAP), width: thick, height: len };
            return (
              <rect
                class="cf-mark cf-dim"
                {...rect}
                rx={isLast ? 3 : 0}
                fill={s.color}
                tabindex="0"
                role="img"
                aria-label={`${c}, ${s.name}: ${fmt(s.raw)}${normalized ? ` (${s.val.toFixed(0)}%)` : ''}`}
                data-tip-title={c}
                data-tip={`${s.color}::${s.name}: ${fmt(s.raw)}${normalized ? ` · ${s.val.toFixed(0)}%` : ''}`}
              />
            );
          })}
        </g>
      );
    })}
  </svg>
</ChartFrame>
--- /** * StackedBarChart — Vertical or horizontal stacked bars, with an optional 100% * mode. Segments are separated by a 2px surface gap instead of borders. * * @param {string[]} categories - Bar labels. * @param {Array<{name: string, data: number[]}>} series - Stack layers (max 8). * @param {boolean} horizontal - Draw bars horizontally. * @param {boolean} normalized - Show each bar as a 100% share. * @param {string} prefix - Value prefix. * @param {string} suffix - Value suffix. * @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[] }>; horizontal?: boolean; normalized?: boolean; prefix?: string; suffix?: string; height?: number; colors?: string[]; variant?: 'glass' | 'solid' | 'ghost'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { title = 'Tickets by status', subtitle = 'Per sprint', categories = ['S18', 'S19', 'S20', 'S21', 'S22', 'S23'], series = [ { name: 'Done', data: [42, 48, 51, 46, 58, 63] }, { name: 'In review', data: [12, 9, 14, 11, 10, 8] }, { name: 'Blocked', data: [5, 7, 3, 6, 4, 2] }, ], horizontal = false, normalized = false, prefix = '', suffix = '', height = 280, colors, variant = 'glass', size = 'md', className = '', } = Astro.props; const W = 640; const H = horizontal ? Math.max(height, categories.length * 40 + 40) : height; const m = horizontal ? { t: 8, r: 16, b: 24, l: 72 } : { t: 12, r: 12, b: 28, l: 44 }; const totals = categories.map((_, i) => series.reduce((a, s) => a + (s.data[i] ?? 0), 0)); const ticks = normalized ? [0, 25, 50, 75, 100] : niceTicks(0, Math.max(...totals), 4); const maxT = ticks[ticks.length - 1]; const v = horizontal ? scaleLinear(0, maxT, m.l, W - m.r) : scaleLinear(0, maxT, H - m.b, m.t); const band = horizontal ? (H - m.t - m.b) / categories.length : (W - m.l - m.r) / categories.length; const thick = band * 0.6; const GAP = 2; const fmt = (n: number) => formatValue(n, prefix, suffix); const fmtAxis = (n: number) => (normalized ? `${n}%` : fmt(n)); const bars = categories.map((c, ci) => { let acc = 0; const segs = series.slice(0, 8).map((s, si) => { const raw = s.data[ci] ?? 0; const val = normalized ? (raw / (totals[ci] || 1)) * 100 : raw; const start = acc; acc += val; return { name: s.name, raw, val, start, end: acc, color: colorAt(si, colors) }; }); return { c, ci, segs }; }); --- <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), 'Total'], rows: categories.map((c, i) => [c, ...series.map((s) => fmt(s.data[i])), fmt(totals[i])]) }} > <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: ${normalized ? '100% ' : ''}stacked bar chart`}> {ticks.map((t) => horizontal ? ( <g> <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={v(t)} x2={v(t)} y1={m.t} y2={H - m.b} /> <text class="cf-label" x={v(t)} y={H - 6} text-anchor="middle">{fmtAxis(t)}</text> </g> ) : ( <g> <line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={v(t)} y2={v(t)} /> <text class="cf-label" x={m.l - 8} y={v(t) + 4} text-anchor="end">{fmtAxis(t)}</text> </g> ))} {bars.map(({ c, ci, segs }) => { const pos = (horizontal ? m.t : m.l) + ci * band + (band - thick) / 2; return ( <g> {horizontal ? <text class="cf-label" x={m.l - 8} y={pos + thick / 2 + 4} text-anchor="end">{c}</text> : <text class="cf-label" x={pos + thick / 2} y={H - 8} text-anchor="middle">{c}</text>} {segs.map((s, si) => { const a = v(s.start); const b = v(s.end); const isLast = si === segs.length - 1; const len = Math.abs(b - a) - (isLast ? 0 : GAP); if (!(len > 0)) return null; const rect = horizontal ? { x: a, y: pos, width: len, height: thick } : { x: pos, y: b + (isLast ? 0 : GAP), width: thick, height: len }; return ( <rect class="cf-mark cf-dim" {...rect} rx={isLast ? 3 : 0} fill={s.color} tabindex="0" role="img" aria-label={`${c}, ${s.name}: ${fmt(s.raw)}${normalized ? ` (${s.val.toFixed(0)}%)` : ''}`} data-tip-title={c} data-tip={`${s.color}::${s.name}: ${fmt(s.raw)}${normalized ? ` · ${s.val.toFixed(0)}%` : ''}`} /> ); })} </g> ); })} </svg> </ChartFrame>

Información Rápida

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