Heatmap
Intensity across two dimensions, as a matrix, a calendar of days or a cohort retention triangle.
≈ 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 AM | 1 AM | 2 AM | 3 AM | 4 AM | 5 AM | 6 AM | 7 AM | 8 AM | 9 AM | 10 AM | 11 AM | 12 PM | 1 PM | 2 PM | 3 PM | 4 PM | 5 PM | 6 PM | 7 PM | 8 PM | 9 PM | 10 PM | 11 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)
| 12 AM | 1 AM | 2 AM | 3 AM | 4 AM | 5 AM | 6 AM | 7 AM | 8 AM | 9 AM | 10 AM | 11 AM | 12 PM | 1 PM | 2 PM | 3 PM | 4 PM | 5 PM | 6 PM | 7 PM | 8 PM | 9 PM | 10 PM | 11 PM | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Mon | 84 orders | 47 orders | 30 orders | 20 orders | 19 orders | 38 orders | 96 orders | 199 orders | 321 orders | 394 orders | 436 orders | 536 orders | 652 orders | 533 orders | 434 orders | 421 orders | 466 orders | 592 orders | 780 orders | 1,004 orders | 853 orders | 678 orders | 406 orders | 197 orders |
| Tue | 87 orders | 49 orders | 30 orders | 20 orders | 19 orders | 41 orders | 100 orders | 210 orders | 332 orders | 412 orders | 461 orders | 584 orders | 672 orders | 526 orders | 444 orders | 431 orders | 483 orders | 582 orders | 787 orders | 1,013 orders | 883 orders | 671 orders | 425 orders | 216 orders |
| Wed | 95 orders | 53 orders | 31 orders | 21 orders | 20 orders | 40 orders | 108 orders | 216 orders | 335 orders | 421 orders | 473 orders | 572 orders | 698 orders | 592 orders | 481 orders | 470 orders | 517 orders | 657 orders | 828 orders | 1,044 orders | 949 orders | 730 orders | 464 orders | 225 orders |
| Thu | 101 orders | 58 orders | 35 orders | 22 orders | 23 orders | 43 orders | 112 orders | 231 orders | 375 orders | 448 orders | 524 orders | 660 orders | 740 orders | 630 orders | 510 orders | 497 orders | 557 orders | 684 orders | 901 orders | 1,093 orders | 965 orders | 730 orders | 497 orders | 241 orders |
| Fri | 113 orders | 60 orders | 38 orders | 25 orders | 25 orders | 52 orders | 122 orders | 271 orders | 414 orders | 508 orders | 576 orders | 725 orders | 795 orders | 709 orders | 571 orders | 549 orders | 591 orders | 750 orders | 993 orders | 1,240 orders | 1,148 orders | 859 orders | 540 orders | 271 orders |
| Sat | 74 orders | 42 orders | 25 orders | 16 orders | 16 orders | 33 orders | 84 orders | 176 orders | 263 orders | 328 orders | 375 orders | 666 orders | 750 orders | 723 orders | 614 orders | 589 orders | 640 orders | 683 orders | 931 orders | 1,121 orders | 1,016 orders | 816 orders | 521 orders | 236 orders |
| Sun | 62 orders | 35 orders | 21 orders | 14 orders | 14 orders | 28 orders | 73 orders | 152 orders | 241 orders | 285 orders | 335 orders | 576 orders | 674 orders | 632 orders | 539 orders | 526 orders | 554 orders | 608 orders | 856 orders | 1,043 orders | 935 orders | 688 orders | 432 orders | 221 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
nullfor missing data, never a made-up 0. - Pick the layout for the data:
matrixfor any rows × columns,calendarfor one value per day,cohortfor a retention triangle. - Matrix and cohort: send
rowsandcolumnsfirst, thencells. 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
0for 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,currencyandunitso values read correctly. Percent values are percentages:34means 34%. - Use
scale.divergingonly for values that go either way around a midpoint, such as change vs last week. - Use
highlightfor the one cell the answer is about, and say the same thing in the reply. Alabelwithout a cell marks the highest value. - Only set
selectablewhen the user's choice changes what you do next. - Lifecycle: this is analytics, so set
fetchedAt(andstaleAfterMs, 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. NoexpiresAt. 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.minandscale.maxfix the ends, e.g.0and100for 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
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
| Prop | Type | Notes |
|---|---|---|
title | string | |
description | string? | The period or source |
layout | "matrix" | "calendar" | "cohort" | |
rows | string[] | Matrix and cohort. Up to 60 |
columns | string[] | 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 |
cohortSizes | number[] | 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 |
currency | string? | ISO 4217, for currency |
unit | string? | 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 |
selectable | boolean | Default false |
weekStart | 0 | 1 | Calendar: Sunday (default) or Monday |
Host props: status (streaming, ready, submitted), onResult, result and className.