Heatmap

Intensity across two dimensions, as a matrix, a calendar of days or a cohort retention triangle.

Pattern

≈ 3,686 tokens per use (tool 1,279 · guidance 323 · typical payload 2,084)How token costs are estimated

Example: Heatmap

When do customers order most?

When customers order

Last 8 weeks

Highest: Fri 7–8 PM · 1,240 orders

12 AM1 AM2 AM3 AM4 AM5 AM6 AM7 AM8 AM9 AM10 AM11 AM12 PM1 PM2 PM3 PM4 PM5 PM6 PM7 PM8 PM9 PM10 PM11 PM
Mon
Tue
Wed
Thu
Fri
Sat
Sun
  • 0–250
  • 250–500
  • 500–750
  • 750–1,000
  • 1,000–1,250

Arrow keys move between cells. Press Enter to choose one.

Highlighted: Friday, 7 PM, 1,240 orders (Busiest)

When customers order
12 AM1 AM2 AM3 AM4 AM5 AM6 AM7 AM8 AM9 AM10 AM11 AM12 PM1 PM2 PM3 PM4 PM5 PM6 PM7 PM8 PM9 PM10 PM11 PM
Mon84 orders47 orders30 orders20 orders19 orders38 orders96 orders199 orders321 orders394 orders436 orders536 orders652 orders533 orders434 orders421 orders466 orders592 orders780 orders1,004 orders853 orders678 orders406 orders197 orders
Tue87 orders49 orders30 orders20 orders19 orders41 orders100 orders210 orders332 orders412 orders461 orders584 orders672 orders526 orders444 orders431 orders483 orders582 orders787 orders1,013 orders883 orders671 orders425 orders216 orders
Wed95 orders53 orders31 orders21 orders20 orders40 orders108 orders216 orders335 orders421 orders473 orders572 orders698 orders592 orders481 orders470 orders517 orders657 orders828 orders1,044 orders949 orders730 orders464 orders225 orders
Thu101 orders58 orders35 orders22 orders23 orders43 orders112 orders231 orders375 orders448 orders524 orders660 orders740 orders630 orders510 orders497 orders557 orders684 orders901 orders1,093 orders965 orders730 orders497 orders241 orders
Fri113 orders60 orders38 orders25 orders25 orders52 orders122 orders271 orders414 orders508 orders576 orders725 orders795 orders709 orders571 orders549 orders591 orders750 orders993 orders1,240 orders1,148 orders859 orders540 orders271 orders
Sat74 orders42 orders25 orders16 orders16 orders33 orders84 orders176 orders263 orders328 orders375 orders666 orders750 orders723 orders614 orders589 orders640 orders683 orders931 orders1,121 orders1,016 orders816 orders521 orders236 orders
Sun62 orders35 orders21 orders14 orders14 orders28 orders73 orders152 orders241 orders285 orders335 orders576 orders674 orders632 orders539 orders526 orders554 orders608 orders856 orders1,043 orders935 orders688 orders432 orders221 orders

Choose a cell in the grid.

When to use

  • The answer is how a number varies across two dimensions: orders by day and hour, sales by product and region, usage by feature and segment.
  • Activity per day over weeks or months: commits, workouts, sales days.
  • Retention by signup cohort over periods.
  • The user should pick a slot from the pattern, such as a time to schedule a campaign.

When not to use

  • One dimension over time. Use a Chart.
  • Exact values matter more than the pattern, or there are only a few cells. Use a Data Table.
  • Values by US state or country. Use the Region Map.

How the agent should use it

  • Take every value from a tool result. Send null for missing data, never a made-up 0.
  • Pick the layout for the data: matrix for any rows × columns, calendar for one value per day, cohort for a retention triangle.
  • Matrix and cohort: send rows and columns first, then cells. The grid's skeleton appears at the right size as soon as both arrive. Cells name their row and column by label or 0-based index.
  • Calendar: one entry per day in the range, with 0 for days with nothing. Days left out show as no data.
  • Cohort: rows oldest first, period 0 first, with cohortSizes. Leave out periods a cohort hasn't reached; that's the empty triangle.
  • Set format, currency and unit so values read correctly. Percent values are percentages: 34 means 34%.
  • Use scale.diverging only for values that go either way around a midpoint, such as change vs last week.
  • Use highlight for the one cell the answer is about, and say the same thing in the reply. A label without a cell marks the highest value.
  • Only set selectable when the user's choice changes what you do next.
  • Lifecycle: this is analytics, so set fetchedAt (and staleAfterMs, e.g. an hour for live orders or a day for a report) on the call; the Renderer shows "may have changed" once it's old. No expiresAt. When the agent re-runs the query, mark the old call superseded (supersededBy).

