Sticky x-Axis Header in Kendo UI for jQuery Heatmap
Environment
| Product | Kendo UI for jQuery Heatmap |
| Version | 2026.1.415 |
Description
In a Kendo UI for jQuery Heatmap, long datasets may require vertical scrolling. However, scrolling often hides the x-axis labels, making it challenging to track data. This article explains how to keep the x-axis labels always visible at the top, even during scrolls. Additionally, it addresses aligning the header across different browsers and creating a grouped header with two levels, such as years and months.
This knowledge base article also answers the following questions:
- How do I create a sticky x-axis header in Kendo UI for jQuery Heatmap?
- How can I create a grouped header with years and months in Kendo UI for jQuery Heatmap?
- How can I align the Heatmap header across browsers?
Solution
To create a sticky x-axis header for the Heatmap, follow these steps:
-
Set up a scrollable wrapper: Place the Heatmap inside a scrollable
<div>withoverflow-y: autoand a fixed height. Add a separate header<div>above the scrollable area to display the x-axis labels. -
Populate the header dynamically: Extract the categories from the Heatmap's data and populate the header
<div>dynamically. This ensures the header adapts to variable data.javascriptconst headerEl = document.getElementById('heatmap-header'); months.forEach((m) => { const div = document.createElement('div'); div.className = 'header-label'; div.textContent = m; headerEl.appendChild(div); }); -
Hide duplicate x-axis labels: Set
xAxis.labels.visible: falsein the Heatmap configuration to hide the default x-axis labels.javascriptxAxis: { labels: { visible: false } } -
Synchronize the header with the Heatmap: To align the header and the Heatmap, calculate the left spacer width based on the Heatmap's plot area bounding box. Recalculate this width whenever the Heatmap renders or resizes.
javascriptfunction syncHeader(chart) { const plotBox = chart._plotArea.box; const headerSpacer = document.querySelector('#header-spacer'); headerSpacer.style.width = `${plotBox.left}px`; } $("#heatmap").kendoChart({ render: function(e) { syncHeader(e.sender); } }); -
Handle responsiveness: Use
width: 100%; max-width: 1200pxfor the wrapper to allow it to shrink on narrow viewports. Attach a$(window).on('resize')handler to callchart.resize()and keep the header aligned. -
Create a grouped header: For a two-level grouped header, such as years and months, dynamically create two sets of
<div>elements—one for each level—and align them above the Heatmap.javascriptyears.forEach((year) => { const yearDiv = document.createElement('div'); yearDiv.className = 'year-label'; yearDiv.textContent = year; yearHeaderEl.appendChild(yearDiv); }); months.forEach((month) => { const monthDiv = document.createElement('div'); monthDiv.className = 'month-label'; monthDiv.textContent = month; monthHeaderEl.appendChild(monthDiv); }); -
Ensure cross-browser compatibility: To maintain consistent header alignment in different browsers, test and adjust CSS styles (e.g.,
box-sizing,padding,margin). Use the modified example linked below for browser-specific adjustments.
Below is a runnable example:
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
#heatmap-wrapper {
width: 100%;
max-width: 1200px;
}
#heatmap-header {
display: flex;
background: #fff;
border-bottom: 1px solid #ddd;
font: bold 12px Arial;
color: #333;
}
#heatmap-header .header-label {
text-align: center;
flex: 1;
padding: 6px 0;
}
#heatmap-header .header-y-spacer {
flex: 0 0 200px;
}
#heatmap-scroll {
width: 100%;
height: 500px;
overflow-y: auto;
}
#heatmap {
width: 100%;
}
</style>
<div id="heatmap-wrapper">
<div id="heatmap-header">
<div class="header-y-spacer">Business Partner</div>
</div>
<div id="heatmap-scroll">
<div id="heatmap"></div>
</div>
</div>
<script>
const partners = [
"MEGGITT SAFETY SYSTEMS INC",
"METTIS AEROSPACE LTD",
"MINALEX CORP",
"HONEYWELL INTL INC",
"ROLLS ROYCE PLC",
"SAFRAN SA",
"BAE SYSTEMS PLC",
"GENERAL ELECTRIC CO",
"PRATT & WHITNEY",
"NORTHROP GRUMMAN CORP",
"RAYTHEON TECHNOLOGIES",
"LOCKHEED MARTIN CORP",
"BOEING COMPANY",
"AIRBUS SE",
"LEONARDO SPA",
"THALES GROUP",
"L3HARRIS TECHNOLOGIES",
"TEXTRON INC",
"SPIRIT AEROSYSTEMS",
"TRIUMPH GROUP INC",
];
const months = [
"Apr 2025",
"May 2025",
"Jun 2025",
"Jul 2025",
"Aug 2025",
"Sep 2025",
"Oct 2025",
"Nov 2025",
"Dec 2025",
"Jan 2026",
"Feb 2026",
"Mar 2026",
"Apr 2026",
];
const values = {
"MEGGITT SAFETY SYSTEMS INC": [2, 4, 9, 5, 4, 3, 4, 8, 7, 8, 10, 10, 9],
"METTIS AEROSPACE LTD": [7, 8, 5, 6, 7, 6, 6, 6, 6, 5, 10, 13, 10],
"MINALEX CORP": [
null,
null,
2,
1,
null,
null,
null,
null,
null,
null,
null,
null,
null,
],
"HONEYWELL INTL INC": [5, 3, 7, 8, 6, 9, 4, 5, 11, 7, 8, 6, 4],
"ROLLS ROYCE PLC": [10, 9, 8, 7, 6, 5, 4, 3, 2, 6, 9, 12, 11],
"SAFRAN SA": [3, 5, 4, 6, 8, 7, 9, 10, 8, 6, 4, 3, 5],
"BAE SYSTEMS PLC": [6, 7, 8, 9, 10, 11, 12, 10, 8, 7, 6, 5, 4],
"GENERAL ELECTRIC CO": [4, 6, 5, 3, 7, 8, 6, 5, 9, 11, 13, 10, 8],
"PRATT & WHITNEY": [8, 7, 6, 5, 4, 3, 5, 7, 9, 10, 11, 9, 7],
"NORTHROP GRUMMAN CORP": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
"RAYTHEON TECHNOLOGIES": [9, 8, 7, 6, 5, 4, 3, 2, 4, 6, 8, 10, 12],
"LOCKHEED MARTIN CORP": [5, 5, 6, 7, 8, 9, 10, 11, 10, 9, 7, 5, 3],
"BOEING COMPANY": [11, 10, 9, 8, 7, 6, 5, 4, 3, 5, 7, 9, 11],
"AIRBUS SE": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 11, 10, 9],
"LEONARDO SPA": [7, 6, 5, 4, 3, 2, 4, 6, 8, 10, 12, 11, 9],
"THALES GROUP": [2, 3, 6, 8, 10, 12, 11, 9, 7, 5, 3, 2, 4],
"L3HARRIS TECHNOLOGIES": [6, 8, 10, 12, 11, 9, 7, 5, 3, 4, 6, 8, 10],
"TEXTRON INC": [4, 3, 2, 5, 7, 9, 11, 13, 10, 8, 6, 4, 2],
"SPIRIT AEROSYSTEMS": [9, 7, 5, 3, 1, 3, 5, 7, 9, 11, 10, 8, 6],
"TRIUMPH GROUP INC": [
null,
null,
3,
5,
7,
9,
8,
6,
4,
2,
null,
null,
null,
],
};
// Build heatmap data as [xCategory, yCategory, value] arrays
const heatmapData = [];
partners.forEach((partner) => {
months.forEach((month, i) => {
const val = values[partner][i];
if (val != null) {
heatmapData.push([month, partner, val]);
}
});
});
// Populate header labels
const headerEl = document.getElementById("heatmap-header");
months.forEach((m) => {
const div = document.createElement("div");
div.className = "header-label";
div.textContent = m;
headerEl.appendChild(div);
});
function syncHeader() {
const chart = $("#heatmap").data("kendoChart");
if (chart && chart._plotArea) {
const plotBox = chart._plotArea.backgroundBox();
document.querySelector(".header-y-spacer").style.flex =
`0 0 ${plotBox.x1}px`;
}
}
function wrapText(e) {
var maxWidth = window.innerWidth < 768 ? 80 : 150;
var text = e.value;
var draw = kendo.drawing;
var geom = kendo.geometry;
var rect = e.rect;
var words = text.split(" ");
var lines = [];
var currentLine = words[0];
// Measure using a temporary text element
for (var i = 1; i < words.length; i++) {
var testLine = currentLine + " " + words[i];
var testEl = new draw.Text(testLine, [0, 0], { font: "12px Arial" });
var testWidth = testEl.bbox().width();
if (testWidth > maxWidth) {
lines.push(currentLine);
currentLine = words[i];
} else {
currentLine = testLine;
}
}
lines.push(currentLine);
var group = new draw.Group();
var lineHeight = 14;
var totalHeight = lines.length * lineHeight;
var startY = rect.origin.y + (rect.size.height - totalHeight) / 2;
for (var j = 0; j < lines.length; j++) {
var textEl = new draw.Text(lines[j], [0, 0], { font: "12px Arial" });
var textWidth = textEl.bbox().width();
var x = rect.origin.x + rect.size.width - textWidth - 5;
var y = startY + j * lineHeight;
var line = new draw.Text(lines[j], [x, y], { font: "12px Arial" });
group.append(line);
}
return group;
}
// Scrollable heatmap chart — x-axis labels hidden (shown in sticky header)
$("#heatmap").kendoChart({
chartArea: {
height: 1000,
},
series: [
{
type: "heatmap",
data: heatmapData,
color: "#4ec94e",
},
],
xAxis: {
categories: months,
labels: { visible: false },
line: { visible: false },
majorTicks: { visible: false },
},
yAxis: {
labels: {
visual: wrapText,
},
},
legend: { visible: false },
render: syncHeader,
});
$(window).on("resize", function () {
$("#heatmap").data("kendoChart").resize();
});
</script>