1 estrella en GitHub = 1 componente nuevo.

Dar una estrella
Gráficos y Estadísticaschartsvgzero-dependencycohortretentionsaastableheatmap

Retention Cohort Table

Matriz triangular de retención por cohortes sombreada en un solo tono con medias por periodo.

Vista Previa en Vivo

User retention

Monthly cohorts, % still active

User retention: retention percentage by cohort and period
CohortUsersM0M1M2M3M4M5
May1,240100%62%48%41%37%35%
Jun1,410100%65%51%44%40%
Jul1,580100%68%55%47%
Aug1,720100%71%57%
Sep1,890100%73%
Oct2,050100%
Average100%68%53%44%39%35%

Uso

Copia y pega este código directamente en tu componente React:

tsx
import { RetentionCohortTable } from 'react-component-kit-ui/charts';

<RetentionCohortTable cohorts={[
    { cohort: 'Aug', size: 1720, retention: [100, 71, 57] },
    { cohort: 'Sep', size: 1890, retention: [100, 73] },
    { cohort: 'Oct', size: 2050, retention: [100] },
  ]} />

Instalación Manual

Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/RetentionCohortTable.tsx + RetentionCohortTable.css:

RetentionCohortTable.tsx
'use client';

import type { CSSProperties } from 'react';
import ChartFrame from './ChartFrame';
import './RetentionCohortTable.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;
}

/**
 * RetentionCohortTable — Triangular cohort retention matrix. Each row is a
 * signup cohort, each column a period since signup, shaded on a single-hue ramp.
 *
 * @param {Array<{cohort: string, size: number, retention: number[]}>} cohorts - Retention % per period (period 0 = 100).
 * @param {string} periodLabel - Column prefix (e.g. 'Week', 'Month').
 * @param {string} color - Ramp hue.
 * @param {string} variant - Card style (glass|solid|ghost).
 * @param {string} size - Density (sm|md|lg).
 */
export interface RetentionCohortTableProps {
  title?: string;
  subtitle?: string;
  cohorts?: Array<{ cohort: string; size: number; retention: number[] }>;
  periodLabel?: string;
  color?: string;
  variant?: 'glass' | 'solid' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
}

export function RetentionCohortTable({
  title = 'User retention',
  subtitle = 'Monthly cohorts, % still active',
  cohorts = [
    { cohort: 'May', size: 1240, retention: [100, 62, 48, 41, 37, 35] },
    { cohort: 'Jun', size: 1410, retention: [100, 65, 51, 44, 40] },
    { cohort: 'Jul', size: 1580, retention: [100, 68, 55, 47] },
    { cohort: 'Aug', size: 1720, retention: [100, 71, 57] },
    { cohort: 'Sep', size: 1890, retention: [100, 73] },
    { cohort: 'Oct', size: 2050, retention: [100] },
  ],
  periodLabel = 'M',
  color = 'var(--chart-3, #199e70)',
  variant = 'glass',
  size = 'md',
  className = '',
}: RetentionCohortTableProps) {
  const periods = Math.max(...cohorts.map((c) => c.retention.length));
  const cols = Array.from({ length: periods }, (_, i) => `${periodLabel}${i}`);
  const shade = (v: number) => `color-mix(in srgb, ${color} ${Math.round(10 + (v / 100) * 85)}%, #141416)`;
  const avg = cols.map((_, i) => {
    const vals = cohorts.map((c) => c.retention[i]).filter((v) => v !== undefined);
    return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
  });
  return (
    <>
      <ChartFrame title={title} subtitle={subtitle} variant={variant} size={size} className={className}>
        <div className="rc" data-rk-14b914="">
          <table className="rc__table" data-rk-14b914="">
            <caption className="sr-only" data-rk-14b914="">{title}: retention percentage by cohort and period</caption>
            <thead data-rk-14b914="">
              <tr data-rk-14b914="">
                <th scope="col" data-rk-14b914="">Cohort</th>
                <th scope="col" data-rk-14b914="">Users</th>
                {cols.map((c, __i: number) => <th key={__i} scope="col" data-rk-14b914="">{c}</th>)}
              </tr>
            </thead>
            <tbody data-rk-14b914="">
              {cohorts.map((c, __i: number) => (
                <tr key={__i} data-rk-14b914="">
                  <th scope="row" data-rk-14b914="">{c.cohort}</th>
                  <td className="rc__size" data-rk-14b914="">{c.size.toLocaleString('en-US')}</td>
                  {cols.map((_, i) => {
                    const v = c.retention[i];
                    return v === undefined
                      ? <td className="rc__empty" aria-label="no data yet" data-rk-14b914=""></td>
                      : <td className="rc__cell" style={st(`background:${shade(v)}`)} data-strong={v > 55 ? '' : undefined} data-rk-14b914="">{v}%</td>;
                  })}
                </tr>
              ))}
            </tbody>
            <tfoot data-rk-14b914="">
              <tr data-rk-14b914="">
                <th scope="row" data-rk-14b914="">Average</th>
                <td data-rk-14b914=""></td>
                {avg.map((a, __i: number) => <td key={__i} className="rc__avg" data-rk-14b914="">{a.toFixed(0)}%</td>)}
              </tr>
            </tfoot>
          </table>
        </div>
      </ChartFrame>
    </>
  );
}

export default RetentionCohortTable;
RetentionCohortTable.css
/* RetentionCohortTable — ported from astro-component-kit (charts/RetentionCohortTable.astro) */

.rc:where([data-rk-14b914]) { overflow-x: auto; }
.rc__table:where([data-rk-14b914]) {
  width: 100%;
  border-collapse: separate;
  border-spacing: 3px;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}
.rc__table:where([data-rk-14b914]) th:where([data-rk-14b914]) {
  padding: 0.4rem 0.5rem;
  font-weight: 600;
  color: var(--c-text-3, #8a8a8a);
  text-align: center;
  white-space: nowrap;
}
.rc__table:where([data-rk-14b914]) tbody:where([data-rk-14b914]) th:where([data-rk-14b914]), .rc__table:where([data-rk-14b914]) tfoot:where([data-rk-14b914]) th:where([data-rk-14b914]) { text-align: left; color: var(--c-text-1, #fff); }
.rc__size:where([data-rk-14b914]) { color: var(--c-text-2, #a3a3a3); text-align: right; padding-right: 0.6rem; }
.rc__cell:where([data-rk-14b914]) {
  min-width: 3rem;
  padding: 0.55rem 0.4rem;
  text-align: center;
  border-radius: 4px;
  color: rgb(var(--ink-rgb, 255 255 255) / 0.78);
}
.rc__cell[data-strong]:where([data-rk-14b914]) { color: #fff; font-weight: 650; }
.rc__empty:where([data-rk-14b914]) { background: rgb(var(--ink-rgb, 255 255 255) / 0.02); border-radius: 4px; }
.rc__avg:where([data-rk-14b914]) { text-align: center; color: var(--c-text-1, #fff); font-weight: 600; padding-top: 0.5rem; }
.sr-only:where([data-rk-14b914]) {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

Información Rápida

Categoría
Gráficos y Estadísticas
Nombre de archivo
RetentionCohortTable.tsx
Dependencias
Ninguna — React + CSS puro
Etiquetas
chartsvgzero-dependencycohortretentionsaastableheatmap