Skip to content

FRD: Cohort Heatmap

Document Summary

FieldDetails
Feature NameCohort Heatmap
StatusDraft
OwnerDavid Holmes
ContributorsDesign, Engineering
Target Releasev2.0.0 (P2)
Related LinksRoadmap item #50
Last Updated2026-05-26

Introduction

Overview

Cohort Heatmap is an analytics widget that renders a two-dimensional grid where rows represent user cohorts (grouped by signup period) and columns represent time periods after signup. Each cell is color-coded by intensity to show retention or engagement rates. The widget builds on the existing HeatStrip cell-rendering pattern and extends it into a full grid with row/column headers, a color legend, and an accessible summary.

Goals

  • Deliver a <CohortHeatmap> component that renders a grid of color-intensity cells with row and column headers.
  • Use design-system color tokens for cell tones with configurable intensity mapping.
  • Include a legend explaining the color scale.
  • Provide an accessible summary table for screen readers.
  • Ship Storybook stories with realistic cohort retention data.

Non-Goals

  • Interactive cell selection or drill-down to user lists.
  • Editable cells or inline data entry.
  • Server-side cohort calculation or query integration.
  • Pivot-table or sortable-column behavior (that is DataGrid territory).

Scope

In Scope

AreaDescription
Component<CohortHeatmap> rendering a color-coded grid of cohort data
Cell RenderingEach cell shows a background color based on intensity (0-1) and optional text value
HeadersRow headers for cohort names, column headers for time periods
LegendColor scale legend mapping intensity values to colors
Accessible SummaryVisually-hidden table with full data for screen readers
StatesLoading skeleton, empty state, error state
StoriesStorybook stories for retention, engagement, and edge cases

Out of Scope

AreaReason
Sortable columnsDataGrid concern; heatmap is read-only visualization
Cell editingHeatmap is a display-only widget
Custom cell renderersCells show color + optional value text; complex renderers deferred
Cohort calculation logicConsumer provides pre-calculated data

Users and Pain Points

User Groups

UserDescriptionNeeds
DevelopersEngineers building retention dashboardsA token-aligned cohort heatmap component
Growth PMsProduct managers tracking cohort behaviorVisual cohort retention grid with clear drop-off patterns
Data AnalystsDashboard consumersQuick scan of cohort trends via color intensity

Pain Points

UserPain PointImpact
DevelopersBuilding cohort heatmaps from scratch requires custom grid layout, color interpolation, and accessibility workHigh development cost, inconsistent results
Growth PMsExisting tables show raw numbers without visual intensity cuesSlower pattern recognition

Definitions

TermDefinition
CohortA group of users who share a common characteristic, typically signup date range
IntensityA normalized value (0 to 1) representing the metric strength for a cell
Retention RateThe percentage of a cohort still active after N time periods
Color ScaleA gradient mapping intensity values to background colors using design tokens

Current State

Existing Behavior

HeatStrip renders a single-row heatmap with intensity-based cell coloring. It uses a single color with opacity scaling (0.2 to 1.0) based on intensity. There is no multi-row grid heatmap component.

Current Limitations

  • HeatStrip is single-row only; no grid layout support.
  • No row/column header support.
  • No color legend component.
  • No accessible summary for multi-dimensional data.

Existing Workarounds

  • Teams use raw HTML tables with inline background-color styles.
  • Some teams use third-party heatmap libraries that do not align with DS tokens.

Proposed Solution

Summary

Introduce <CohortHeatmap> that accepts rows (cohort data with cell intensities), columnHeaders, and color configuration. The component renders a grid using the HeatStrip cell-rendering pattern (color + opacity scaling) extended to a 2D layout. A legend renders below the grid showing the color scale.

Key Capabilities

  • Two-dimensional grid with cohort rows and time-period columns.
  • Cell coloring via design-system color tokens with intensity-based opacity.
  • Optional text value overlay on cells (e.g., “45%”).
  • Color scale legend with min/max labels.
  • Visually-hidden data table for screen reader accessibility.
  • Sticky row headers for horizontal scrolling on wide grids.

User Experience

