Skip to main content
Skip table of contents

How to: Create a Calendar Heatmap

image-20250819-151953.png

Steps to create:

  1. Select a chart you want to replace the calendar heatmap with and go to the code view

  2. Select the Date Column and the metric you want to display

    1. image-20250819-164431.png

  3. Paste the following code, replace the dateColumn, metricColumn and metricPrefix to the date and metric column you selected. If the metric is not a currency, change the metricPrefix to remove the dollar sign:

    JS
    function CalendarHeatmapByDay({ data = [] }) {
    
      const dateColumn = 'date';
      const metricColumn = 'cost';
      const metricPrefix = '$';
    
      const byDate = new Map();
      const yearsSet = new Set();
    
      data.forEach(item => {
        if (!item[dateColumn]) return;
        const [mm, dd, yyyy] = item[dateColumn].split("/").map(s => s.padStart(2, "0"));
        const iso = `${yyyy}-${mm}-${dd}`;
        yearsSet.add(yyyy);
        const v = Number(item[metricColumn]) || 0;
        byDate.set(iso, (byDate.get(iso) || 0) + v);
      });
    
      const years = [...yearsSet].sort((a, b) => Number(a) - Number(b));
      const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
    
      const maxCost = byDate.size === 0 ? 0 : Math.max(...byDate.values());
      const GREENS = [
        "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d",
        "#238b45", "#006d2c", "#00441b"
      ];
    
      const costs = Array.from(byDate.values()).filter(v => v > 0).sort((a, b) => a - b);
      const median = costs.length ? (costs[Math.floor((costs.length - 1) / 2)] + costs[Math.ceil((costs.length - 1) / 2)]) / 2 : 0;
      const absDevs = costs.map(v => Math.abs(v - median)).sort((a, b) => a - b);
      const mad = absDevs.length ? (absDevs[Math.floor((absDevs.length - 1) / 2)] + absDevs[Math.ceil((absDevs.length - 1) / 2)]) / 2 : 0;
      let scale = mad * 1.4826;
      if (!scale) {
        const min = costs.length ? costs[0] : 0;
        const max = costs.length ? costs[costs.length - 1] : 0;
        scale = (max - min) / 6 || 1;
      }
      const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
    
      const colorFor = cost => {
        if (cost <= 0 || !isFinite(cost)) return "#f0f0f0";
        const z = (cost - median) / scale;
        const zClamped = clamp(z, -2.5, 2.5);
        const t = (zClamped + 2.5) / 5;
        const idx = Math.round(t * (GREENS.length - 1));
        return GREENS[idx];
      };
    
      const daysInMonth = (y, m) => new Date(y, m + 1, 0).getDate();
      const firstDow = (y, m) => new Date(y, m, 1).getDay();
      const weeksInMonth = (y, m) => Math.ceil((firstDow(y, m) + daysInMonth(y, m)) / 7);
    
      const baseLabelCol = 18;
      const dim = 18;
      const dayGap = 4;
      var showWeekdayLabels = true;
    
      const showTooltip = (e, text) => {
        const tooltip = document.createElement('div');
        tooltip.style.position = 'fixed';
        tooltip.style.background = 'rgba(0,0,0,0.75)';
        tooltip.style.color = '#fff';
        tooltip.style.padding = '3px 6px';
        tooltip.style.borderRadius = '3px';
        tooltip.style.fontSize = '11px';
        tooltip.style.pointerEvents = 'none';
        tooltip.style.zIndex = '1000';
        tooltip.innerHTML = text;
        document.body.appendChild(tooltip);
    
        const rect = e.target.getBoundingClientRect();
        tooltip.style.top = `${rect.top - tooltip.offsetHeight - 4}px`;
        tooltip.style.left = `${rect.left + rect.width / 2 - tooltip.offsetWidth / 2}px`;
        e.target._tooltip = tooltip;
      };
    
      const hideTooltip = (e) => {
        if (e.target._tooltip) {
          document.body.removeChild(e.target._tooltip);
          e.target._tooltip = null;
        }
      };
    
      return React.createElement("div", { style: { padding: 12 } },
        React.createElement("div", { style: { marginBottom: 8, fontSize: 16, fontWeight: 700 } }, "Media Spend by Day"),
        years.length === 0
          ? React.createElement("div", { style: { color: "#666" } }, "No data.")
          : years.map(yyyy =>
            React.createElement("div", { key: yyyy, style: { marginBottom: 12 } },
              React.createElement("div", { style: { fontWeight: 700, fontSize: 14, marginBottom: 4 } }, yyyy),
              React.createElement("div", {
                style: {
                  display: "grid",
                  gridTemplateColumns: "repeat(auto-fill, 160px)",
                  justifyContent: "start",
                  gap: 8, // month gap
                  alignItems: "start",
                  gridAutoFlow: "row dense"
                }
              },
                Array.from({ length: 12 }).map((_, mIndex0) => {
                  const monthLabel = monthNames[mIndex0];
                  const weeks = weeksInMonth(Number(yyyy), mIndex0);
                  const firstDay = firstDow(Number(yyyy), mIndex0);
    
                  const tiles = [];
                  const totalDays = daysInMonth(Number(yyyy), mIndex0);
                  let hasData = false;
    
                  const labelCol = showWeekdayLabels ? baseLabelCol : 0;
                  const tileColStart = showWeekdayLabels ? 2 : 1;
    
                  for (let d = 1; d <= totalDays; d++) {
                    const dt = new Date(Number(yyyy), mIndex0, d);
                    const dow = dt.getDay();
                    const week = Math.floor((firstDay + (d - 1)) / 7);
                    const mm = String(mIndex0 + 1).padStart(2, "0");
                    const dd = String(d).padStart(2, "0");
                    const iso = `${yyyy}-${mm}-${dd}`;
                    const cost = byDate.get(iso) || 0;
                    if (cost > 0) hasData = true;
    
                    tiles.push(
                      React.createElement("div", {
                        key: iso,
                        style: {
                          gridRow: dow + 2,
                          gridColumn: week + tileColStart,
                          width: dim,
                          height: dim,
                          borderRadius: 3,
                          background: colorFor(cost),
                          cursor: cost > 0 ? "pointer" : "default"
                        },
                        onMouseEnter: (e) => showTooltip(e, `${monthLabel} ${d}, ${yyyy} — ${metricPrefix}${cost.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`),
                        onMouseLeave: hideTooltip
                      })
                    );
                  }
    
                  if (!hasData) return null;
    
                  const weekdayLabels = showWeekdayLabels
                    ? ["S", "M", "T", "W", "T", "F", "S"].map((lbl, i) =>
                      React.createElement("div", {
                        key: lbl,
                        style: {
                          gridRow: i + 2,
                          gridColumn: 1,
                          width: baseLabelCol,
                          textAlign: "right",
                          paddingRight: 2,
                          fontSize: 9,
                          color: "#888",
                          alignSelf: "center",
                          justifySelf: "end",
                          lineHeight: `${dim}px`
                        }
                      }, lbl)
                    )
                    : [];
                  showWeekdayLabels = false;
                  return React.createElement("div", {
                    key: `${yyyy}-${mIndex0}`,
                    style: {
                      maxWidth: "160px",
                      borderRadius: 8,
                      padding: 4,
                      background: "#fff"
                    }
                  },
                    React.createElement("div", {
                      style: { fontWeight: 600, fontSize: 12, marginBottom: 4, textAlign: "center" }
                    }, monthLabel),
                    React.createElement("div", {
                      style: {
                        display: "grid",
                        gridTemplateColumns: `${showWeekdayLabels ? baseLabelCol + 'px ' : ''}repeat(${weeks}, ${dim}px)`,
                        gridTemplateRows: `12px repeat(7, ${dim}px)`,
                        gap: dayGap,
                        alignItems: "center",
                        justifyContent: "start"
                      }
                    },
                      showWeekdayLabels ? React.createElement("div", { style: { gridRow: 1, gridColumn: 1 } }) : null,
                      ...Array.from({ length: weeks }).map((_, w) =>
                        React.createElement("div", {
                          key: `w-${w}`,
                          style: {
                            gridRow: 1,
                            gridColumn: w + tileColStart,
                            fontSize: 9,
                            color: "#aaa",
                            justifySelf: "center"
                          }
                        })
                      ),
                      ...weekdayLabels,
                      ...tiles
                    )
                  );
                })
              )
            )
          ),
        React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4, marginTop: 6 } },
          React.createElement("span", { style: { fontSize: 11, color: "#666" } }, "Less"),
          ...GREENS.map((c, i) =>
            React.createElement("div", { key: i, style: { width: 12, height: 12, borderRadius: 3, background: c } })
          ),
          React.createElement("span", { style: { fontSize: 11, color: "#666" } }, "More")
        )
      );
    }
    
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.