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
Installation
npx shadcn@latest add https://r.assistant-ui.com/heat-graphThis installs a pre-styled HeatGraph component to components/assistant-ui/heat-graph.tsx along with the heat-graph package.
npm install heat-graphpnpm add heat-graphyarn add heat-graphQuick Start
"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.
| Prop | Type | Default | Description |
|---|---|---|---|
data | DataPoint[] | required | Array of { date: string | Date, count: number } |
start | string | Date | 1 year before end | Start of the date range |
end | string | Date | today | End of the date range |
weekStart | "sunday" | "monday" | "sunday" | First day of the week |
classify | ClassifyFn | autoLevels(5) | Bucketing function mapping counts to levels |
colorScale | string[] | — | 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.