用DataviewJS热力图显示当年日记中的任务完成情况

本教程将指导你如何使用 Obsidian 的 DataviewJS 插件创建一个热力图视图,用于展示日记文件中的任务完成情况。以下是完整的实现步骤。


功能概述

  • 日历式布局:每个月的小方块按照周排列,空白占位符正确显示。
  • 2行6列布局:每个月的小方块组按照2行6列排列。
  • 交互功能:鼠标悬停时显示工具提示(日期和任务数量)。
  • 动态边框:代表“今天”的小方块有黑色边框,方便快速定位。
  • 简洁美观:无边框设计,整体布局紧凑且现代化。

前置条件

  1. 安装 Obsidian 和必要插件

  2. 创建日记文件夹

    • 在你的 Vault 中创建一个名为 日记 的文件夹。

    • 每个日记文件的命名格式为 YYYY-MM-DD.md(例如 2023-10-05.md)。

    • 文件内容中使用 - [x] 标记已完成的任务。例如:

      - [x] 完成任务 1
      - [ ] 待办任务 2
      

实现步骤

1. 创建 DataviewJS 代码块

在任意笔记中插入以下 DataviewJS 代码块:

// 获取当前年份
const currentYear = new Date().getFullYear();

// 定义颜色映射
const colors = {
    0: "#f0f0f0", // 更浅的灰色(替代白色)
    1: "#FFD1FF", // 稍微加深的浅薰衣草紫
    2: "#E6B3FF", // 柔和薰衣草紫
    3: "#9933FF", // 基础薰衣草紫
    4: "#6600CC", // 深薰衣草紫
    5: "#330099"  // 浓薰衣草紫
};

// 获取今天的日期,格式化为 YYYY-MM-DD
const today = new Date();
const todayFormatted = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;

// 获取“日记”文件夹中的所有文件
const diaryFolder = "日记"; // 日记文件夹路径
const files = app.vault.getMarkdownFiles().filter(file => file.path.startsWith(diaryFolder));

// 创建一个日期到任务数量的映射
const taskCounts = {};
for (let file of files) {
    const fileName = file.name.replace(".md", ""); // 去掉 .md 后缀
    if (!/^\d{4}-\d{2}-\d{2}$/.test(fileName)) continue; // 确保文件名是 YYYY-MM-DD 格式
    const date = fileName;
    const content = await app.vault.read(file);
    const completedTasks = (content.match(/- \[x\]/gi) || []).length; // 统计已完成任务数量
    taskCounts[date] = completedTasks;
}

// 辅助函数:判断某年是否为闰年
function isLeapYear(year) {
    return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}

// 生成热力图
console.log("开始生成热力图...");
const heatmapContainer = document.createElement("div");
heatmapContainer.className = "heatmap-container";

const months = ["January", "February", "March", "April", "May", "June",
                "July", "August", "September", "October", "November", "December"];
const monthDays = [31, isLeapYear(currentYear) ? 29 : 28, 31, 30, 31, 30,
                   31, 31, 30, 31, 30, 31];

