What is a Heat Map Calendar?
This guide will walk you through adding a Heat Map Calendar to your Glide app.
HOW TO (3 EASY STEPS)
STEP 1: COPY THE CODES
• Use a one-row table or the user table if signed in.
• Copy the desktop version code and paste it into a JavaScript column.
Desktop Version
if (!p1 || !p2 || !p3) return '';
const DAY_MS = 864e5;
function parseNumber(s) {
if (typeof s !== 'string' && typeof s !== 'number') return 0;
const cleaned = String(s).replace(/[^0-9.\-]/g, '');
return Number(cleaned) || 0;
}
function hexToRgb(h) {
const i = parseInt(h.slice(1), 16);
return { r: i >> 16, g: (i >> 8) & 0xff, b: i & 0xff };
}
function rgbToHex({ r, g, b }) {
return '#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1).padStart(6, '0');
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
default: h = (r - g) / d + 4;
}
h /= 6;
}
return { h, s, l };
}
function hslToRgb(h, s, l) {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
const [minInput, maxInput, maxHex, aggTypeRaw, theme] = p3.split('||');
const minVal = parseNumber(minInput);
const maxVal = parseNumber(maxInput) || minVal;
const raw = (aggTypeRaw || 'sum').toLowerCase();
const aggType =
raw === 'avg' || raw === 'average' ? 'average' :
raw === 'max' || raw === 'maximum' ? 'maximum' :
raw === 'min' || raw === 'minimum' ? 'minimum' :
raw === 'count' ? 'count' :
'sum';
const MAX_COLOR = maxHex;
const maxRgbVal = hexToRgb(MAX_COLOR);
const { h: baseH, s: baseS, l: baseL } = rgbToHsl(maxRgbVal.r, maxRgbVal.g, maxRgbVal.b);
function autoMinColor() {
return rgbToHex(hslToRgb(baseH, baseS * 0.2, 0.9));
}
function generatePopColors() {
const POP_SHIFTS = [-75/360, -96.2/360, 227.2/360];
const POP_SATS = [0.88, 1, 1];
const POP_LIGHTS = [0.61, 0.69, 0.79];
return POP_SHIFTS.map((shift, i) => {
const H = (baseH + shift + 1) % 1;
return rgbToHex(hslToRgb(H, POP_SATS[i], POP_LIGHTS[i]));
});
}
const MIN_COLOR = autoMinColor();
const POP_COLORS = generatePopColors();
const minRgb = hexToRgb(MIN_COLOR);
const maxRgb = hexToRgb(MAX_COLOR);
const deltaRgb = {
r: maxRgb.r - minRgb.r,
g: maxRgb.g - minRgb.g,
b: maxRgb.b - minRgb.b
};
const invRange = 1 / (maxVal - minVal);
const MIN_OPACITY = 0.2;
const colorCache = new Map();
function getColor(v) {
if (colorCache.has(v)) return colorCache.get(v);
const t = Math.max(0, Math.min(1, (v - minVal) * invRange));
const result = {
r: Math.round(minRgb.r + deltaRgb.r * t),
g: Math.round(minRgb.g + deltaRgb.g * t),
b: Math.round(minRgb.b + deltaRgb.b * t),
t
};
colorCache.set(v, result);
return result;
}
const dataArr = typeof p1 === 'string' ? JSON.parse(p1) : p1;
const dateKeyCache = new Map();
const getDateKey = s => {
const cached = dateKeyCache.get(s);
if (cached !== undefined) return cached;
const d = new Date(s);
if (isNaN(d)) { dateKeyCache.set(s, null); return null; }
d.setHours(0, 0, 0, 0);
const key = `${d.getMonth()+1}/${d.getDate()}/${d.getFullYear()}`;
dateKeyCache.set(s, key);
return key;
};
const byDate = dataArr.reduce((acc, o) => {
const key = getDateKey(o.date);
if (!key) return acc;
const v = parseNumber(o.value);
let cur = acc[key];
if (!cur) {
cur = { sum:0, count:0, max:-Infinity, min:Infinity, images:[], titles:[], descriptions:[], significant:0 };
acc[key] = cur;
}
cur.sum += v;
cur.count++;
if (v > cur.max) cur.max = v;
if (v < cur.min) cur.min = v;
if (o.image) cur.images.push(o.image);
if (o.title) cur.titles.push(o.title);
if (o.description) cur.descriptions.push(o.description);
let sigVal = 0;
const parsed = parseNumber(o.significant);
if (o.significant !== '') sigVal = (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 3) ? parsed : 1;
if (sigVal > cur.significant) cur.significant = sigVal;
return acc;
}, {});
Object.values(byDate).forEach(item => {
let val;
switch (aggType) {
case 'count': val = item.count; break;
case 'average': val = item.count ? item.sum / item.count : 0; break;
case 'maximum': val = item.max; break;
case 'minimum': val = item.min; break;
default: val = item.sum;
}
item.value = Math.round(val);
});
const year = typeof p2 === 'string' ? parseInt(p2, 10) : p2;
const startTs = new Date(year, 0, 1).getTime();
const daysInYear = Math.floor((new Date(year+1, 0, 1).getTime() - startTs) / DAY_MS);
const blankDays = (new Date(startTs).getDay() + 6) % 7;
const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
const labelMap = {0:'MON',2:'WED',4:'FRI'};
const separator = '<div style="width:100%;border-top:1px dashed #999;margin:6px 0;"></div>';
const EMPTY_CELL = '<div style="background:transparent;flex:1 1 0;min-width:0;aspect-ratio:1/1;border-radius:2px;margin:2px;"></div>';
const BASE_CELL_STYLE = 'flex:1 1 0;min-width:0;aspect-ratio:1/1;border-radius:2px;margin:2px;position:relative;display:flex;align-items:center;justify-content:center;';
const parts = [];
parts.push('<div style="display:flex;flex-direction:column;justify-content:flex-start;">');
parts.push('<div style="display:flex;width:100%;padding:0 17px;box-sizing:border-box;margin-bottom:8px;">');
months.forEach(m => {
parts.push(`<div style="flex:1 1 0;min-width:0;text-align:center;color:${theme==='light'? '#0d0d0d':'#fff'};font-size:12px;">${m}</div>`);
});
parts.push('</div>');
parts.push(`<div style="background-color:${theme==='light'? '#f9f9f9':'#1f1f1f'};padding:8px;border-radius:8px;display:flex;flex-direction:column;flex:1 1 0;min-width:0;">`);
for (let dow = 0; dow < 7; dow++) {
parts.push('<div style="position:relative;display:flex;width:100%;min-width:0;">');
parts.push(`<div style="position:absolute;left:-50px;top:6px;width:30px;text-align:right;color:${theme==='light'? '#0d0d0d':'#fff'};font-size:12px;line-height:1.2;">${labelMap[dow]||""}</div>`);
for (let week = 0; week < 53; week++) {
const idx = week*7 + dow;
if (idx < blankDays || idx >= blankDays + daysInYear) {
parts.push(EMPTY_CELL);
} else {
const dt = new Date(startTs + (idx - blankDays)*DAY_MS);
const key = `${dt.getMonth()+1}/${dt.getDate()}/${year}`;
const item = byDate[key];
if (item) {
const sig = item.significant;
let cellBg;
if (sig >= 1 && sig <= 3) {
cellBg = POP_COLORS[sig-1];
} else {
const { r, g, b, t } = getColor(item.value);
const opacity = MIN_OPACITY + t * (1 - MIN_OPACITY);
cellBg = `rgba(${r},${g},${b},${opacity})`;
}
const hasSig = sig > 0;
const suffix = hasSig ? (week < 8 ? '-right' : week >= 45 ? '-left' : '') : '';
const popupCls = hasSig ? `popup-content-below${suffix}` : `popup-content${suffix}`;
const isBelow = popupCls.includes('below');
const popupSty = isBelow
? `width:300px;background-color:#ffffff;white-space:normal;overflow-wrap:break-word;padding:6px 12px;`
: `width:auto;white-space:nowrap;padding:6px 12px;${theme==='light'? 'background-color:#212121;opacity:0.95;color:#ffffff;':'background-color:#ffffff;opacity:0.95;color:#000000;'}`;
const cls = `popup-wrapper${hasSig?" significant pointer":""}`;
const cellStyle = `background:${cellBg};${BASE_CELL_STYLE}`;
parts.push(`<div class="${cls}" style="${cellStyle}">`);
parts.push(`<div class="${popupCls}" style="${popupSty}">`);
if (!popupCls.includes('-right') && !popupCls.includes('-left')) {
if (isBelow) {
parts.push('<div style="position:absolute;bottom:100%;left:50%;transform:translateX(-50%);width:0;height:0;border-width:6px;border-style:solid;border-color:transparent transparent #ffffff transparent;"></div>');
} else {
parts.push(`<div style="position:absolute;top:100%;left:50%;transform:translateX(-50%);width:0;height:0;border-width:6px;border-style:solid;border-color:${theme==='light'? '#212121':'#ffffff'} transparent transparent transparent;"></div>`);
}
}
if (isBelow && item.images.length) {
parts.push(item.images.map((img,i) =>
`<div style="display:flex;align-items:center;"><div style="display:inline-block;border-radius:50%;padding:3.75px;background-color:white;box-shadow:0px 4px 8px rgba(0,0,0,0.1);margin-right:12px;"><div style="width:30px;height:30px;border-radius:50%;overflow:hidden;"><img src="${img}" alt="avatar" style="width:100%;height:100%;object-fit:cover;"></div></div><div style="display:flex;flex-direction:column;"><span style="font-weight:725;color:#2e2e2e;font-size:13px;">${item.titles[i]||""}</span><span style="color:#4a4f40;font-size:12px;font-weight:525;">${item.descriptions[i]||""}</span></div></div>`
).join(separator));
} else {
parts.push(`<div style="text-align:center;font-weight:600;">${item.value.toLocaleString()}</div>`);
}
parts.push('</div></div>');
} else {
parts.push(`<div style="background:${theme==='light'? '#f5f5f5':'#212121'};${BASE_CELL_STYLE}"></div>`);
}
}
}
parts.push('</div>');
}
parts.push('</div></div>');
return parts.join('');
• Copy the mobile version code and paste it into a separate JavaScript column designated for mobile.
Mobile Version
if (!p1 || !p2 || !p3) return '';
const DAY_MS = 864e5;
function parseNumber(s) {
if (typeof s !== 'string' && typeof s !== 'number') return 0;
const cleaned = String(s).replace(/[^0-9.\\-]/g, '');
return Number(cleaned) || 0;
}
function hexToRgb(h) {
const i = parseInt(h.slice(1), 16);
return { r: i >> 16, g: (i >> 8) & 0xff, b: i & 0xff };
}
function rgbToHex({ r, g, b }) {
return "#" + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1).padStart(6, "0");
}
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
default: h = (r - g) / d + 4;
}
h /= 6;
}
return { h, s, l };
}
function hslToRgb(h, s, l) {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
}
const [minInput, maxInput, maxHex, aggTypeRaw, theme] = p3.split("||");
const minVal = parseNumber(minInput);
const maxVal = parseNumber(maxInput) || minVal;
const raw = (aggTypeRaw || "sum").toLowerCase();
const aggType =
raw === "avg" || raw === "average" ? "average" :
raw === "max" || raw === "maximum" ? "maximum" :
raw === "min" || raw === "minimum" ? "minimum" :
raw === "count" ? "count" :
"sum";
const MAX_COLOR = maxHex;
const maxRgbVal = hexToRgb(MAX_COLOR);
const { h: baseH, s: baseS, l: baseL } = rgbToHsl(maxRgbVal.r, maxRgbVal.g, maxRgbVal.b);
function autoMinColor() {
return rgbToHex(hslToRgb(baseH, baseS * 0.2, 0.9));
}
function generatePopColors() {
const POP_SHIFTS = [-75/360, -96.2/360, 227.2/360];
const POP_SATS = [0.88, 1, 1];
const POP_LIGHTS = [0.61, 0.69, 0.79];
return POP_SHIFTS.map((shift, i) => {
const H = (baseH + shift + 1) % 1;
return rgbToHex(hslToRgb(H, POP_SATS[i], POP_LIGHTS[i]));
});
}
const MIN_COLOR = autoMinColor();
const POP_COLORS = generatePopColors();
const minRgb = hexToRgb(MIN_COLOR);
const maxRgb = hexToRgb(MAX_COLOR);
const deltaRgb = {
r: maxRgb.r - minRgb.r,
g: maxRgb.g - minRgb.g,
b: maxRgb.b - minRgb.b
};
const invRange = 1 / (maxVal - minVal);
const MIN_OPACITY = 0.2;
const colorCache = new Map();
function getColor(v) {
if (colorCache.has(v)) return colorCache.get(v);
const t = Math.max(0, Math.min(1, (v - minVal) * invRange));
const result = {
r: Math.round(minRgb.r + deltaRgb.r * t),
g: Math.round(minRgb.g + deltaRgb.g * t),
b: Math.round(minRgb.b + deltaRgb.b * t),
t
};
colorCache.set(v, result);
return result;
}
const dataArr = typeof p1 === "string" ? JSON.parse(p1) : p1;
const dateKeyCache = new Map();
const getDateKey = s => {
const cached = dateKeyCache.get(s);
if (cached !== undefined) return cached;
const d = new Date(s);
if (isNaN(d)) { dateKeyCache.set(s, null); return null; }
d.setHours(0, 0, 0, 0);
const key = `${d.getMonth()+1}/${d.getDate()}/${d.getFullYear()}`;
dateKeyCache.set(s, key);
return key;
};
const byDate = dataArr.reduce((acc, o) => {
const key = getDateKey(o.date);
if (!key) return acc;
const v = parseNumber(o.value);
let cur = acc[key];
if (!cur) {
cur = { sum:0, count:0, max:-Infinity, min:Infinity, images:[], titles:[], descriptions:[], significant:0 };
acc[key] = cur;
}
cur.sum += v;
cur.count++;
if (v > cur.max) cur.max = v;
if (v < cur.min) cur.min = v;
if (o.image) cur.images.push(o.image);
if (o.title) cur.titles.push(o.title);
if (o.description) cur.descriptions.push(o.description);
let sigVal = 0;
const parsed = parseNumber(o.significant);
if (o.significant !== "") sigVal = (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 3) ? parsed : 1;
if (sigVal > cur.significant) cur.significant = sigVal;
return acc;
}, {});
Object.values(byDate).forEach(item => {
let val;
switch (aggType) {
case "count": val = item.count; break;
case "average": val = item.count ? item.sum / item.count : 0; break;
case "maximum": val = item.max; break;
case "minimum": val = item.min; break;
default: val = item.sum;
}
item.value = Math.round(val);
});
const year = typeof p2 === "string" ? parseInt(p2, 10) : p2;
const startTs = new Date(year, 0, 1).getTime();
const daysInYear = Math.floor((new Date(year+1, 0, 1).getTime() - startTs) / DAY_MS);
const blankDays = (new Date(startTs).getDay() + 6) % 7;
const separator = '<div style="width:100%;border-top:1px dashed #999;margin:6px 0;"></div>';
const EMPTY_CELL = '<div style="background:transparent;flex:1 1 0;min-width:0;aspect-ratio:1/1;border-radius:2px;margin:2px;"></div>';
const BASE_CELL_STYLE = 'flex:1 1 0;min-width:0;aspect-ratio:1/1;border-radius:2px;margin:2px;position:relative;display:flex;align-items:center;justify-content:center;';
const parts = [];
parts.push('<div style="display:flex;flex-direction:column;justify-content:flex-start;">');
parts.push(`<div style="background-color:${theme==='light'? '#f9f9f9':'#1f1f1f'};padding:8px;border-radius:8px;display:flex;flex-direction:column;flex:1 1 0;min-width:0;">`);
for (let dow = 0; dow < 21; dow++) {
parts.push(`<div style="position:relative;display:flex;width:100%;min-width:0;">`);
for (let week = 0; week < 18; week++) {
const idx = week*21 + dow;
if (idx < blankDays || idx >= blankDays + daysInYear) {
parts.push(EMPTY_CELL);
} else {
const dt = new Date(startTs + (idx - blankDays)*DAY_MS);
const key = `${dt.getMonth()+1}/${dt.getDate()}/${year}`;
const item = byDate[key];
if (item) {
const sig = item.significant;
let cellBg;
if (sig >= 1 && sig <= 3) {
cellBg = POP_COLORS[sig-1];
} else {
const { r, g, b, t } = getColor(item.value);
const opacity = MIN_OPACITY + t * (1 - MIN_OPACITY);
cellBg = `rgba(${r},${g},${b},${opacity})`;
}
const hasSig = sig > 0;
const suffix = hasSig ? (week < 6 ? "-right" : week >= 12 ? "-left" : "") : "";
const popupCls = hasSig ? `popup-content-below${suffix}` : `popup-content${suffix}`;
const isBelow = popupCls.includes("below");
const popupSty = isBelow
? `width:260px;background-color:#ffffff;white-space:normal;overflow-wrap:break-word;padding:6px 12px;`
: theme==='light'
? `white-space:nowrap;padding:6px 12px;background-color:#212121;opacity:0.95;color:#ffffff;`
: `white-space:nowrap;padding:6px 12px;background-color:#ffffff;opacity:0.95;color:#000000;`;
const cls = `popup-wrapper${hasSig?" significant pointer":""}`;
const cellStyle = `background:${cellBg};${BASE_CELL_STYLE}`;
parts.push(`<div class="${cls}" style="${cellStyle}">`);
parts.push(`<div class="${popupCls}" style="${popupSty}">`);
if (!popupCls.includes('-right') && !popupCls.includes('-left')) {
if (isBelow) {
parts.push(`<div style="position:absolute;bottom:100%;left:50%;transform:translateX(-50%);width:0;height:0;border-width:6px;border-style:solid;border-color:transparent transparent #ffffff transparent;"></div>`);
} else {
parts.push(`<div style="position:absolute;top:100%;left:50%;transform:translateX(-50%);width:0;height:0;border-width:6px;border-style:solid;border-color:${theme==='light'? '#212121':'#ffffff'} transparent transparent transparent;"></div>`);
}
}
if (isBelow && item.images.length) {
parts.push(item.images.map((img,i) =>
`<div style="display:flex;align-items:center;">`+
`<div style="display:inline-block;border-radius:50%;padding:3.75px;background-color:white;box-shadow:0px 4px 8px rgba(0,0,0,0.1);margin-right:12px;">`+
`<div style="width:30px;height:30px;border-radius:50%;overflow:hidden;">`+
`<img src="${img}" alt="avatar" style="width:100%;height:100%;object-fit:cover;">`+
`</div>`+
`</div>`+
`<div style="display:flex;flex-direction:column;">`+
`<span style="font-weight:725;color:#2e2e2e;font-size:13px;">${item.titles[i]||""}</span>`+
`<span style="color:#4a4f40;font-size:12px;font-weight:525;">${item.descriptions[i]||""}</span>`+
`</div>`+
`</div>`
).join(separator));
} else {
parts.push(`<div style="text-align:center;font-weight:600;">${item.value.toLocaleString()}</div>`);
}
parts.push(`</div></div>`);
} else {
parts.push(`<div style="background:${theme==='light'? '#f5f5f5':'#212121'};${BASE_CELL_STYLE}"></div>`);
}
}
}
parts.push(`</div>`);
}
parts.push(`</div></div>`);
return parts.join("");
• Navigate to Settings > Appearance > Custom CSS, paste the provided CSS, and enable Preview Custom CSS.
CSS
.popup-wrapper {
position: relative;
display: inline-block;
}
.popup-wrapper.significant {
position: relative;
transform: translateY(0);
z-index: 1;
transition: transform 0.2s ease-in-out, z-index 0s linear 0.2s;
will-change: transform;
}
.popup-wrapper.significant:hover {
transform: translateY(-1px);
z-index: 100;
transition: transform 0.2s ease-in-out, z-index 0s linear 0s;
}
.popup-content,
.popup-content-right,
.popup-content-left {
visibility: hidden;
width: auto;
min-width: auto;
max-width: 300px;
white-space: nowrap;
background: #fff;
color: #4a4f40;
text-align: left;
padding: 8px 8px 12px 8px;
border-radius: 6px;
font-family: Arial, sans-serif;
font-size: 13px;
line-height: 1.4;
position: absolute;
z-index: 100;
opacity: 0;
transition: opacity 0s ease-in-out;
pointer-events: none;
user-select: none;
}
.popup-content-below,
.popup-content-below-right,
.popup-content-below-left {
visibility: hidden;
width: auto;
min-width: 80px;
max-width: 300px;
white-space: normal;
background: #fff;
color: #4a4f40;
text-align: left;
padding: 12px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1), 0 16px 24px rgba(0,0,0,0.06);
font-family: Arial, sans-serif;
font-size: 13px;
line-height: 1.4;
position: absolute;
z-index: 100;
opacity: 0;
transition: visibility 0s linear 0.2s, opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
pointer-events: none;
user-select: none;
}
.popup-content {
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
}
.popup-content-below {
top: calc(100% + 6px);
left: 50%;
transform: translateX(-50%) translateY(-10px);
opacity: 0;
visibility: hidden;
transition: visibility 0s linear 0.2s, opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.popup-content-right {
top: 50%;
left: calc(100% + 6px);
transform: translateY(-50%);
}
.popup-content-below-right {
top: calc(100% + 6px);
left: 0;
transform: translateY(-10px);
opacity: 0;
visibility: hidden;
transition: visibility 0s linear 0.2s, opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.popup-content-left {
top: 50%;
right: calc(100% + 6px);
transform: translateY(-50%);
}
.popup-content-below-left {
top: calc(100% + 6px);
right: 0;
left: auto;
transform: translateY(-10px);
opacity: 0;
visibility: hidden;
transition: visibility 0s linear 0.2s, opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.popup-wrapper:hover .popup-content,
.popup-wrapper:hover .popup-content-right,
.popup-wrapper:hover .popup-content-left {
visibility: visible;
opacity: 1;
}
.popup-wrapper:hover .popup-content-below,
.popup-wrapper:hover .popup-content-below-right,
.popup-wrapper:hover .popup-content-below-left {
visibility: visible;
opacity: 1;
transform: translateX(-50%) translateY(0);
transition: visibility 0s linear 0s, opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.popup-wrapper:hover .popup-content-below-right {
transform: none;
}
.popup-wrapper:hover .popup-content-below-left {
transform: translateY(0);
}
.popup-wrapper.pointer,
.popup-wrapper.pointer * {
cursor: pointer !important;
}
.popup-wrapper:not(.pointer),
.popup-wrapper:not(.pointer) * {
cursor: default !important;
}
STEP 2: SETUP YOUR JSON
[
{
"date":"",
"value":"",
"significant":"",
"image":"",
"title":"",
"description":""
}
]
• Below are four examples to guide you in structuring your JSON data.
4 Example JSONs
Normal Square (If significant
is empty the output square will be Normal)
[
{
"date":"1/1/2025",
"value":"500",
"significant":"",
"image":"",
"title":"",
"description":""
}
]
Significant Square 1 (Accent Color #1)
[
{
"date":"1/1/2025",
"value":"500",
"significant":"1",
"image":"https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/4FvVz7Tss4RspS8DYXtI.jpg",
"title":"ALAN",
"description":"Sales goal reached 500"
}
]
Significant Square 2 (Accent Color #2)
[
{
"date":"1/1/2025",
"value":"500",
"significant":"2",
"image":"https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/jSUzn36XMMGEw9c92b0S.jpg",
"title":"SUSAN",
"description":"Appointment goal reached 15"
}
]
Significant Square 3 (Accent Color #3)
[
{
"date":"1/1/2025",
"value":"500",
"significant":"3",
"image":"https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/mGxfUAfOBymqPH2fKIwk.jpg",
"title":"ELINA",
"description":"Sales goal reached 1500 and Appointment goal reached 25"
}
]
• Paste this JSON file from the sample app into a Basic Text column, then use it to follow along with the tutorial
Sample App JSON
[ { "date": "1/1/2025", "value": "10610", "significant": "3", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/U7YEqKPmvXfGJOkzacu9.jpg", "title": "DEREK", "description": "Sales goal reached $10610 and Appointments reached 18" }, { "date": "1/2/2025", "value": "333880", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/2uy9O5TucWARKTAwc4v4.jpg", "title": "CHRISTINA", "description": "Appointments reached 200" }, { "date": "1/3/2025", "value": "19662", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/4FvVz7Tss4RspS8DYXtI.jpg", "title": "ALAN", "description": "" }, { "date": "1/4/2025", "value": "4324", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/2uy9O5TucWARKTAwc4v4.jpg", "title": "CHRISTINA", "description": "" }, { "date": "1/5/2025", "value": "200192", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/jSUzn36XMMGEw9c92b0S.jpg", "title": "SUSAN", "description": "" }, { "date": "1/6/2025", "value": "20834", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/Fw4LuWSIznkgZzXHdrMV.jpg", "title": "BOBBY", "description": "" }, { "date": "1/7/2025", "value": "24144", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/mGxfUAfOBymqPH2fKIwk.jpg", "title": "ELINA", "description": "" }, { "date": "1/8/2025", "value": "63963", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/U7YEqKPmvXfGJOkzacu9.jpg", "title": "DEREK", "description": "" }, { "date": "1/9/2025", "value": "8573", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/mGxfUAfOBymqPH2fKIwk.jpg", "title": "ELINA", "description": "" }, { "date": "1/10/2025", "value": "2396", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/4FvVz7Tss4RspS8DYXtI.jpg", "title": "ALAN", "description": "" }, { "date": "1/11/2025", "value": "30046", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/Zi7DjLbbVi9xbOd4HBZ2/pub/cxTZMa61JCk1SmkFJe5J.jpg", "title": "KAREN", "description": "" }, { "date": "1/12/2025", "value": "15290", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/mGxfUAfOBymqPH2fKIwk.jpg", "title": "ELINA", "description": "" }, { "date": "1/13/2025", "value": "12375", "significant": "1", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/4FvVz7Tss4RspS8DYXtI.jpg", "title": "ALAN", "description": "Sales goal reached of $12375" }, { "date": "1/14/2025", "value": "8900", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/4FvVz7Tss4RspS8DYXtI.jpg", "title": "ALAN", "description": "" }, { "date": "1/15/2025", "value": "27671", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/Fw4LuWSIznkgZzXHdrMV.jpg", "title": "BOBBY", "description": "" }, { "date": "1/16/2025", "value": "29604", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/2uy9O5TucWARKTAwc4v4.jpg", "title": "CHRISTINA", "description": "" }, { "date": "1/17/2025", "value": "8458", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/Fw4LuWSIznkgZzXHdrMV.jpg", "title": "BOBBY", "description": "" }, { "date": "1/18/2025", "value": "5123", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/Zi7DjLbbVi9xbOd4HBZ2/pub/cxTZMa61JCk1SmkFJe5J.jpg", "title": "KAREN", "description": "" }, { "date": "1/19/2025", "value": "28394", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/2uy9O5TucWARKTAwc4v4.jpg", "title": "CHRISTINA", "description": "" }, { "date": "1/20/2025", "value": "15215", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/jSUzn36XMMGEw9c92b0S.jpg", "title": "SUSAN", "description": "" }, { "date": "1/21/2025", "value": "17201", "significant": "", "image": "https://storage.googleapis.com/glide-prod.appspot.com/uploads-v2/ZeEfA81C9UEE3YTyyHMo/pub/2uy9O5TucWARKTAwc4v4.jpg", "title": "CHRISTINA", "description": "" } ]
STEP 3: CONFIGURE SETTINGS & DISPLAY
Open the JavaScript column and set:
- P1: JSON – Basic Text column containing the Sample App JSON data
- P2: Calendar year (e.g., 2025)
- P3: A settings string using double-pipe separators:
Min||Max||Color||Aggregation||Theme
Min
& Max
Define the data range for your color scale (1 to 100000 works well for the sample app JSON data):
- Values ≤ Min use the lightest intensity
- Values ≥ Max use the darkest intensity
- Intermediate values blend proportionally
Color
Specify in HEX (e.g., #39d400)
Aggregation
Supported: MIN, MAX, SUM, AVG, COUNT
Theme
Light or Dark
Example Setting for JavaScript columns.
• Insert two Rich Text components into your layout—one assigned to the Desktop JavaScript column and one to the Mobile JavaScript column.
• Apply opposite visibility rules to the two Rich Text components for seamless desktop-to-mobile transitions.
- Desktop Version (Show component when Device/Screen Size is not small)
- Mobile Version (Show component when Device/Screen Size is not large)
And that’s a wrap! You’ve now set your thresholds, picked your aggregation, and themed your calendar for both light and dark modes. Sit back, watch those days light up, and turn raw numbers into colorful insights.
Why don’t heat maps ever get cold? Because they always find a way to warm up your data!
Enjoy!
-Eric