Users see a grid where the top-left cell is empty, the first row contains time-period headers (Week 1, Week 2, etc.), and each subsequent row is a cohort. Cells are colored from light (low intensity) to saturated (high intensity). Hovering a cell shows a tooltip with the exact value. A legend below explains the color scale.

Developer Experience

<CohortHeatmap
title="Weekly Retention"
columnHeaders={["Week 1", "Week 2", "Week 3", "Week 4"]}
rows={[
{
label: "Jan 2026",
cells: [
{ intensity: 1.0, value: "100%" },
{ intensity: 0.72, value: "72%" },
{ intensity: 0.55, value: "55%" },
{ intensity: 0.41, value: "41%" },
],
},
]}
color="primary"
/>

Requirements

IDRequirementPriorityNotes
FR-001CohortHeatmap renders a 2D grid with color-intensity cellsMust-
FR-002Row and column headers display cohort names and time periodsMust-
FR-003A color scale legend renders below the gridMust-
FR-004An accessible summary table is available for screen readersMust-
FR-005Cell tooltips show exact values on hoverShould-
FR-006Loading, empty, and error states are supportedMust-

Priority Definitions

PriorityMeaning
MustRequired for this feature to ship.
ShouldImportant, but can be deferred if needed.
CouldNice to have. Not required for initial release.

Functional Requirements

IDRequirementUser BenefitPriority
FUNC-001rows prop accepts { label: string; cells: { intensity: number; value?: string }[] }[]Typed cohort data structureMust
FUNC-002columnHeaders prop accepts string[] for time period labelsClear column identificationMust
FUNC-003Cell background color uses color token with opacity from intensity * 0.8 + 0.2Visual intensity mapping matching HeatStrip patternMust
FUNC-004Optional value text overlays on each cell in a readable contrast colorExact values visible without tooltipShould
FUNC-005Legend renders a gradient bar with “Low” and “High” labelsColor scale contextMust
FUNC-006Row headers stick on horizontal scroll for wide gridsCohort identification while scrollingShould
FUNC-007Hover tooltip shows row label, column header, and valuePrecise data point identificationShould

Non-Functional Requirements

IDRequirementCategoryPriority
NFR-001Renders in under 100ms for grids up to 52 rows x 12 columnsPerformanceMust
NFR-002Cell text meets WCAG AA contrast against the cell background colorAccessibilityMust
NFR-003Works in light and dark themesThemingMust
NFR-004No new runtime dependenciesMaintainabilityMust
NFR-005Grid scrolls horizontally on narrow viewports without layout breakageResponsivenessMust

API / Interface Requirements

Public API

NameTypeDescriptionRequired
titlestringHeatmap headingNo
columnHeadersstring[]Time period labels for columnsYes
rowsCohortRow[]Array of cohort rows with cellsYes
colorstringDesign token color class for cells (e.g., "primary", "success")No (default: "primary")
showValuesbooleanShow value text on cellsNo (default: false)
isLoadingbooleanShow loading skeletonNo
errorstringError messageNo
classNamestringAdditional CSS classesNo

CohortRow Type

interface CohortHeatmapCell {
intensity: number; // 0 to 1
value?: string; // display text, e.g. "72%"
tooltip?: string; // custom tooltip override
}
interface CohortRow {
label: string;
cells: CohortHeatmapCell[];
}

Example Usage

import { CohortHeatmap } from "@/components/ui/cohort-heatmap";
<CohortHeatmap
title="Monthly Retention"
columnHeaders={["Month 1", "Month 2", "Month 3"]}
rows={[
{ label: "Q1 2026", cells: [{ intensity: 1, value: "100%" }, { intensity: 0.65, value: "65%" }, { intensity: 0.42, value: "42%" }] },
{ label: "Q2 2026", cells: [{ intensity: 1, value: "100%" }, { intensity: 0.71, value: "71%" }] },
]}
showValues
color="primary"
/>

API Notes

  • Rows with fewer cells than columnHeaders.length render empty cells for missing positions.
  • intensity is clamped to 0-1 internally.
  • color maps to a design-system token class, matching HeatStrip’s color prop pattern.

Accessibility Requirements

