1 GitHub star = 1 new component.

Star the repo
Charts & Stats chartsvgzero-dependencytablecomparisondeltasparkline

Metric Delta Table

Comparison table with inline bars, current vs previous values, signed deltas and sparklines.

Live Preview

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%

Usage

Copy and paste this code into your Astro page:

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 }, ]} />

Manual Installation

If you are not using the npm package, create a new file and paste the following code: 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>

Quick Info

Category
Charts & Stats
Filename
MetricDeltaTable.astro
Dependencies
apexcharts (peer)
Tags
chartsvgzero-dependencytablecomparisondeltasparkline