---
cssclasses:
  - hide-properties
tags:
  - aiassist
modified: 20260405 15:00
created: 20260405 15:00
---

## Heatmap активности

> [!info] Описание
> GitHub-style карта активности за последний год. Каждая ячейка — один день, интенсивность цвета отражает количество созданных заметок.

```dataviewjs
// === Данные ===
const allNotes = dv.pages()
  .where(p => !p.file.path.startsWith("Файлы/"))
  .where(p => !p.file.path.startsWith("Шаблоны/"))
  .where(p => !p.file.path.startsWith("Базы/"))
  .where(p => !p.file.path.startsWith(".obsidian/"))
  .where(p => !p.file.path.startsWith("Бонус/"));

const heatData = {};
allNotes.forEach(p => {
  if (p.file.cday) {
    const dateStr = p.file.cday.toFormat("yyyy-MM-dd");
    heatData[dateStr] = (heatData[dateStr] || 0) + 1;
  }
});

const levelGetter = (count) => (count < 5 ? count : 5);

// === Константы ===
const today = new Date();
const daylong = 24 * 60 * 60 * 1000;
const todayDay = today.getDay();
const mondayOffset = todayDay === 0 ? 6 : todayDay - 1;
const startDay = today - (mondayOffset + 52 * 7) * daylong;

const dbNum = (num) => (num > 9 ? String(num) : "0" + num);
const months = ["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"];

function pluralNotes(n) {
  const mod100 = n % 100;
  const mod10 = n % 10;
  if (mod100 >= 11 && mod100 <= 19) return "заметок";
  if (mod10 === 1) return "заметка";
  if (mod10 >= 2 && mod10 <= 4) return "заметки";
  return "заметок";
}

// === Рендер ===
let code = `
<style>
.dms-heatmap {
  display: flex;
  flex-wrap: nowrap;
  justify-content: flex-end;
  width: 100%;
  overflow: visible;
}
.dms-heatmap-weekrow {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  flex-grow: 1;
  min-width: 18px;
  position: relative;
  padding-top: 28px;
}
.dms-heatmap-month {
  position: absolute;
  top: 0;
  line-height: 28px;
  font-size: 14px;
}
.dms-heatmap-day {
  border: 1px solid rgba(128, 128, 128, .08);
  background-color: rgba(128, 128, 128, .05);
  border-radius: 3px;
  margin: 0 1px 1px 0;
  box-sizing: border-box;
  cursor: pointer;
  position: relative;
}
.dms-heatmap-day:hover::before,
.dms-heatmap-day:hover::after {
  display: none !important;
}
.dms-heatmap-day.dms-heatmap-future-day {
  opacity: 0;
}
.dms-heatmap-day > img {
  position: relative;
  border: none;
  background: none;
  width: 100%;
  display: block;
  opacity: 0;
  z-index: -100;
}
.dms-heatmap-level-1 { background-color: hsl(120, 60%, 80%, .5); }
.dms-heatmap-level-2 { background-color: hsl(120, 60%, 70%, .5); }
.dms-heatmap-level-3 { background-color: hsl(120, 60%, 60%, .5); }
.dms-heatmap-level-4 { background-color: hsl(120, 60%, 50%, .5); }
.dms-heatmap-level-5 { background-color: hsl(120, 60%, 40%, .5); }
.dms-heatmap-weekday-mark {
  border: 1px solid rgba(128, 128, 128, 0);
  background: none;
}
.dms-heatmap-mark-show {
  width: 100%;
  flex-grow: 0;
  text-align: right;
  font-size: 12px;
  line-height: 2em;
}
.dms-heatmap-level-mark {
  display: inline-block;
  width: 12px;
  height: 12px;
}
@media screen and (max-width: 480px) {
  .dms-heatmap-weekrow {
    min-width: 32px;
  }
}
</style>
<div class="dms-heatmap">`;

let dayCount = 0;

const addDay = (className, tipLabel) => {
  code += '<div class="' + className.join(" ") + '" ' + tipLabel + ' data-heatmap-tooltip>' +
    '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADElEQVQImWP4//8/AAX+Av5Y8msOAAAAAElFTkSuQmCC">' +
    '</div>';
};

for (let weekIndex = 0; weekIndex < 53; weekIndex++) {
  if (startDay + dayCount * daylong > today) break;
  code += '<div class="dms-heatmap-weekrow">';
  for (let d = 0; d < 7; d++) {
    const theDay = new Date(startDay + dayCount * daylong);
    const year = theDay.getFullYear();
    const month = dbNum(theDay.getMonth() + 1);
    const day = dbNum(theDay.getDate());
    const theDayStr = year + "-" + month + "-" + day;

    if (day === "01") code += '<div class="dms-heatmap-month">' + months[theDay.getMonth()] + '</div>';

    const count = heatData[theDayStr] || 0;
    const level = levelGetter(count);
    const className = ["dms-heatmap-day", "dms-heatmap-level-" + level];
    const future = dayCount > 364 && theDay - today >= daylong - 1;
    if (future) className.push("dms-heatmap-future-day");
    if (d === 0) className.push("dms-heatmap-day-first");
    if (d === 6) className.push("dms-heatmap-day-last");

    const dateLabel = months[theDay.getMonth()] + " " + theDay.getDate() + ", " + theDay.getFullYear();
    const tipLabel = !future
      ? 'aria-label="' + count + " " + pluralNotes(count) + " — " + dateLabel + '"'
      : "";

    addDay(className, tipLabel);
    dayCount++;
  }
  code += '</div>';
}
code += '</div>';

// Легенда
code += '<div class="dms-heatmap-mark-show">Мало ';
for (let d = 0; d < 6; d++) {
  const className = ["dms-heatmap-day", "dms-heatmap-level-mark", "dms-heatmap-level-" + d];
  const tipLabel = 'aria-label="уровень ' + d + '"';
  addDay(className, tipLabel);
}
code += ' Много</div>';

// Рендер через Shadow DOM
const root = dv.el("div", "", { cls: "dms-heatmap-container" });
const rootShadow = root.attachShadow({ mode: "open" });
rootShadow.innerHTML = code;

// Тултипы
if (!document.getElementById("dms-heatmap-tooltip")) {
  const tooltip = document.createElement("div");
  tooltip.id = "dms-heatmap-tooltip";
  Object.assign(tooltip.style, {
    position: "fixed", pointerEvents: "none", zIndex: "9999",
    background: "#333336", color: "#CCCCCE", borderRadius: "3px",
    boxShadow: "0 2px 6px rgba(0,0,0,.1)", fontSize: "12px",
    lineHeight: "12px", padding: "3px 6px", whiteSpace: "nowrap",
    display: "none"
  });
  document.body.appendChild(tooltip);
}
const tooltip = document.getElementById("dms-heatmap-tooltip");

function showTooltip(text, x, y) {
  tooltip.textContent = text;
  tooltip.style.display = "block";
  const margin = 10;
  tooltip.style.left = x + margin + "px";
  tooltip.style.top = y - 20 + "px";
  const rect = tooltip.getBoundingClientRect();
  if (rect.right > window.innerWidth) tooltip.style.left = window.innerWidth - rect.width - margin + "px";
  if (rect.left < 0) tooltip.style.left = margin + "px";
  if (rect.top < 0) tooltip.style.top = margin + "px";
}

setTimeout(() => {
  const shadow = root.shadowRoot;
  if (!shadow) return;
  shadow.querySelectorAll("[data-heatmap-tooltip]").forEach(el => {
    el.addEventListener("mouseenter", e => {
      const label = el.getAttribute("aria-label");
      if (!label) return;
      const rect = el.getBoundingClientRect();
      showTooltip(label, rect.right, rect.top);
    });
    el.addEventListener("mousemove", e => {
      const label = el.getAttribute("aria-label");
      if (!label) return;
      showTooltip(label, e.clientX, e.clientY);
    });
    el.addEventListener("mouseleave", () => { tooltip.style.display = "none"; });
  });
}, 0);
```
