← Files DataARCHIVED FILE

templates/data-app/examples/business-performance/content/dashboard/CohortTable.jsx

3.02 KB · Sep 30, 2026 · 23:19 UTC

↓ Download file

import React, { useMemo } from "react";
import { ChartMark, ChartTooltip } from "../../data-app-public.jsx";
import { cohortCellId } from "./retention-interaction.js";

export function CohortTooltip({ cell }) {
  const measuredAt = new Date(`${cell.cutoff}T00:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
  return <ChartTooltip active label={`${cell.cohortLabel} · Month ${cell.age}`} headerValue={`${Math.round(cell.retentionRate * 100)}%`}
    details={[{ label: "Retained", value: `${cell.retainedAccounts} of ${cell.eligibleAccounts} customers` }, { label: "Measured", value: measuredAt }]} />;
}

export function CohortTable({ cells, selectedCell, onSelect }) {
  const rows = useMemo(() => {
    const grouped = new Map();
    for (const cell of cells) {
      if (!grouped.has(cell.cohort)) grouped.set(cell.cohort, []);
      grouped.get(cell.cohort).push(cell);
    }
    return [...grouped.values()];
  }, [cells]);
  const ages = [...new Set(cells.map(cell => cell.age))].sort((a,b) => a-b);
  const date = value => new Date(`${value}T00:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
  return <div className="bp-cohort-scroll" tabIndex={0} role="region" aria-label="Customer retention by cohort" data-reviewed-rows data-chart-interaction-root>
    <table className="bp-cohort-table">
      <thead><tr><th scope="col">First paid</th><th scope="col">Customers</th>{ages.map(age => <th key={age} scope="col">Month {age}</th>)}</tr></thead>
      <tbody>{rows.map(row => <tr key={row[0].cohort}>
        <th scope="row">{row[0].cohortLabel}</th><td className="bp-cohort-size">{row[0].eligibleAccounts}</td>
        {ages.map(age => {
          const cell = row.find(item => item.age === age);
          const observed = cell?.mature && cell.retentionRate != null;
          const value = observed ? `${Math.round(cell.retentionRate * 100)}%` : "—";
          const description = observed ? `${cell.cohortLabel}, Month ${age}: ${value}, ${cell.retainedAccounts} of ${cell.eligibleAccounts} customers retained on ${date(cell.cutoff)}` : `${row[0].cohortLabel}, Month ${age}: not yet observed`;
          return <td key={age} data-observed={observed || undefined} style={observed ? { "--cohort-fill": `color-mix(in srgb, var(--chart-1) ${8 + Math.max(0, Math.min(1, cell.retentionRate)) * 42}%, var(--surface))` } : undefined}>
            {observed ? <ChartMark id={cohortCellId(cell)} aria-label={description}
              aria-pressed={selectedCell?.cohort === cell.cohort && selectedCell?.age === age}
              context={{ kind: "chart", chartType: "heatmap", label: `${cell.cohortLabel} · Month ${age}`, series: "Paid-customer retention", value,
                row: cell, actions: [{ label: "View customers", onSelect: () => onSelect(cell) }] }} tooltip={<CohortTooltip cell={cell} />}>
              {value}</ChartMark> : <span aria-label={description}>{value}</span>}
          </td>;
        })}
      </tr>)}</tbody>
    </table>
  </div>;
}

SHA-256: ba55735a9deab14b54e20f59a80d4d4683ccd60e144f8593db8eb497a871fe12