import { HeatGraphDemo } from "@/app/heat-graph/heat-graph-demo";
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#
<Tabs items={["shadcn", "npm", "pnpm", "yarn"]}>
sh 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.
sh npm install heat-graph
sh pnpm add heat-graph
sh yarn add heat-graph
Quick 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]">
{() => (
<HeatGraph.Cell className="aspect-square rounded-sm" />
)}
</HeatGraph.Grid>
<HeatGraph.Tooltip>
{({ cell }) => (
<div>
{cell.count} contributions on {cell.date.toLocaleDateString()}
</div>
)}
</HeatGraph.Tooltip>
</HeatGraph.Root>
);
}
Anatomy#
import * as HeatGraph from "heat-graph";
<HeatGraph.Root data={data} colorScale={colors}>
{/* Month labels — iterates internally, renders each label */}
<HeatGraph.MonthLabels>
{({ label, totalWeeks }) => (
<span style={{ left: `${(label.column / totalWeeks) * 100}%` }}>
{HeatGraph.MONTH_SHORT[label.month]}
</span>
)}
</HeatGraph.MonthLabels>
{/* Day-of-week labels — iterates internally, renders each label */}
<HeatGraph.DayLabels>
{({ label }) => (
<span>{HeatGraph.DAY_SHORT[label.dayOfWeek]}</span>
)}
</HeatGraph.DayLabels>
{/* Grid + Cells — iterates internally, renders each cell */}
<HeatGraph.Grid>
{() => <HeatGraph.Cell />}
</HeatGraph.Grid>
{/* Legend — iterates internally, renders each level */}
<HeatGraph.Legend>
{() => <HeatGraph.LegendLevel />}
</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.
Iterates over cells internally, calling the children render function for each cell. Each cell is wrapped in a context that Cell reads from.
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#
Iterates over month labels, calling the children render function for each label.
<HeatGraph.MonthLabels>
{({ label, totalWeeks }) => (
<span style={{ left: `${(label.column / totalWeeks) * 100}%` }}>
{HeatGraph.MONTH_SHORT[label.month]}
</span>
)}
</HeatGraph.MonthLabels>
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#
Iterates over day-of-week labels, calling the children render function for each label.
<HeatGraph.DayLabels>
{({ label }) => (
<span>{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#
Iterates over legend levels, calling the children render function for each item. 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.
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.