Reading the heatmap

  • Shading: values fall into 3–7 equal-width buckets (default 5) on round bounds, like the Region Map: steps of one ink mixed into the neutral base, light to strong, so they read without color vision. Counts get whole-number bounds. scale.min and scale.max fix the ends, e.g. 0 and 100 for percentages.
  • Zero and no data: on a scale that starts at 0, exact zeros get the plain neutral base and a "0" legend swatch. Cells with no data are outlined and empty, with "No data" in the legend.
  • Diverging: with scale.diverging.midpoint, buckets are symmetric around the midpoint and use a second ink for the low side (--heatmap-low, --heatmap-high, both set from the chart tokens). Low-side cells also carry a small minus mark, so the two sides differ without color. With an odd number of steps the middle bucket straddles the midpoint; values exactly at the midpoint are always neutral. Signed values show "+".
  • Summary: computed from the data and shown under the title. Matrix: "Highest: Fri 7–8 PM · 1,240 orders" (ties say "tied with …"; diverging adds the lowest). Calendar: "227 days active · longest streak 21 days". Cohort: "Week 4 retention averages 35.2%", quoting the furthest period that at least half the cohorts have reached.
  • Labels: hour columns ("7 PM", "19:00") show as "7p" and thin out when cells are narrow. Long column labels turn vertical. The calendar labels Monday, Wednesday and Friday, and each month where it starts.
  • Cell size: cells are square and grow to fill the card (12 px minimum, 22 px when selectable). When they don't fit, the grid scrolls sideways inside the card with the row labels pinned; the calendar opens scrolled to the newest weeks. Cells 36 px and up show their value (matrix and cohort), with the text color picked for contrast against each bucket.
  • Highlight: a ring on the cell and a label above it, so it never relies on color.

Keyboard and screen readers

The grid is a single tab stop. Arrow keys move between cells, Home and End go to the ends of the row (with Ctrl, of the grid), and Page Up and Page Down move five rows, or about a month in the calendar. Escape hides the tooltip. Hovering or moving to a cell shows its row, column and value.

  • Display only: the grid holds focus and points at the current cell with aria-activedescendant; each cell's name reads like "Friday, 7 PM: 1,240 orders".
  • Selectable: each cell is a button with that name, and focus moves between them (roving tab index). Enter, Space or a click chooses it.

The figure's label is the summary. A visually hidden table carries every value; for the calendar it lists the 10 busiest days and a table of monthly totals instead.

Installation

npx shadcn@latest add https://your-site.com/r/heatmap.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { heatmapTool } from "@/components/genui/heatmap.schema"

const tools = {
  [heatmapTool.name]: tool({
    description: heatmapTool.description,
    inputSchema: heatmapTool.inputSchema,
  }),
}

Without selectable the heatmap is the whole answer, so give the tool an execute that returns immediately. With it, the agent receives the labels it sent, or the date for a calendar:

{ action: "select", row: "Fri", column: "7 PM" }
{ action: "select", date: "2026-03-09" }

With LangGraph

Push it from a graph node with ui.push({ name: "show_heatmap", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

Pass the stored tool output as result. The heatmap restores the chosen cell, locks, and never calls onResult again:

<Heatmap
  {...part.input}
  result={part.state === "output-available" ? part.output : undefined}
/>

Schema

PropTypeNotes
titlestring
descriptionstring?The period or source
layout"matrix" | "calendar" | "cohort"
rowsstring[]Matrix and cohort. Up to 60
columnsstring[]Matrix and cohort. Up to 60. Hour labels show compact
cells{ row, column, value }[]Data-bound. Matrix and cohort. row/column: label or 0-based index. value: null is no data
days{ date, value }[]Data-bound. Calendar. date: YYYY-MM-DD. Up to 400
cohortSizesnumber[]Data-bound. Cohort: people per cohort, in row order
format"number" | "percent" | "currency" | "duration-min"Default number. Percent values are percentages; duration-min is minutes
currencystring?ISO 4217, for currency
unitstring?Plural noun after values, e.g. "orders"
scale{ steps?, min?, max?, diverging?: { midpoint } }steps 3–7, default 5
highlight{ row?, column?, date?, label? }One cell to call out. label alone marks the highest value
selectablebooleanDefault false
weekStart0 | 1Calendar: Sunday (default) or Monday

Host props: status (streaming, ready, submitted), onResult, result and className.

Source

On this page