136 / Charts
Heat Calendar
A year of daily counts laid out as weeks, with a tooltip on every day and arrow keys that move through them.
1,532 commits in the last year
Hover a day, or tab in and use the arrow keys.
"use client"
import {
HeatCalendar,
type HeatCalendarDay,
} from "mischief-ui/heat-calendar"
const END = Date.UTC(2026, 10, 1)
function commits(): HeatCalendarDay[] {
let seed = 7
const random = () => {
seed = (seed * 16807) % 2147483647
return seed / 2147483647
}
return Array.from({ length: 365 }, (_, index) => {
const time = END - (364 - index) * 86_400_000
const weekday = new Date(time).getUTCDay()
const weekend = weekday === 0 || weekday === 6
const holiday = index > 270 && index < 284
const roll = random()
const count =
holiday || roll < (weekend ? 0.7 : 0.15)
? 0
: Math.round(random() * (weekend ? 3 : 9) + (index / 365) * 4)
return { date: new Date(time).toISOString().slice(0, 10), count }
})
}
const days = commits()
const total = days.reduce((sum, day) => sum + day.count, 0)
export function HeatCalendarDemo() {
return (
<div className="bg-card text-card-foreground border-border w-full max-w-[52rem] min-w-0 rounded-xl border p-4 md:p-6">
<h3 className="text-sm font-medium">
{total.toLocaleString("en-US")} commits in the last year
</h3>
<p className="text-muted-foreground mt-1 mb-4 text-sm">
Hover a day, or tab in and use the arrow keys.
</p>
<HeatCalendar
data={days}
end="2026-11-01"
label="Commits per day"
locale="en-US"
unit={{ one: "commit", other: "commits" }}
/>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/heat-calendarimport { HeatCalendar } from "mischief-ui/heat-calendar"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react" import { cn } from "@/lib/utils" export type HeatCalendarDay = { /** A calendar day as YYYY-MM-DD, read in UTC. */ date: string count: number} export type HeatCalendarProps = Omit< React.HTMLAttributes<HTMLDivElement>,Usage
export function Commits({ days }: { days: HeatCalendarDay[] }) {
return (
<HeatCalendar
data={days}
label="Commits per day"
unit={{ one: "commit", other: "commits" }}
/>
)
}Dates are days, read in UTC
Each entry is a calendar day written as YYYY-MM-DD and a count. Days are read and named in UTC, so a day never shifts by one because the reader's clock is in another time zone. Bucket your events into days on the server, in whatever time zone your product counts in, and pass the result. Two entries for the same day are added together.
<HeatCalendar
data={[
{ date: "2026-03-02", count: 4 },
{ date: "2026-03-03", count: 12 },
]}
start="2026-01-01"
end="2026-03-31"
/>Week start and names
Month and weekday names come from Intl in the given locale, and so does the first day of the week where the browser reports it. weekStart overrides it, with 0 for Sunday. unit names what is being counted, in its singular and plural forms, for the cell names and the tooltip.
How the shades are chosen
A day with nothing is drawn in the muted colour. Every other day falls into one of four steps by its share of the busiest day in range, each step the chart colour mixed further into the background. Because the mix is with the background, the scale works in a dark theme without a second palette.
On a narrow screen
The weeks scroll sideways while the weekday labels stay put, and the scroll starts at the newest week, since that is usually the one being checked.
API
dataHeatCalendarDay[]Daily counts as { date, count }, with date as YYYY-MM-DD.date, count (HeatCalendarDay)string, numberOne calendar day in UTC and its count.startstringFirst day shown. Defaults to 52 weeks before the end.endstringLast day shown. Defaults to the latest day in the data, or today.weekStartnumberFirst row, 0 for Sunday. Defaults to the locale's first day of the week.localestringThe locale for dates, weekdays, and numbers.unit{ one: string; other: string }What is counted. Defaults to contribution and contributions.labelstringThe accessible name of the grid. Defaults to Activity.colorstringAny CSS colour. Defaults to var(--chart-1), then var(--primary).classNamestringClasses for placement....divPropsHTMLAttributesNative div attributes.Accessibility
The calendar is a grid with one row per weekday, each row headed by the weekday's full name. Only one day is a tab stop. The up and down arrows move by a day, left and right by a week, Home and End to the first and last week on that weekday, and Ctrl+Home and Ctrl+End to the first and last day. Every day is named in full, such as 12 contributions on Tuesday, March 3, 2026, so the shade is never the only way to read it. The tooltip repeats that for pointer and keyboard users and closes on Escape. Month labels and the legend are decoration and hidden from assistive technology.