1 estrella en GitHub = 1 componente nuevo.

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

Metric Delta Table

Tabla comparativa con barras en línea, valores actuales y previos, variaciones y sparklines.

Vista Previa en Vivo

Channel performance

Signups this month vs last month

Metric This month Last month Change Trend
Organic search 4,820 4,310 up 11.8%
Direct 3,120 3,290 down 5.2%
Newsletter 1,840 1,220 up 50.8%
Paid social 1,310 1,480 down 11.5%
Partners 960 710 up 35.2%

Uso

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

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

<MetricDeltaTable
  rows={[
    { label: 'Organic', current: 4820, previous: 4310, trend: [30, 34, 38, 44, 48] },
    { label: 'Paid', current: 1310, previous: 1480 },
  ]}
/>
--- import { MetricDeltaTable } from 'astro-component-kit/charts'; --- <MetricDeltaTable rows={[ { label: 'Organic', current: 4820, previous: 4310, trend: [30, 34, 38, 44, 48] }, { label: 'Paid', current: 1310, previous: 1480 }, ]} />

Instalación Manual

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

astro
---
/**
 * MetricDeltaTable — Comparison table with inline bars, current vs previous
 * values and signed deltas with direction icons. A "table + chart" hybrid.
 *
 * @param {Array<{label: string, current: number, previous: number, invert?: boolean}>} rows - Metrics.
 * @param {string} currentLabel - Header for current values.
 * @param {string} previousLabel - Header for previous values.
 * @param {string} prefix - Value prefix.
 * @param {string} suffix - Value suffix.
 * @param {string} color - Inline bar color.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
import ChartFrame from './ChartFrame.astro';
import SparklineChart from './SparklineChart.astro';
import { formatValue } from './chart-utils';

interface Row { label: string; current: number; previous: number; invert?: boolean; trend?: number[] }
interface Props {
  title?: string;
  subtitle?: string;
  rows?: Row[];
  currentLabel?: string;
  previousLabel?: string;
  prefix?: string;
  suffix?: string;
  color?: string;
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

const {
  title = 'Channel performance',
  subtitle = 'Signups this month vs last month',
  rows = [
    { label: 'Organic search', current: 4820, previous: 4310, trend: [30, 34, 33, 38, 41, 44, 48] },
    { label: 'Direct', current: 3120, previous: 3290, trend: [36, 35, 34, 33, 33, 32, 31] },
    { label: 'Newsletter', current: 1840, previous: 1220, trend: [10, 12, 13, 14, 16, 17, 18] },
    { label: 'Paid social', current: 1310, previous: 1480, trend: [16, 15, 15, 14, 14, 13, 13] },
    { label: 'Partners', current: 960, previous: 710, trend: [6, 7, 7, 8, 8, 9, 10] },
  ],
  currentLabel = 'This month',
  previousLabel = 'Last month',
  prefix = '',
  suffix = '',
  color = 'var(--chart-1, #3987e5)',
  variant = 'glass',
  size = 'md',
  className = '',
} = Astro.props;

const max = Math.max(...rows.map((r) => r.current)) || 1;
const fmt = (v: number) => formatValue(v, prefix, suffix, false);
---

<ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className}>
  <div class="md">
    <table class="md__table">
      <thead>
        <tr>
          <th scope="col">Metric</th>
          <th scope="col" class="md__num">{currentLabel}</th>
          <th scope="col" class="md__num md__hide-sm">{previousLabel}</th>
          <th scope="col" class="md__num">Change</th>
          <th scope="col" class="md__hide-sm"><span class="sr-only">Trend</span></th>
        </tr>
      </thead>
      <tbody>
        {rows.map((r) => {
          const d = r.previous ? ((r.current - r.previous) / r.previous) * 100 : 0;
          const up = d >= 0;
          const good = r.invert ? !up : up;
          return (
            <tr>
              <th scope="row">
                <span class="md__label">{r.label}</span>
                <span class="md__bar" aria-hidden="true"><span style={`width:${(r.current / max) * 100}%; background:${color}`}></span></span>
              </th>
              <td class="md__num md__strong">{fmt(r.current)}</td>
              <td class="md__num md__hide-sm">{fmt(r.previous)}</td>
              <td class="md__num">
                <span class={`md__delta ${good ? 'md__delta--good' : 'md__delta--bad'}`}>
                  <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><path d={up ? 'M5 1 L9 7 H1 Z' : 'M5 9 L9 3 H1 Z'} fill="currentColor" /></svg>
                  <span class="sr-only">{up ? 'up' : 'down'}</span>
                  {Math.abs(d).toFixed(1)}%
                </span>
              </td>
              <td class="md__hide-sm">{r.trend && <SparklineChart data={r.trend} size="sm" variant="line" label={`${r.label} trend`} color={good ? 'var(--chart-pos, #34c38f)' : 'var(--chart-neg, #f06a6a)'} />}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  </div>
</ChartFrame>

<style>
  .md { overflow-x: auto; }
  .md__table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
  .md__table th, .md__table td {
    padding: 0.7rem 0.6rem;
    border-bottom: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06);
    text-align: left;
    vertical-align: middle;
  }
  .md__table thead th { font-size: 0.6875rem; font-weight: 600; color: var(--c-text-3, #8a8a8a); text-transform: uppercase; letter-spacing: 0.04em; }
  .md__table tbody tr { transition: background 0.2s ease; }
  .md__table tbody tr:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.03); }
  .md__table tbody th { font-weight: 500; min-width: 9rem; }
  .md__label { display: block; color: var(--c-text-1, #fff); margin-bottom: 0.35rem; }
  .md__bar { display: block; height: 4px; border-radius: 2px; background: rgb(var(--ink-rgb, 255 255 255) / 0.05); overflow: hidden; }
  .md__bar span { display: block; height: 100%; border-radius: 2px; }
  .md__num { text-align: right !important; white-space: nowrap; color: var(--c-text-2, #a3a3a3); }
  .md__strong { color: var(--c-text-1, #fff); font-weight: 650; }
  .md__delta { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--c-text-1, #fff); font-weight: 600; }
  .md__delta--good svg { color: var(--chart-pos, #34c38f); }
  .md__delta--bad svg { color: var(--chart-neg, #f06a6a); }
  .sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  @media (max-width: 560px) { .md__hide-sm { display: none; } }
</style>
--- /** * MetricDeltaTable — Comparison table with inline bars, current vs previous * values and signed deltas with direction icons. A "table + chart" hybrid. * * @param {Array<{label: string, current: number, previous: number, invert?: boolean}>} rows - Metrics. * @param {string} currentLabel - Header for current values. * @param {string} previousLabel - Header for previous values. * @param {string} prefix - Value prefix. * @param {string} suffix - Value suffix. * @param {string} color - Inline bar color. * @param {string} variant - Card style (glass|solid|ghost). * @param {string} size - Density (sm|md|lg). */ import ChartFrame from './ChartFrame.astro'; import SparklineChart from './SparklineChart.astro'; import { formatValue } from './chart-utils'; interface Row { label: string; current: number; previous: number; invert?: boolean; trend?: number[] } interface Props { title?: string; subtitle?: string; rows?: Row[]; currentLabel?: string; previousLabel?: string; prefix?: string; suffix?: string; color?: string; variant?: 'glass' | 'solid' | 'ghost'; size?: 'sm' | 'md' | 'lg'; className?: string; } const { title = 'Channel performance', subtitle = 'Signups this month vs last month', rows = [ { label: 'Organic search', current: 4820, previous: 4310, trend: [30, 34, 33, 38, 41, 44, 48] }, { label: 'Direct', current: 3120, previous: 3290, trend: [36, 35, 34, 33, 33, 32, 31] }, { label: 'Newsletter', current: 1840, previous: 1220, trend: [10, 12, 13, 14, 16, 17, 18] }, { label: 'Paid social', current: 1310, previous: 1480, trend: [16, 15, 15, 14, 14, 13, 13] }, { label: 'Partners', current: 960, previous: 710, trend: [6, 7, 7, 8, 8, 9, 10] }, ], currentLabel = 'This month', previousLabel = 'Last month', prefix = '', suffix = '', color = 'var(--chart-1, #3987e5)', variant = 'glass', size = 'md', className = '', } = Astro.props; const max = Math.max(...rows.map((r) => r.current)) || 1; const fmt = (v: number) => formatValue(v, prefix, suffix, false); --- <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className}> <div class="md"> <table class="md__table"> <thead> <tr> <th scope="col">Metric</th> <th scope="col" class="md__num">{currentLabel}</th> <th scope="col" class="md__num md__hide-sm">{previousLabel}</th> <th scope="col" class="md__num">Change</th> <th scope="col" class="md__hide-sm"><span class="sr-only">Trend</span></th> </tr> </thead> <tbody> {rows.map((r) => { const d = r.previous ? ((r.current - r.previous) / r.previous) * 100 : 0; const up = d >= 0; const good = r.invert ? !up : up; return ( <tr> <th scope="row"> <span class="md__label">{r.label}</span> <span class="md__bar" aria-hidden="true"><span style={`width:${(r.current / max) * 100}%; background:${color}`}></span></span> </th> <td class="md__num md__strong">{fmt(r.current)}</td> <td class="md__num md__hide-sm">{fmt(r.previous)}</td> <td class="md__num"> <span class={`md__delta ${good ? 'md__delta--good' : 'md__delta--bad'}`}> <svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><path d={up ? 'M5 1 L9 7 H1 Z' : 'M5 9 L9 3 H1 Z'} fill="currentColor" /></svg> <span class="sr-only">{up ? 'up' : 'down'}</span> {Math.abs(d).toFixed(1)}% </span> </td> <td class="md__hide-sm">{r.trend && <SparklineChart data={r.trend} size="sm" variant="line" label={`${r.label} trend`} color={good ? 'var(--chart-pos, #34c38f)' : 'var(--chart-neg, #f06a6a)'} />}</td> </tr> ); })} </tbody> </table> </div> </ChartFrame> <style> .md { overflow-x: auto; } .md__table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; font-variant-numeric: tabular-nums; } .md__table th, .md__table td { padding: 0.7rem 0.6rem; border-bottom: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.06); text-align: left; vertical-align: middle; } .md__table thead th { font-size: 0.6875rem; font-weight: 600; color: var(--c-text-3, #8a8a8a); text-transform: uppercase; letter-spacing: 0.04em; } .md__table tbody tr { transition: background 0.2s ease; } .md__table tbody tr:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.03); } .md__table tbody th { font-weight: 500; min-width: 9rem; } .md__label { display: block; color: var(--c-text-1, #fff); margin-bottom: 0.35rem; } .md__bar { display: block; height: 4px; border-radius: 2px; background: rgb(var(--ink-rgb, 255 255 255) / 0.05); overflow: hidden; } .md__bar span { display: block; height: 100%; border-radius: 2px; } .md__num { text-align: right !important; white-space: nowrap; color: var(--c-text-2, #a3a3a3); } .md__strong { color: var(--c-text-1, #fff); font-weight: 650; } .md__delta { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--c-text-1, #fff); font-weight: 600; } .md__delta--good svg { color: var(--chart-pos, #34c38f); } .md__delta--bad svg { color: var(--chart-neg, #f06a6a); } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @media (max-width: 560px) { .md__hide-sm { display: none; } } </style>

Información Rápida

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