heat-graph

Headless, composable activity heatmap components for React.

heat-graph provides headless, Radix-style primitives for building GitHub-style activity heatmap graphs.

  • Composable — Radix-style compound components you fully control
  • Headless — Zero styling opinions, bring your own CSS/Tailwind
  • Tooltip built-in — Powered by Radix Popper for positioning
  • Customizable bucketing — Plug in your own classification function
MarAprMayJunJulAugSepOctNovDecJanFebMar
MonWedFri
Less
More

Installation

npx shadcn@latest add https://r.assistant-ui.com/heat-graph

This installs a pre-styled HeatGraph component to components/assistant-ui/heat-graph.tsx along with the heat-graph package.

npm install heat-graph
pnpm add heat-graph
yarn add heat-graph

Quick Start

components/activity-graph.tsx
"use client";

import * as HeatGraph from "heat-graph";

const COLORS = ["#ebedf0", "#c6d7f9", "#8fb0f3", "#5888e8", "#2563eb"];

export function ActivityGraph({ data }: { data: HeatGraph.DataPoint[] }) {
  return (
    <HeatGraph.Root data={data} weekStart="monday" colorScale={COLORS}>
      <HeatGraph.Grid className="gap-[3px]">
        {({ cells }) =>
          cells.map((cell) => (
            <HeatGraph.Cell
              key={`${cell.column}-${cell.row}`}
              className="aspect-square rounded-sm"
            />
          ))
        }
      </HeatGraph.Grid>
      <HeatGraph.Tooltip>
        {({ cell }) => (
          <div>
            {cell.count} contributions on {cell.date.toLocaleDateString()}
          </div>
        )}
      </HeatGraph.Tooltip>
    </HeatGraph.Root>
  );
}

Components using Heat Graph must be Client Components ("use client"), since they rely on React Context and interactivity.

Anatomy

import * as HeatGraph from "heat-graph";

<HeatGraph.Root data={data} colorScale={colors}>

  {/* Month labels */}
  <HeatGraph.MonthLabels>
    {({ labels, totalWeeks }) => labels.map((l) => (
      <span key={l.column} style={{ left: `${(l.column / totalWeeks) * 100}%` }}>
        {HeatGraph.MONTH_SHORT[l.month]}
      </span>
    ))}
  </HeatGraph.MonthLabels>

  {/* Day-of-week labels */}
  <HeatGraph.DayLabels>
    {({ labels }) => labels.map((l) => (
      <span key={l.row}>{HeatGraph.DAY_SHORT[l.dayOfWeek]}</span>
    ))}
  </HeatGraph.DayLabels>

  {/* Grid + Cells */}
  <HeatGraph.Grid>
    {({ cells }) => cells.map((cell) => (
      <HeatGraph.Cell key={`${cell.column}-${cell.row}`} />
    ))}
  </HeatGraph.Grid>

  {/* Legend */}
  <HeatGraph.Legend>
    {({ items }) => items.map((item) => (
      <HeatGraph.LegendLevel key={item.level} />
    ))}
  </HeatGraph.Legend>

  {/* Tooltip */}
  <HeatGraph.Tooltip>
    {({ cell }) => <div>{cell.count} on {cell.date.toLocaleDateString()}</div>}
  </HeatGraph.Tooltip>

</HeatGraph.Root>

API Reference

Root

The top-level provider. Renders a <div> that computes the grid layout and provides state to all children. Accepts all standard div props.

PropTypeDefaultDescription
dataDataPoint[]requiredArray of { date: string | Date, count: number }
startstring | Date1 year before endStart of the date range
endstring | DatetodayEnd of the date range
weekStart"sunday" | "monday""sunday"First day of the week
classifyClassifyFnautoLevels(5)Bucketing function mapping counts to levels
colorScalestring[]Array of colors, one per level (index 0 = lowest)

Grid

A <div> with CSS Grid layout. Renders gridTemplateColumns and gridTemplateRows based on the computed data. Accepts all standard div props.

Provides a CellCollection via render prop. The .map() callback receives a CellData object and automatically wraps each element in the context needed by Cell.

type CellData = {
  date: Date;
  count: number;
  level: number;
  column: number;
  row: number;
};

Cell

A <div> that reads from cell context. Automatically applies:

  • Grid positioning (gridColumn, gridRow)
  • Background color from colorScale
  • Tooltip hover handlers

Accepts all standard div props. Pass colorScale to override the Root-level color scale.

MonthLabels

Render prop component providing month label data.

<HeatGraph.MonthLabels>
  {({ labels, totalWeeks }) =>
    labels.map((label) => (
      <span
        key={label.column}
        style={{ left: `${(label.column / totalWeeks) * 100}%` }}
      >
        {HeatGraph.MONTH_SHORT[label.month]}
      </span>
    ))
  }
</HeatGraph.MonthLabels>

Provides { labels, totalWeeks }. Each label has { month: number, column: number }. Use totalWeeks to compute label positions. Use MONTH_SHORT[label.month] for English labels, or format with Intl.DateTimeFormat for localization.

DayLabels

Render prop component providing day-of-week label data.

<HeatGraph.DayLabels>
  {({ labels }) =>
    labels.map((label) => (
      <span key={label.row}>{HeatGraph.DAY_SHORT[label.dayOfWeek]}</span>
    ))
  }
</HeatGraph.DayLabels>

Each label has { dayOfWeek: number, row: number } where dayOfWeek is 0=Sun..6=Sat. Use DAY_SHORT[label.dayOfWeek] for English labels, or format with Intl.DateTimeFormat for localization.

Legend

Render prop component providing legend items. Each item has { level: number, color: string | undefined }.

LegendLevel

A <div> that reads from legend item context. Automatically applies backgroundColor from the color scale. Use inside Legend's .map().

Tooltip

Renders only when a cell is hovered. Positioned by Radix Popper relative to the hovered cell. Accepts Radix Popper Content props (side, sideOffset, align, etc.).

<HeatGraph.Tooltip side="top" sideOffset={8} className="...">
  {({ cell }) => <div>{cell.count} contributions</div>}
</HeatGraph.Tooltip>

autoLevels(n)

Default classification function. Maps counts into n evenly-distributed levels (0 to n-1). Level 0 is always count 0.

type ClassifyFn = (counts: number[]) => (count: number) => number;

To provide a custom classifier:

const myClassify: HeatGraph.ClassifyFn = (counts) => {
  const p75 = percentile(counts, 75);
  return (count) => {
    if (count === 0) return 0;
    if (count < p75 * 0.25) return 1;
    if (count < p75 * 0.5) return 2;
    if (count < p75) return 3;
    return 4;
  };
};

<HeatGraph.Root data={data} classify={myClassify}>

MONTH_SHORT

English month abbreviations array: ["Jan", "Feb", ..., "Dec"]. Index by MonthLabel.month.

DAY_SHORT

English day abbreviations array: ["Sun", "Mon", ..., "Sat"]. Index by DayLabel.dayOfWeek.