Animated heatmap visualization, rendered to MP4
Build an animated heatmap in plain HTML and CSS — a 12×7 grid where cells fade in with intensity-based color values, scrubbing left-to-right. Deterministic MP4 from JSON.
The analytics team has a contribution heatmap, a release-frequency heatmap, a server-load heatmap — and every one of them lives as a static PNG that nobody scrolls down to. The data is rich and the rendering is dead. The fix is not "make a fancier static heatmap." The fix is to animate the scrub: reveal the grid column-by-column over six seconds and let the eye follow the cadence.
An animated heatmap is one of the simplest data-viz wins available in plain HTML. Twelve columns, seven rows, eighty-four cells. Each cell has an intensity value [0, 1]. The animation is a left-to-right wipe with a per-cell fade-in. The output is an MP4 that re-renders nightly off whatever query feeds the data.
What an "animated heatmap" actually is
Four ingredients:
- A grid — typically days × weeks (7 × 52 for a year), months × hours (12 × 24 for traffic), or some other regular two-axis layout. CSS Grid handles it in one line.
- An intensity-to-color ramp — a function
intensity → rgb. The most common is a single-hue ramp (lighter to darker) interpolated in OKLab or HSL. - A scrub reveal — a vertical "wave" sweeps left-to-right across the grid. Each cell fades in as the wave crosses it.
- Axis labels — small, restrained, on the edges. The grid is the visualization; the labels are the legend.
The wave is what makes the chart a video. A static heatmap shows you the data; an animated heatmap shows you the order — Monday morning to Sunday night, January to December — which is half the insight in most temporal heatmaps.
The data shape
A heatmap's natural form is a flat array of { x, y, v } rows, or a 2D matrix. Either works; flat arrays are easier to query out of SQL.
{
"title": "Activity heatmap · Q2",
"rows": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
"cols": ["W1","W2","W3","W4","W5","W6","W7","W8","W9","W10","W11","W12"],
"cells": [
{ "x": 0, "y": 0, "v": 0.62 },
{ "x": 1, "y": 0, "v": 0.71 },
{ "x": 0, "y": 1, "v": 0.55 },
{ "x": 11, "y": 6, "v": 0.18 }
]
}The grid's geometry is data-driven. Twelve columns at 1080p give you 84-pixel cells with 4-pixel gaps — large enough to read individual cells, small enough that the whole chart fits on screen. For a year heatmap (52 × 7), shrink the cell to a 10×10 square; the wave still works, the cells just become texture.
The color ramp
A heatmap is only as good as its color ramp. Three rules:
- Use one hue, varied by lightness and saturation. Two-hue ramps (e.g., blue → red) suggest a divergent scale — useful for "below/above average," wrong for "low to high."
- Interpolate in a perceptually uniform space. Plain HSL or RGB gives you "muddy middle" values where the eye can't distinguish 0.4 from 0.5. OKLab or LCh interpolation fixes it. Two lines of math.
- Clamp at the extremes. A cell at 0 should be visibly different from no data at all. Set the minimum to ~5% saturation, not 0%, so empty cells (which you render as transparent or a different shade) are unambiguous.
For brand work, the HyperFrames ramp is #f0e9e3 → #ff3b1f → #8a1808 — cream to signal-orange to deep red. Three stops, interpolated linearly. The chart never looks like a Bloomberg terminal screenshot, which is the point.
The scrub wave
A naive reveal would fade in every cell at the same time. That's not a heatmap video; that's a heatmap with a fade-in. The scrub wave is what gives the chart its narrative — January first, December last, the eye follows the cadence.
The wave is a moving "x position" measured in column units. As t advances from 0 to 6 seconds, waveX sweeps from 0 to past the column count. For each cell, compute the distance from the wave; cells behind the wave are fully opaque, cells ahead are invisible, cells at the wave's edge are mid-fade.
const waveX = ((t - 0.4) / 4.6) * cols.length;
cells.forEach(el => {
const d = waveX - el.dataset.x;
const p = d < 0 ? 0 : d > 1.2 ? 1 : d / 1.2;
el.style.opacity = easeOut(p);
});The 1.2 is the wave's "width" in column units. A wider wave (2 or 3 columns) feels softer; a narrower wave (0.5 columns) feels like a wipe. 1.2 is the sweet spot — visible enough to read as a wave, narrow enough that the chart lands quickly.
Parameterizing the heatmap
Two knobs matter: the color ramp endpoints and the wave width. Both come up in every render review.
A blue cold + orange hot makes for a divergent ramp — read as "below/above average," not "low to high." If that's what your data says, use it. If not, stick with single-hue.
When to use rows vs. columns as time
The convention is that time runs left-to-right (columns). Rows are the categorical dimension — days of the week, hours of the day, server names. The scrub wave runs left-to-right because that's the reading direction; reversing it (right-to-left, or top-to-bottom) reads as a glitch.
For an hour-of-day × day-of-week heatmap (showing when activity happens during a typical week), make hours the columns. The wave then represents "as the day progresses," which is the right reading.
Render to MP4
The HTML is the template; the JSON is the data; the renderer turns hf-seek into MP4 frames. Because every cell's opacity is a pure function of t and its x position, the output is deterministic — re-render tomorrow and the bytes match.
hyperframes render heatmap.html --out activity.mp4 --duration 6 --fps 30A nightly cron that queries the latest data and re-renders the video is about 15 lines of code. See render 10k variants overnight for the patterns that scale this from "one heatmap" to "one per team."
FAQ
Can I render a 365-day GitHub-style contribution heatmap?
Yes. 52 weeks × 7 days = 364 cells, plus a partial first/last week. The scrub wave moves over weeks, not days; bump the wave width to 2–3 columns so a full week reveals together. The cell color ramp typically uses 5 discrete buckets instead of a continuous gradient — that's the GitHub convention and worth preserving.
How do I handle missing data?
Render missing cells as a different color — usually 4% black on the cream background. That visually distinguishes "0 activity" (a low-intensity colored cell) from "no data" (a neutral gray cell). The animation treats them the same.
What's the best aspect ratio for a heatmap video?
Match the data shape. A 12 × 7 quarter view is naturally 16:9-ish. A 52 × 7 year view is 7.4:1 — letterbox it inside 16:9 with the title in the empty space. Square (1:1) works for monthly views (about 30 × 30 grid). Don't crop or distort — the cells should be square or near-square so the eye can read columns and rows equally.
Can I add tooltips or hover values?
Yes for the live HTML embed; no for the MP4. Video is one-way; the audience can't hover. If specific cell values matter, label them inline — pin the 3–5 highest-intensity cells with a small inset number that fades in after the wave passes. Don't label all 84 cells; the chart becomes a table.
How do I show two heatmaps side-by-side (e.g., this month vs. last month)?
Render them in two grids inside the same template, with a shared color ramp (compute the global min/max across both, then ramp consistently). The wave passes across both grids simultaneously, so the eye reads them as a unit. Don't animate them on a stagger; the comparison is the point.
Where to go next
A heatmap is the densest data-viz pattern HTML can render gracefully. Once it works, the same primitives — grid layout, intensity ramp, scrub wave — handle calendar visualizations, correlation matrices, and cohort retention tables. Drop the JSON into the playground and swap your own data. The deterministic rendering doc explains the frame-pinned contract that makes nightly re-renders byte-identical. For a higher-level recipe on data-fed templates, see programmatic video from data and the animated line chart post for related charting primitives.
Eighty-four cells, one wave, zero seaborn screenshots.
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026animated,
author = {Kira Tanaka},
title = {Animated heatmap visualization, rendered to MP4},
year = {2026},
url = {https://hyperframes.video/blog/animated-heatmap-data-viz},
note = {HyperFrames blog}
}Kira Tanaka. (2026, May 21). Animated heatmap visualization, rendered to MP4. HyperFrames. https://hyperframes.video/blog/animated-heatmap-data-viz
[Animated heatmap visualization, rendered to MP4](https://hyperframes.video/blog/animated-heatmap-data-viz) — Kira Tanaka, 2026
Kira works on the render core: headless Chromium scheduling, frame capture, and the encoder pipeline. She cares about reproducible builds and small numbers next to the word "variance."
CSS animated pie chart (and donut) — no JavaScript required
Build animated pie and donut charts with pure CSS conic-gradient and stroke-dashoffset. Two techniques, full source, MP4 export.
Animated KPI cards that look like money
A KPI card that animates its value, with the easing and typography choices that make a number feel like a result.
Animate a bar chart from JSON in 10 minutes
From a tiny JSON file to a 16:9 animated bar chart you can render to MP4. CSS, no charting library.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.