IDRequirementNotes
A11Y-001A visually-hidden &lt;table&gt; presents complete grid data to screen readersIncludes row headers, column headers, and cell values
A11Y-002Cell value text meets WCAG AA contrast ratio against the cell backgroundAuto-switch to dark or light text based on intensity
A11Y-003Legend is described in a way accessible to screen readersaria-label on legend element
A11Y-004Tooltip content is accessible via keyboard focusCells are focusable; tooltip shows on focus
A11Y-005Grid has role="img" with descriptive aria-label for the visual representationSeparate from the accessible table

Checklist

  • Keyboard support is defined.
  • Focus behavior is defined.
  • Screen reader behavior is defined.
  • Color contrast requirements are met.
  • Reduced motion behavior is considered.
  • Semantic HTML expectations are documented.
  • ARIA usage is defined only where needed.

Content and Documentation Requirements

IDRequirementLocationPriority
DOC-001Storybook docs page with overview and props tableStorybookMust
DOC-002”When to use / When not to use” guidanceStorybook docsMust
DOC-003Stories with realistic retention and engagement cohort dataStorybookMust
DOC-004Story demonstrating horizontal scroll behavior on wide gridsStorybookShould
DOC-005Story demonstrating showValues modeStorybookMust

Dependencies

DependencyTypeOwnerStatusNotes
HeatStrip cell patternEngineeringDesign SystemReadyReference for intensity-to-opacity mapping
Design tokens (color, spacing)DesignDesign SystemReadyCell coloring and grid spacing

Risks and Tradeoffs

Risk / TradeoffImpactMitigation
Large grids (52x12) may be slow to render with many DOM nodesPerformance degradationVirtualize rows if grid exceeds a threshold; benchmark during implementation
Opacity-based coloring may not provide enough differentiation at extremesHard to distinguish 0.8 from 0.9 intensityConsider a stepped color scale with 5-6 discrete levels as an alternative
Cell value text on colored backgrounds may have contrast issuesAccessibility failureAuto-switch text color to white or black based on computed background luminance

Open Questions

IDQuestionOwnerStatusResolution
Q-001Should a continuous gradient or discrete stepped color scale be the default?David HolmesOpen
Q-002Should row virtualization be included in v1 or deferred?David HolmesOpen
Q-003Should the heatmap support a secondary color for a “diverging” scale (positive/negative values)?David HolmesOpen

Acceptance Criteria

IDCriteriaRelated Requirement
AC-001Grid renders with correct row and column headersFR-001, FR-002
AC-002Cell colors reflect intensity values using the specified color tokenFUNC-003
AC-003Color legend renders with Low/High labelsFR-003
AC-004Visually-hidden table presents data to screen readersFR-004
AC-005showValues displays text on cells with adequate contrastFUNC-004, NFR-002
AC-006Grid scrolls horizontally on narrow viewports with sticky row headersFUNC-006, NFR-005
AC-007Loading, empty, and error states render correctlyFR-006
AC-008All Storybook stories render without errorsDOC-003

LLM Handoff Instructions

Expected LLM Behavior

  • Follow the requirements and acceptance criteria in this document.
  • Do not expand scope beyond the In Scope section.
  • Respect the Out of Scope section.
  • Reference HeatStrip for the intensity-to-opacity mapping pattern (0.2 + intensity * 0.8).
  • Use CSS position: sticky for row header pinning on horizontal scroll.
  • Auto-switch cell text color for contrast: use white text when intensity > 0.6, dark text otherwise.
  • Include a visually-hidden &lt;table&gt; with complete data for screen readers.
  • Place stories under the SaaS Widgets Storybook section.

LLM Should Not

  • Use a third-party heatmap library.
  • Build interactive cell selection or editing.
  • Modify the existing HeatStrip component.
  • Add sorting or filtering to the grid.

Decision Log

DateDecisionReasonOwner
2026-05-26Use opacity scaling matching HeatStrip rather than a discrete stepped paletteConsistency with existing DS pattern; discrete steps can be added laterDavid Holmes
2026-05-26Provide a visually-hidden table rather than making the visual grid a &lt;table&gt;Visual grid uses CSS Grid for flexible sizing; accessible table is a separate semantic elementDavid Holmes

Document History

DateAuthorChange
2026-05-26David HolmesInitial draft