months.forEach((month, monthIndex) => {
    console.log(`正在处理月份: ${month}`);

    // 创建每个月的容器
    const monthDiv = document.createElement("div");
    monthDiv.className = "month-container";

    // 添加月份标题
    const monthTitle = document.createElement("div");
    monthTitle.className = "month-title";
    monthTitle.textContent = month; // 设置月份名称
    monthDiv.appendChild(monthTitle);

    // 创建每个月的日历布局
    const calendarDiv = document.createElement("div");
    calendarDiv.className = "calendar";

    // 获取该月的第一天是星期几(0 表示周日,1 表示周一,依此类推)
    let firstDayOfWeek = new Date(currentYear, monthIndex, 1).getDay(); // 默认 0=周日, 1=周一...
    firstDayOfWeek = (firstDayOfWeek === 0) ? 6 : firstDayOfWeek - 1; // 调整为周一作为第一天

    const daysInMonth = monthDays[monthIndex];

    // 添加空白占位符(如果该月的第一天不是周一)
    for (let i = 0; i < firstDayOfWeek; i++) {
        const placeholderDiv = document.createElement("div");
        placeholderDiv.className = "day placeholder";
        calendarDiv.appendChild(placeholderDiv);
    }

    // 添加每一天的小方块
    for (let day = 1; day <= daysInMonth; day++) {
        const date = `${currentYear}-${String(monthIndex + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
        const count = taskCounts[date] || 0;
        const color = colors[Math.min(count, 5)]; // 最大为 5 个任务
        const dayDiv = document.createElement("div");
        dayDiv.className = "day";
        dayDiv.style.backgroundColor = color;

        // 如果是今天的日期,添加黑色边框
        if (date === todayFormatted) {
            dayDiv.style.border = "1px solid #000"; // 黑色边框
        } else {
            dayDiv.style.border = "none"; // 其他日期无边框
        }

        // 添加自定义工具提示
        const tooltip = document.createElement("div");
        tooltip.className = "tooltip";
        tooltip.textContent = `${date}: ${count} tasks`;
        tooltip.style.display = "none"; // 默认隐藏
        dayDiv.appendChild(tooltip);

        // 鼠标悬停时显示工具提示
        dayDiv.addEventListener("mouseenter", () => {
            tooltip.style.display = "block";
        });

        // 鼠标移出时隐藏工具提示
        dayDiv.addEventListener("mouseleave", () => {
            tooltip.style.display = "none";
        });

        calendarDiv.appendChild(dayDiv);
    }

    // 将日历布局添加到月份容器中
    monthDiv.appendChild(calendarDiv);
    heatmapContainer.appendChild(monthDiv);
});
dv.container.appendChild(heatmapContainer); // 将热力图插入到 DataviewJS 容器中
console.log("热力图生成完成!");

2. 添加 CSS 样式

为了使热力图更加美观,我们需要添加一些 CSS 样式。以下是具体操作步骤:

(a) 打开 Obsidian 的 CSS 片段设置

  1. 打开设置

    • 在 Obsidian 中,点击左下角的齿轮图标(设置)。
  2. 进入外观设置

    • 在设置页面中,找到并点击“外观”选项。
  3. 打开 CSS 片段文件夹

    • 向下滚动到“CSS 片段”部分,点击“打开片段文件夹”按钮。
    • 这将打开一个文件夹,存储所有自定义的 CSS 文件。

(b) 创建新的 CSS 文件

  1. 新建文件

    • 在打开的 CSS 片段文件夹中,右键单击空白区域,选择“新建文件”。
    • 将文件命名为 heatmap.css(或其他你喜欢的名字,但必须以 .css 结尾)。
  2. 粘贴样式代码

    • 打开刚刚创建的 heatmap.css 文件,将以下样式代码粘贴进去:
/* 热力图容器样式 */
.heatmap-container {
    display: grid;
    grid-template-columns: repeat(6, 1fr); /* 四列布局 */
    grid-template-rows: repeat(2, auto);   /* 三行布局 */
    gap: 5px; /* 缩小每个月之间的间距 */
    padding: 5px;
    width: 750px; /* 容器宽度 */
}

/* 每个月的容器样式 */
.month-container {
    display: flex;
    flex-direction: column;
    gap: 5px; /* 缩小标题和日历之间的间距 */
    background-color: transparent; /* 移除背景色 */
    border: none; /* 移除边框 */
    padding: 0; /* 移除内边距 */
}

/* 每个月的标题样式 */
.month-title {
    font-size: 14px; /* 缩小字体大小 */
    font-weight: bold; /* 加粗 */
    text-align: center; /* 居中对齐 */
    color: #333; /* 文字颜色 */
    margin-bottom: 5px; /* 缩小标题与日历的间距 */
}

/* 日历布局样式 */
.calendar {
    display: grid;
    grid-template-columns: repeat(7, 15px); /* 每周 7 天,每列固定宽度 15px */
    row-gap: 1px;    /* 纵向间距 */
    column-gap: 1px; /* 横向间距 */
    justify-content: center; /* 居中对齐 */
}

/* 每个小方块的样式 */
.day {
    width: 15px;  /* 固定宽度 */
    height: 15px; /* 固定高度 */
    border: none; /* 移除边框 */
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative; /* 用于定位工具提示 */
}

/* 空白占位符样式 */
.placeholder {
    background-color: transparent; /* 空白占位符 */
    border: none; /* 隐藏边框 */
}

/* 自定义工具提示样式 */
.tooltip {
    position: absolute;
    top: 100%; /* 显示在小方块下方 */
    left: 50%;
    transform: translateX(-50%); /* 水平居中 */
    background-color: #333; /* 背景颜色 */
    color: #fff; /* 文字颜色 */
    padding: 5px;
    border-radius: 4px; /* 圆角 */
    white-space: nowrap; /* 防止文字换行 */
    font-size: 12px; /* 字体大小 */
    z-index: 10; /* 确保工具提示在最上层 */
    display: none; /* 默认隐藏 */
}

/* 鼠标悬停时显示工具提示 */
.day:hover .tooltip {
    display: block; /* 显示工具提示 */
}

(c) 启用 CSS 片段

  1. 返回 Obsidian 设置

    • 回到 Obsidian 的设置页面,刷新“CSS 片段”部分。
  2. 启用片段

    • 你会看到刚刚创建的 heatmap.css 文件出现在列表中。
    • 点击右侧的开关,启用该片段。
  3. 验证样式

    • 刷新 Obsidian 页面(或重新打开笔记),确认热力图的样式是否生效。

3. 测试和验证

  1. 运行代码

    • 打开包含 DataviewJS 代码块的笔记,确认热力图正确生成。
  2. 检查功能

    • 确认每个月的小方块是否按周排列。
    • 确认鼠标悬停提示是否正常工作。
    • 确认“今天”的小方块是否有黑色边框。
  3. 调整细节

    • 如果需要进一步优化,可以根据需求调整 CSS 样式或 JavaScript 逻辑。

常见问题

  1. 热力图未显示

    • 确保日记文件夹路径正确(默认为 日记)。
    • 确保日记文件的命名格式为 YYYY-MM-DD.md
  2. CSS 样式未生效

    • 确保 CSS 文件名正确(例如 heatmap.css)。
    • 确保 CSS 片段已启用。
  3. 任务统计不准确

    • 确保日记文件中使用了 - [x] 标记已完成任务。

总结

通过以上步骤,你可以轻松实现一个功能强大的热力图视图,用于展示日记文件中的任务完成情况。希望这个教程对你有所帮助!如果你有任何问题或建议,请随时提出。


祝你使用愉快! :blush: