ApexCharts Weekly Activity Heatmap (Day × Hour) — Free Snippet
ApexCharts Weekly Activity Heatmap (Day × Hour) · Charts · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="ahm-wrap">
<div class="ahm-card">
<div class="ahm-head">
<div>
<div class="ahm-title">When do customers message support?</div>
<div class="ahm-sub">Average conversations started per hour, last 8 weeks</div>
</div>
<label class="ahm-tz">Time zone
<select id="ahmShift">
<option value="0">UTC</option>
<option value="-5">New York (UTC−5)</option>
<option value="1">Berlin (UTC+1)</option>
<option value="5.5">Mumbai (UTC+5:30)</option>
</select>
</label>
</div>
<div id="ahmChart"></div>
<div class="ahm-peak" id="ahmPeak"></div>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f8fafc;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.ahm-wrap{width:100%;max-width:880px}
.ahm-card{background:#fff;border:1px solid #e2e8f0;border-radius:16px;padding:20px 18px 14px;box-shadow:0 1px 8px rgba(15,23,42,.06)}
.ahm-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
.ahm-title{font-size:15px;font-weight:700;color:#0f172a}
.ahm-sub{font-size:12px;color:#64748b;margin-top:3px}
.ahm-tz{font-size:12px;color:#475569;font-weight:600;display:flex;align-items:center;gap:8px}
.ahm-tz select{font:500 12px system-ui;padding:6px 8px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#0f172a}
.ahm-peak{font-size:12.5px;color:#334155;padding:4px 6px 0}
.ahm-peak b{color:#0f172a}var DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// Base data is stored in UTC: BASE[day][hour]. A realistic shape: busy
// weekday office hours, a lunch dip, quieter weekends.
var seed = 11;
function rand() { seed = (seed * 16807) % 2147483647; return seed / 2147483647; }
var BASE = DAYS.map(function (d, di) {
var weekend = di >= 5;
var row = [];
for (var h = 0; h < 24; h++) {
var office = Math.exp(-Math.pow((h - 14) / 4.2, 2));
var lunch = h === 12 ? 0.8 : 1;
var v = (weekend ? 9 : 26) * office * lunch + 1.5 + rand() * 3;
row.push(Math.round(v));
}
return row;
});
// Shifting to another zone moves every value by N hours, which can push
// it into the previous or next DAY. Work on a flat 168-hour week and wrap.
function shifted(offsetHours) {
var flat = [];
BASE.forEach(function (row) { flat = flat.concat(row); });
var shift = Math.round(offsetHours);
var out = DAYS.map(function () { return new Array(24).fill(0); });
for (var i = 0; i < 168; i++) {
var j = ((i + shift) % 168 + 168) % 168;
out[Math.floor(j / 24)][j % 24] = flat[i];
}
return out;
}
function toSeries(grid) {
// ApexCharts heatmaps draw series bottom-to-top, so reverse the days to
// put Monday at the top where people expect it.
return DAYS.map(function (d, di) {
return {
name: d,
data: grid[di].map(function (v, h) { return { x: (h < 10 ? '0' : '') + h + ':00', y: v }; }),
};
}).reverse();
}
var peakEl = document.getElementById('ahmPeak');
function describePeak(grid) {
var best = { v: -1 };
grid.forEach(function (row, d) { row.forEach(function (v, h) { if (v > best.v) best = { v: v, d: d, h: h }; }); });
peakEl.innerHTML = 'Busiest hour: <b>' + DAYS[best.d] + ' ' + best.h + ':00</b> with about <b>' + best.v + '</b> conversations.';
}
var chart = new ApexCharts(document.getElementById('ahmChart'), {
chart: { type: 'heatmap', height: 300, toolbar: { show: false }, fontFamily: 'system-ui, sans-serif' },
series: toSeries(BASE),
dataLabels: { enabled: false },
stroke: { width: 2, colors: ['#fff'] },
plotOptions: {
heatmap: {
radius: 3,
enableShades: false,
// Explicit colour bands instead of automatic shading: a fixed legend
// means the same colour always means the same load, across zones.
colorScale: {
ranges: [
{ from: 0, to: 5, color: '#e0f2fe', name: 'Quiet (0–5)' },
{ from: 6, to: 12, color: '#7dd3fc', name: 'Steady (6–12)' },
{ from: 13, to: 20, color: '#0284c7', name: 'Busy (13–20)' },
{ from: 21, to: 999, color: '#0c4a6e', name: 'Peak (21+)' },
],
},
},
},
xaxis: {
labels: {
rotate: 0,
formatter: function (v) { return v && v.slice(0, 2) % 3 === 0 ? v.slice(0, 2) : ''; },
},
axisTicks: { show: false },
tooltip: { enabled: false },
},
legend: { position: 'bottom', markers: { shape: 'square' } },
tooltip: {
y: {
// The series name is the day; add the hour from the hovered point.
formatter: function (v, o) {
var pt = o.w.config.series[o.seriesIndex].data[o.dataPointIndex];
return pt.x + ' · ' + v + ' conversations';
},
},
},
});
chart.render();
describePeak(BASE);
document.getElementById('ahmShift').addEventListener('change', function (e) {
var grid = shifted(Number(e.target.value));
chart.updateSeries(toSeries(grid));
describePeak(grid);
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
ApexCharts Day × Hour Heatmap — Finding the Busy Hours in a Week

A week of hourly counts is 168 numbers. As a table it is unreadable; as a day-by-hour heatmap the pattern jumps out immediately: the weekday afternoon block, the lunch dip, the quiet weekend. This layout is used for support staffing, posting schedules, server load and store footfall.
Series are rows, points are columns
In an ApexCharts heatmap each series is one row and each { x, y } point is a cell. The snippet builds seven series (days), each with 24 points (hours). ApexCharts stacks series from the bottom up, so the array is reversed to put Monday on top.
Fixed colour ranges instead of shades
By default heatmap cells are shaded relative to the data. That makes colours mean different things after the data changes. Here enableShades: false and colorScale.ranges define four named bands with fixed boundaries. The legend lists them, and "Busy" means 13–20 conversations no matter which time zone you view.
Time zones cross midnight
Converting from UTC to another zone is not a per-row operation. Moving New York five hours back pushes Monday 02:00 UTC into Sunday evening. The snippet flattens the week to 168 hours, shifts every index with wrap-around (((i + shift) % 168 + 168) % 168), and rebuilds the grid. Half-hour zones are rounded to the nearest hour, which is noted as a limitation you may need to handle.
A text summary
The busiest day and hour are computed from the same grid and written under the chart, so the key insight is available without reading colours.
Readable axis labels
Only every third hour is labelled on the x-axis, keeping labels horizontal without collisions.
Step by step
How to Use
- 1Load ApexChartsInclude apexcharts.min.js from the CDN.
- 2Paste the snippetA 7-day by 24-hour grid renders with a four-band legend.
- 3Hover cellsThe tooltip shows the day, hour and count.
- 4Change time zoneThe grid shifts, wrapping hours into the previous or next day.
- 5Read the summaryThe busiest hour is stated in text below the chart.
- 6Load real dataFill BASE[day][hour] with your own UTC counts.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
An array of series, one per row. Each series has a name and a data array of { x, y } points, where x is the column label and y the value. Seven series with 24 points each gives a day-by-hour grid.
ApexCharts draws heatmap series from the bottom of the chart upward. Reversing the array puts the first day, Monday, at the top.
Set plotOptions.heatmap.enableShades to false and define plotOptions.heatmap.colorScale.ranges as objects with from, to, color and name. The names appear in the legend.
Store data in UTC, flatten the week into 168 hourly values, shift each index by the zone offset with modular wrap-around, and rebuild the 7×24 grid. Shifting row by row is wrong, because hours near midnight belong to the neighbouring day.
With hourly buckets a 30-minute offset can't be represented exactly, so this snippet rounds it. For exact results, bucket raw timestamps by local hour after converting them, instead of shifting pre-aggregated hours.