Gráficos y Estadísticaschartsvgzero-dependencytablecomparisondeltasparkline
Metric Delta Table
Tabla comparativa con barras en línea, valores actuales y previos, variaciones y sparklines.
Vista Previa en Vivo
Signups this month vs last month
| Metric | This month | Last month | Change | Trend |
|---|---|---|---|---|
| Organic search | 4,820 | 4,310 | up11.8% | |
| Direct | 3,120 | 3,290 | down5.2% | |
| Newsletter | 1,840 | 1,220 | up50.8% | |
| Paid social | 1,310 | 1,480 | down11.5% | |
| Partners | 960 | 710 | up35.2% |
Uso
Copia y pega este código directamente en tu componente React:
tsx
import { MetricDeltaTable } from 'react-component-kit-ui/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/MetricDeltaTable.tsx + MetricDeltaTable.css:
MetricDeltaTable.tsx
'use client';
import type { CSSProperties } from 'react';
import ChartFrame from './ChartFrame';
import SparklineChart from './SparklineChart';
import { formatValue } from './chart-utils';
import './MetricDeltaTable.css';
/** "a: b; --c: d" → style object */
function st(css: string | undefined): CSSProperties {
const o: Record<string, string> = {};
for (const decl of String(css ?? '').split(';')) {
const i = decl.indexOf(':');
if (i < 0) continue;
const k = decl.slice(0, i).trim();
if (k) o[k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(i + 1).trim();
}
return o as CSSProperties;
}
interface Row { label: string; current: number; previous: number; invert?: boolean; trend?: number[] }
/**
* 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).
*/
export interface MetricDeltaTableProps {
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;
}
export function MetricDeltaTable({
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 = '',
}: MetricDeltaTableProps) {
const max = Math.max(...rows.map((r) => r.current)) || 1;
const fmt = (v: number) => formatValue(v, prefix, suffix, false);
return (
<>
<ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className}>
<div className="md" data-rk-df9b34="">
<table className="md__table" data-rk-df9b34="">
<thead data-rk-df9b34="">
<tr data-rk-df9b34="">
<th scope="col" data-rk-df9b34="">Metric</th>
<th scope="col" className="md__num" data-rk-df9b34="">{currentLabel}</th>
<th scope="col" className="md__num md__hide-sm" data-rk-df9b34="">{previousLabel}</th>
<th scope="col" className="md__num" data-rk-df9b34="">Change</th>
<th scope="col" className="md__hide-sm" data-rk-df9b34=""><span className="sr-only" data-rk-df9b34="">Trend</span></th>
</tr>
</thead>
<tbody data-rk-df9b34="">
{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 data-rk-df9b34="">
<th scope="row" data-rk-df9b34="">
<span className="md__label" data-rk-df9b34="">{r.label}</span>
<span className="md__bar" aria-hidden="true" data-rk-df9b34=""><span style={st(`width:${(r.current / max) * 100}%; background:${color}`)} data-rk-df9b34=""></span></span>
</th>
<td className="md__num md__strong" data-rk-df9b34="">{fmt(r.current)}</td>
<td className="md__num md__hide-sm" data-rk-df9b34="">{fmt(r.previous)}</td>
<td className="md__num" data-rk-df9b34="">
<span className={`md__delta ${good ? 'md__delta--good' : 'md__delta--bad'}`} data-rk-df9b34="">
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true" data-rk-df9b34=""><path d={up ? 'M5 1 L9 7 H1 Z' : 'M5 9 L9 3 H1 Z'} fill="currentColor" data-rk-df9b34="" /></svg>
<span className="sr-only" data-rk-df9b34="">{up ? 'up' : 'down'}</span>
{Math.abs(d).toFixed(1)}%
</span>
</td>
<td className="md__hide-sm" data-rk-df9b34="">{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>
</>
);
}
export default MetricDeltaTable;MetricDeltaTable.css
/* MetricDeltaTable — ported from astro-component-kit (charts/MetricDeltaTable.astro) */
.md:where([data-rk-df9b34]) { overflow-x: auto; }
.md__table:where([data-rk-df9b34]) { width: 100%; border-collapse: collapse; font-size: 0.8125rem; font-variant-numeric: tabular-nums; }
.md__table:where([data-rk-df9b34]) th:where([data-rk-df9b34]), .md__table:where([data-rk-df9b34]) td:where([data-rk-df9b34]) {
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:where([data-rk-df9b34]) thead:where([data-rk-df9b34]) th:where([data-rk-df9b34]) { font-size: 0.6875rem; font-weight: 600; color: var(--c-text-3, #8a8a8a); text-transform: uppercase; letter-spacing: 0.04em; }
.md__table:where([data-rk-df9b34]) tbody:where([data-rk-df9b34]) tr:where([data-rk-df9b34]) { transition: background 0.2s ease; }
.md__table:where([data-rk-df9b34]) tbody:where([data-rk-df9b34]) tr:hover:where([data-rk-df9b34]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.03); }
.md__table:where([data-rk-df9b34]) tbody:where([data-rk-df9b34]) th:where([data-rk-df9b34]) { font-weight: 500; min-width: 9rem; }
.md__label:where([data-rk-df9b34]) { display: block; color: var(--c-text-1, #fff); margin-bottom: 0.35rem; }
.md__bar:where([data-rk-df9b34]) { display: block; height: 4px; border-radius: 2px; background: rgb(var(--ink-rgb, 255 255 255) / 0.05); overflow: hidden; }
.md__bar:where([data-rk-df9b34]) span:where([data-rk-df9b34]) { display: block; height: 100%; border-radius: 2px; }
.md__num:where([data-rk-df9b34]) { text-align: right !important; white-space: nowrap; color: var(--c-text-2, #a3a3a3); }
.md__strong:where([data-rk-df9b34]) { color: var(--c-text-1, #fff); font-weight: 650; }
.md__delta:where([data-rk-df9b34]) { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--c-text-1, #fff); font-weight: 600; }
.md__delta--good:where([data-rk-df9b34]) svg:where([data-rk-df9b34]) { color: var(--chart-pos, #34c38f); }
.md__delta--bad:where([data-rk-df9b34]) svg:where([data-rk-df9b34]) { color: var(--chart-neg, #f06a6a); }
.sr-only:where([data-rk-df9b34]) {
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:where([data-rk-df9b34]) { display: none; } }Información Rápida
- Categoría
- Gráficos y Estadísticas
- Nombre de archivo
MetricDeltaTable.tsx- Dependencias
- Ninguna — React + CSS puro
- Etiquetas
- chartsvgzero-dependencytablecomparisondeltasparkline