Free Resource
Looker Heatmap Table
A production-ready custom visualization for Looker built in vanilla JavaScript. Color-codes any pivoted measure by configurable thresholds — great for coverage rates, performance tracking, and QA dashboards.
heatmap_table — Looker Preview
Coverage Rate:
Low (< 60%)
Medium (60–80%)
High (≥ 80%)
| Team / Region | Q1 2026 | Q2 2026 | Q3 2026 | Q4 2026 |
|---|---|---|---|---|
| Frontend | 94% | 88% | 72% | 91% |
| Backend | 67% | 85% | 90% | 74% |
| Data | 43% | 61% | 78% | 86% |
| Mobile | 82% | 55% | 83% | — |
How to Install
1
Copy the source code
Copy the full JavaScript code from the block below. Host it anywhere accessible via HTTPS — a GitHub repo, CDN, or your own server works fine.
2
Register in Looker Admin
Go to Admin → Visualizations and click Add Visualization. Set ID to
heatmap_table, Label to Heatmap Table, and paste the URL to your hosted JS file in the Main field.3
Set up the query
Your Looker query needs exactly 1 dimension (rows) + 1 pivot dimension (columns) + 1 measure (cell values). Enable the pivot on the column dimension in the Explore view.
4
Select the visualization
In your Explore, switch the visualization type to
Heatmap Table. Configure thresholds and colors from the Edit → Thresholds panel on the right.Source Code
/**
* Heatmap Table -- Looker Custom Visualization
* Developed by Martin Velez — RavencoreX
* https://ravencorex.com
*
* Renders a table where rows = dimension values, columns = pivot values,
* and cells are colored by configurable thresholds (High / Medium / Low).
*
* Query: 1 dimension (rows) + 1 pivot dimension (columns) + 1 measure (cell value).
*
* Admin → Visualizations:
* ID: heatmap_table
* Label: Heatmap Table
* Main: <URL to your hosted copy of this file>
*/
looker.plugins.visualizations.add({
id: "heatmap_table",
label: "Heatmap Table",
options: {
// -- Thresholds --
threshold_high: {
type: "number", label: "High Threshold >= (%)",
default: 80, section: "Thresholds", order: 1
},
threshold_medium: {
type: "number", label: "Medium Threshold >= (%)",
default: 60, section: "Thresholds", order: 2
},
color_high: {
type: "string", label: "High Color", default: "#22C55E",
display: "color", section: "Thresholds", order: 3
},
color_medium: {
type: "string", label: "Medium Color", default: "#F59E0B",
display: "color", section: "Thresholds", order: 4
},
color_low: {
type: "string", label: "Low Color", default: "#EF4444",
display: "color", section: "Thresholds", order: 5
},
label_high: {
type: "string", label: "High Label", default: "High",
section: "Thresholds", order: 6
},
label_medium: {
type: "string", label: "Medium Label", default: "Medium",
section: "Thresholds", order: 7
},
label_low: {
type: "string", label: "Low Label", default: "Low",
section: "Thresholds", order: 8
},
// -- Legend --
show_legend: {
type: "string", label: "Show Legend", display: "select",
values: [{ "Yes": "true" }, { "No": "false" }],
default: "true", section: "Legend", order: 1
},
legend_title: {
type: "string", label: "Legend Title", default: "Coverage Rate:",
section: "Legend", order: 2
},
legend_position: {
type: "string", label: "Legend Position", display: "select",
values: [{ "Above Table": "above" }, { "Below Table": "below" }],
default: "above", section: "Legend", order: 3
},
legend_align: {
type: "string", label: "Legend Alignment", display: "select",
values: [{ "Left": "left" }, { "Center": "center" }, { "Right": "right" }],
default: "left", section: "Legend", order: 4
},
legend_font_weight: {
type: "string", label: "Legend Font Weight", display: "select",
values: [{ "Normal": "normal" }, { "Bold": "bold" }],
default: "normal", section: "Legend", order: 5
},
// -- Table --
header_label: {
type: "string", label: "Header Label (Row / Col)",
default: "", placeholder: "e.g. Type / Region",
section: "Table", order: 1
},
cell_font_size: {
type: "number", label: "Cell Font Size (px)", default: 16,
section: "Table", order: 2
},
header_font_size: {
type: "number", label: "Header Font Size (px)", default: 14,
section: "Table", order: 3
},
cell_height: {
type: "number", label: "Cell Height (px)", default: 56,
section: "Table", order: 4
},
cell_border_radius: {
type: "number", label: "Cell Border Radius (px)", default: 6,
section: "Table", order: 5
},
cell_gap: {
type: "number", label: "Cell Gap (px)", default: 4,
section: "Table", order: 6
},
col_header_font_weight: {
type: "string", label: "Column Header Font Weight", display: "select",
values: [{ "Bold": "bold" }, { "Normal": "normal" }],
default: "bold", section: "Table", order: 7
},
row_header_font_weight: {
type: "string", label: "Row Header Font Weight", display: "select",
values: [{ "Semi-Bold": "600" }, { "Bold": "bold" }, { "Normal": "normal" }],
default: "600", section: "Table", order: 8
},
cell_value_font_weight: {
type: "string", label: "Cell Value Font Weight", display: "select",
values: [{ "Bold": "bold" }, { "Normal": "normal" }],
default: "bold", section: "Table", order: 9
},
font_family: {
type: "string", label: "Font Family",
default: "'Inter','Helvetica Neue',Arial,sans-serif",
section: "Table", order: 10
},
value_is_percentage: {
type: "string", label: "Values are Percentages", display: "select",
values: [{ "Yes (0-1 or 0-100)": "true" }, { "No (raw numbers)": "false" }],
default: "true", section: "Table", order: 11
}
},
create: function (element, config) {
element.innerHTML = "";
element.style.fontFamily = config.font_family || "'Inter','Helvetica Neue',Arial,sans-serif";
element.style.display = "flex";
element.style.alignItems = "flex-start";
element.style.justifyContent = "center";
element.style.width = "100%";
element.style.height = "100%";
element.style.overflow = "hidden";
element.style.background = "white";
element.style.padding = "0";
element.style.margin = "0";
element.style.boxSizing = "border-box";
},
updateAsync: function (data, element, config, queryResponse, details, doneRendering) {
element.innerHTML = "";
element.style.overflow = "hidden";
element.style.padding = "0";
element.style.margin = "0";
var parent = element.parentElement;
while (parent && parent !== document.body) {
parent.style.overflow = "hidden";
parent.style.padding = "0";
parent = parent.parentElement;
}
if (!document.getElementById("_heatmap_reset_css")) {
var st = document.createElement("style");
st.id = "_heatmap_reset_css";
st.textContent = "#vis, #vis-container, .looker-vis-context { padding:0!important; margin:0!important; overflow:hidden!important; }";
document.head.appendChild(st);
}
if (!data || data.length === 0) {
element.innerHTML = '<p style="color:#9CA3AF;text-align:center;padding:20px;font-size:13px;">No data returned</p>';
doneRendering(); return;
}
var dimensions = queryResponse.fields.dimension_like || queryResponse.fields.dimensions || [];
var measures = queryResponse.fields.measure_like || [];
if (measures.length === 0) {
measures = (queryResponse.fields.measures || []).concat(queryResponse.fields.table_calculations || []);
}
var pivots = queryResponse.pivots || [];
if (measures.length < 1) {
element.innerHTML = '<p style="color:#9CA3AF;text-align:center;padding:20px;font-size:13px;">Add at least 1 measure</p>';
doneRendering(); return;
}
if (pivots.length === 0) {
element.innerHTML = '<p style="color:#9CA3AF;text-align:center;padding:20px;font-size:13px;">Add a pivot dimension to generate columns</p>';
doneRendering(); return;
}
this.clearErrors();
var threshHigh = config.threshold_high != null ? Number(config.threshold_high) : 80;
var threshMed = config.threshold_medium != null ? Number(config.threshold_medium) : 60;
var colorHigh = config.color_high || "#22C55E";
var colorMed = config.color_medium || "#F59E0B";
var colorLow = config.color_low || "#EF4444";
var labelHigh = config.label_high || "High";
var labelMed = config.label_medium || "Medium";
var labelLow = config.label_low || "Low";
var showLegend = config.show_legend !== "false";
var legendTitle = config.legend_title != null ? config.legend_title : "Coverage Rate:";
var legendPos = config.legend_position || "above";
var legendAlign = config.legend_align || "left";
var headerLabel = config.header_label || "";
var cellFz = Number(config.cell_font_size) || 16;
var headerFz = Number(config.header_font_size) || 14;
var cellH = Number(config.cell_height) || 56;
var cellRadius = config.cell_border_radius != null ? Number(config.cell_border_radius) : 6;
var cellGap = config.cell_gap != null ? Number(config.cell_gap) : 4;
var fontFamily = config.font_family || "'Inter','Helvetica Neue',Arial,sans-serif";
var isPercentage = config.value_is_percentage !== "false";
var legendFw = config.legend_font_weight === "bold" ? "700" : "400";
var colHeaderFw = config.col_header_font_weight === "normal" ? "400" : "700";
var rowHeaderFw = config.row_header_font_weight || "600";
if (rowHeaderFw === "bold") rowHeaderFw = "700";
if (rowHeaderFw === "normal") rowHeaderFw = "400";
var cellValueFw = config.cell_value_font_weight === "normal" ? "400" : "700";
var pivotKeys = [];
var pivotLabels = {};
for (var pi = 0; pi < pivots.length; pi++) {
pivotKeys.push(pivots[pi].key);
pivotLabels[pivots[pi].key] = pivots[pi].key;
}
var rowDim = dimensions.length > 0 ? dimensions[0] : null;
var measureField = measures[0];
var rows = [];
for (var ri = 0; ri < data.length; ri++) {
var row = data[ri];
var rowLabel = rowDim
? (row[rowDim.name].rendered || String(row[rowDim.name].value) || "--")
: "Row " + (ri + 1);
var cells = [];
for (var ci = 0; ci < pivotKeys.length; ci++) {
var pk = pivotKeys[ci];
var mCell = row[measureField.name];
var val = null, rendered = null, links = null;
if (mCell && mCell[pk]) {
val = Number(mCell[pk].value);
rendered = mCell[pk].rendered || null;
links = mCell[pk].links || null;
}
cells.push({ pivotKey: pk, value: val, rendered: rendered, links: links });
}
rows.push({ label: rowLabel, cells: cells });
}
function getCellDisplay(val, rendered) {
if (val === null || isNaN(val)) return { display: "--", pct: null };
var pct = isPercentage
? ((val > 0 && val <= 1) ? val * 100 : val)
: val;
var display = rendered && !isPercentage
? rendered
: isPercentage
? Math.round(pct) + "%"
: rendered || String(Math.round(val));
return { display: display, pct: pct };
}
function getColor(pct) {
if (pct === null) return "#F3F4F6";
if (pct >= threshHigh) return colorHigh;
if (pct >= threshMed) return colorMed;
return colorLow;
}
var wrapper = document.createElement("div");
wrapper.style.cssText = "font-family:" + fontFamily + ";padding:16px;box-sizing:border-box;width:100%;display:flex;flex-direction:column;gap:12px;";
function buildLegend() {
var legend = document.createElement("div");
var alignMap = { left: "flex-start", center: "center", right: "flex-end" };
legend.style.cssText = "display:flex;align-items:center;gap:16px;flex-wrap:wrap;justify-content:" + (alignMap[legendAlign] || "flex-start") + ";";
if (legendTitle) {
var titleEl = document.createElement("span");
titleEl.style.cssText = "font-size:13px;color:#6B7280;font-weight:500;";
titleEl.textContent = legendTitle;
legend.appendChild(titleEl);
}
[{ color: colorLow, label: labelLow }, { color: colorMed, label: labelMed }, { color: colorHigh, label: labelHigh }]
.forEach(function(item) {
var wrap = document.createElement("div");
wrap.style.cssText = "display:flex;align-items:center;gap:6px;";
var swatch = document.createElement("div");
swatch.style.cssText = "width:16px;height:16px;border-radius:3px;background:" + item.color + ";flex-shrink:0;";
var lbl = document.createElement("span");
lbl.style.cssText = "font-size:13px;color:#374151;font-weight:" + legendFw + ";";
lbl.textContent = item.label;
wrap.appendChild(swatch);
wrap.appendChild(lbl);
legend.appendChild(wrap);
});
return legend;
}
if (showLegend && legendPos === "above") wrapper.appendChild(buildLegend());
var table = document.createElement("table");
table.style.cssText = "width:100%;border-collapse:separate;border-spacing:" + cellGap + "px;table-layout:fixed;";
var thead = document.createElement("thead");
var hRow = document.createElement("tr");
var thCorner = document.createElement("th");
thCorner.style.cssText = "text-align:left;font-size:" + headerFz + "px;font-weight:" + colHeaderFw + ";color:#374151;padding:12px 16px;background:#F9FAFB;border-radius:" + cellRadius + "px;";
thCorner.textContent = headerLabel;
hRow.appendChild(thCorner);
for (var hi = 0; hi < pivotKeys.length; hi++) {
var th = document.createElement("th");
th.style.cssText = "text-align:center;font-size:" + headerFz + "px;font-weight:" + colHeaderFw + ";color:#374151;padding:12px 8px;background:#F9FAFB;border-radius:" + cellRadius + "px;";
th.textContent = pivotLabels[pivotKeys[hi]];
hRow.appendChild(th);
}
thead.appendChild(hRow);
table.appendChild(thead);
var tbody = document.createElement("tbody");
for (var tri = 0; tri < rows.length; tri++) {
var tr = document.createElement("tr");
var tdLabel = document.createElement("td");
tdLabel.style.cssText = "padding:12px 16px;font-size:" + headerFz + "px;font-weight:" + rowHeaderFw + ";color:#374151;background:#F9FAFB;border-radius:" + cellRadius + "px;height:" + cellH + "px;vertical-align:middle;";
tdLabel.textContent = rows[tri].label;
tr.appendChild(tdLabel);
for (var tci = 0; tci < rows[tri].cells.length; tci++) {
var cell = rows[tri].cells[tci];
var info = getCellDisplay(cell.value, cell.rendered);
var bgColor = getColor(info.pct);
var td = document.createElement("td");
td.style.cssText = "text-align:center;vertical-align:middle;font-size:" + cellFz + "px;font-weight:" + cellValueFw + ";color:" + (info.pct === null ? "#9CA3AF" : "white") + ";background:" + bgColor + ";border-radius:" + cellRadius + "px;height:" + cellH + "px;padding:8px;";
td.textContent = info.display;
if (cell.links && cell.links.length > 0) {
td.style.cursor = "pointer";
(function(links) {
td.addEventListener("click", function(e) {
LookerCharts.Utils.openDrillMenu({ links: links, event: e });
});
})(cell.links);
}
tr.appendChild(td);
}
tbody.appendChild(tr);
}
table.appendChild(tbody);
wrapper.appendChild(table);
if (showLegend && legendPos === "below") wrapper.appendChild(buildLegend());
element.appendChild(wrapper);
doneRendering();
}
});
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
| Thresholds | |||
threshold_high |
number | 80 |
Minimum % to show High color |
threshold_medium |
number | 60 |
Minimum % to show Medium color |
color_high / color_medium / color_low |
color | Green / Amber / Red | Cell background colors per tier |
label_high / label_medium / label_low |
string | High / Medium / Low | Legend labels for each tier |
| Legend | |||
show_legend |
select | Yes |
Show or hide the color legend |
legend_title |
string | Coverage Rate: |
Label shown before legend swatches |
legend_position |
select | Above Table |
Position legend above or below table |
legend_align |
select | Left |
Horizontal alignment of legend |
| Table | |||
header_label |
string | — | Text in the top-left corner cell (e.g. "Team / Quarter") |
cell_height |
number | 56 |
Row height in px |
cell_font_size |
number | 16 |
Font size for cell values |
cell_border_radius |
number | 6 |
Corner rounding for cells (px) |
cell_gap |
number | 4 |
Space between cells (px) |
value_is_percentage |
select | Yes |
Auto-formats values as %. Accepts 0–1 or 0–100 scale. |
Need help with your Looker setup?
Custom visualizations are one piece. If you're looking to improve data quality, governance, or performance across your Looker instance — let's talk.
Book a Free 30-min Call