Obsidian 教程:3种方法实现习惯打卡热力图 (Tracker/Heatmap Calendar/DataviewJS) | 视觉化你的自律与进步
下载文件
Heatmap-Dataviewjs
// --- 配置项 ---
// 1. 请将 "notes/diary" 替换为你的日记文件夹路径
const DIARY_FOLDER = "diary";
// 2. 请将 "阅读" 替换为你想要追踪的任务关键词
const TASK_TO_TRACK = "fitness";
// 3. 指定要显示的月份,格式为 "YYYY-MM"
const HEATMAP_MONTH = "2025-07";
// --- 配置结束 ---
// --- 配置项 ---
// 1. 文件夹路径
const DIARY_FOLDER = "diary";
// 2. 追踪的任务关键词
const TASK_TO_TRACK = "fitness";
// 3. 显示的月份
const HEATMAP_MONTH = "2025-07";
// 4. 颜色配置 (您可以随意修改这里的颜色代码!)
const COMPLETED_COLOR = "#006d77"; // 已完成任务的颜色 (推荐: #216e39, #006d77)
const INCOMPLETE_COLOR = "#e2f0f1"; // 未完成任务的颜色 (推荐: #d6e6d7, #e2f0f1)
const TEXT_COLOR_DARK_BG = "#ffffff"; // 深色背景上的文字颜色 (通常是白色)
const TEXT_COLOR_LIGHT_BG = "#000000"; // 浅色背景上的文字颜色 (通常是黑色)
// --- 配置结束 ---
const targetMoment = moment(HEATMAP_MONTH, "YYYY-MM");
if (!targetMoment.isValid()) {
dv.el("p", `❌ **错误:** 无效的月份格式。`);
} else {
// --- 1. 数据准备 ---
const targetYear = targetMoment.year();
const targetMonth = targetMoment.month();
const taskCompletionData = new Map();
const pages = dv.pages(`"${DIARY_FOLDER}"`).where(p => p.file.day);
const processedTaskQuery = TASK_TO_TRACK.trim().toLowerCase();
for (const page of pages) {
const pageMoment = moment(page.file.day.toJSDate());
if (pageMoment.year() !== targetYear || pageMoment.month() !== targetMonth) continue;
const tasks = page.file.tasks;
const specificTask = tasks.find(t => t.text.trim().toLowerCase().includes(processedTaskQuery));
if (specificTask) {
taskCompletionData.set(page.file.day.day, specificTask.completed);
}
}
// --- 2. 样式定义 (只保留布局) ---
dv.el("style", `
.heatmap-calendar-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; width: 100%; max-width: 350px; margin: 0 auto; }
.heatmap-calendar-grid { border-collapse: collapse; width: 100%; }
.heatmap-calendar-grid th, .heatmap-calendar-grid td { text-align: center; padding: 0; width: 14.28%; height: 40px; line-height: 40px; font-size: 12px; }
.heatmap-calendar-grid th { font-weight: bold; color: var(--text-muted); }
.day-cell { border: 2px solid var(--background-primary); border-radius: 8px; background-color: var(--background-secondary); color: var(--text-normal); }
.day-cell.empty { background-color: transparent; border: none; }
`);
// --- 3. 绘制日历 ---
const daysInMonth = targetMoment.daysInMonth();
const firstDayOfMonth = targetMoment.clone().startOf('month').day();
let calendarHtml = `<div class="heatmap-calendar-container">`;
calendarHtml += `<h2>${targetMoment.format("YYYY 年 MMMM")}</h2>`;
calendarHtml += `<table class="heatmap-calendar-grid">`;
calendarHtml += `<thead><tr><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr></thead>`;
calendarHtml += `<tbody><tr>`;
for (let i = 0; i < firstDayOfMonth; i++) { calendarHtml += `<td class="day-cell empty"></td>`; }
for (let day = 1; day <= daysInMonth; day++) {
const currentDayOfWeek = (firstDayOfMonth + day - 1) % 7;
if (currentDayOfWeek === 0 && day !== 1) { calendarHtml += `</tr><tr>`; }
const status = taskCompletionData.get(day);
let styleString = "";
if (status === true) {
styleString = `style="background-color: ${COMPLETED_COLOR}; color: ${TEXT_COLOR_DARK_BG}; font-weight: bold;"`;
} else if (status === false) {
styleString = `style="background-color: ${INCOMPLETE_COLOR}; color: ${TEXT_COLOR_LIGHT_BG};"`;
}
calendarHtml += `<td class="day-cell" ${styleString}>${day}</td>`;
}
let lastDayOfWeek = (firstDayOfMonth + daysInMonth - 1) % 7;
while (lastDayOfWeek < 6) { calendarHtml += `<td class="day-cell empty"></td>`; lastDayOfWeek++; }
calendarHtml += `</tr></tbody></table></div>`;
dv.el("div", calendarHtml, { raw: true });
}
HeatMap-Canlendar
代码
// --- 配置区 ---
// 1. 请在这里修改您的日记文件夹名称
const DIARY_FOLDER = 'diary';
// 2. 定义所有你想追踪的习惯,以及它们的配置
const habitsToTrack = {
"fitness": { color: "orange", icon: "🏋️" },
"reading": { color: "blue", icon: "📚" },
"meditation": { color: "purple", icon: "🧘" }
};
// --- 代码开始 ---
if (typeof renderHeatmapCalendar !== 'function') {
dv.el("div", "❌ **错误:** 找不到 `renderHeatmapCalendar` 函数。");
} else {
for (const habitName in habitsToTrack) {
const config = habitsToTrack[habitName];
dv.header(3, `${config.icon} ${habitName.charAt(0).toUpperCase() + habitName.slice(1)}`);
const entries = [];
for (const page of dv.pages(`"${DIARY_FOLDER}" and #daily-note`)) {
if (page.file.day && page.file.tasks.some(t => t.completed && t.text.toLowerCase().trim() === habitName)) {
entries.push({
date: page.file.day.toISODate(),
intensity: 1,
color: config.color,
});
}
}
// 【调色板】在这里定义或修改您的颜色主题
const calendarData = {
colors: {
blue: ["#8cb9ff", "#69a3ff", "#428bff", "#1872ff", "#0058e2"],
orange: ["#ffa244", "#fd7f00", "#dd6f00", "#bf6000", "#9b4e00"],
pink: ["#ff96cb", "#ff70b8", "#ff3a9d", "#ee0077", "#c30062"],
purple: ["#d8b4fe", "#c084fc", "#a855f7", "#9333ea", "#7e22ce"],
},
entries: entries,
};
const container = dv.el("div", "");
renderHeatmapCalendar(container, calendarData);
}
}
// --- 喝水热力图 ---
// --- 配置区 ---
// 1. 请在这里修改您的日记文件夹名称
const DIARY_FOLDER = 'diary';
// 2. 您想追踪的行内字段的名称
const FIELD_NAME = "喝水";
// 3. 为这个热力图选择一个颜色主题
const COLOR_THEME = "red";
// --- 代码开始 ---
dv.header(3, `💧 ${FIELD_NAME}热力图`);
// 步骤 1: 检查核心函数是否存在
if (typeof renderHeatmapCalendar !== 'function') {
dv.el("div", "❌ **错误:** 找不到 `renderHeatmapCalendar` 函数。请确保 Heatmap Calendar 插件已启用并刷新 Obsidian (Ctrl/Cmd + R)。", {
attr: { style: "background-color: var(--background-secondary); border-left: 4px solid var(--color-red); padding: 12px;" }
});
} else {
// 步骤 2: 使用同步循环来安全地构建数据
const entries = [];
// 使用精确查询(文件夹+标签)
for (const page of dv.pages(`"${DIARY_FOLDER}" and #daily-note`)) {
// 检查页面是否同时拥有:有效的文件名日期 和 我们要追踪的字段
if (page.file.day && page[FIELD_NAME]) {
entries.push({
// 从文件名获取正确日期
date: page.file.day.toISODate(),
// 【关键】将字段的数值赋值给 intensity
intensity: page[FIELD_NAME],
// 使用配置中定义的颜色主题
color: COLOR_THEME,
});
}
}
// 步骤 3: 准备最终的日历数据
const calendarData = {
colors: {
blue: ["#8cb9ff", "#69a3ff", "#428bff", "#1872ff", "#0058e2"],
green: ["#c6e48b", "#7bc96f", "#49af5d", "#2e8840", "#196127"],
red: ["#ff9e82", "#ff7b55", "#ff4d1a", "#e73400", "#bd2a00"],
orange: ["#ffa244", "#fd7f00", "#dd6f00", "#bf6000", "#9b4e00"],
pink: ["#ff96cb", "#ff70b8", "#ff3a9d", "#ee0077", "#c30062"],
},
entries: entries,
};
// 步骤 4: 渲染日历
const container = dv.el("div", "");
renderHeatmapCalendar(container, calendarData);
dv.paragraph(`*图表已生成。共找到了 ${entries.length} 条 "${FIELD_NAME}" 记录。*`);
}
HeatMap-Tracker
searchType: task.done
searchTarget: fitness
datasetName: "健身"
folder: diary
endDate: 2025-07-31
month:
searchType: task.done
searchTarget: reading
datasetName: "阅读"
folder: diary
endDate: 2025-07-31
month:
searchType: task.done
searchTarget: meditation
datasetName: "冥想"
folder: diary
endDate: 2025-07-31
month:
color: "#e06c75"
Heatmap-Calendar-AllYear
// --- 全年模拟数据热力图 (精简与美化版) ---
// 步骤 1: 检查核心函数是否存在
if (typeof renderHeatmapCalendar !== 'function') {
dv.el("div", "❌ **错误:** 找不到 `renderHeatmapCalendar` 函数。");
} else {
// ====================================================================
// 步骤 2: 定义一个函数,用于生成一整年的随机数据
// ====================================================================
/**
* @param {number} year - 要生成的年份, 例如 2024
* @param {string} color - 该数据集要使用的主题颜色名
* @param {number} probability - 每一天有数据的概率 (0.0 to 1.0)
* @param {number} minIntensity - 随机强度的最小值
* @param {number} maxIntensity - 随机强度的最大值
* @returns {Array} - 返回一个包含全年随机数据的 entries 数组
*/
function generateFakeYearData(year, color, probability, minIntensity, maxIntensity) {
const entries = [];
let currentDate = dv.luxon.DateTime.fromObject({ year: year, month: 1, day: 1 });
while (currentDate.year === year) {
if (Math.random() < probability) {
const randomIntensity = Math.floor(Math.random() * (maxIntensity - minIntensity + 1)) + minIntensity;
entries.push({
date: currentDate.toISODate(),
intensity: randomIntensity,
color: color,
});
}
currentDate = currentDate.plus({ days: 1 });
}
return entries;
}
// ====================================================================
// 步骤 3: 配置您想要生成的热力图
// ====================================================================
const heatmapsToGenerate = [
{
name: "项目A贡献",
color: "green",
probability: 0.6,
minIntensity: 1,
maxIntensity: 10,
},
{
name: "精力水平",
color: "teal", // <--- 已替换为更高级的青色系
probability: 0.8,
minIntensity: 1,
maxIntensity: 5,
},
{
name: "学习时长(小时)",
color: "amber", // <--- 已替换为更高级的琥珀色系
probability: 0.5,
minIntensity: 1,
maxIntensity: 4,
}
];
// ====================================================================
// 步骤 4: 循环遍历配置,生成并渲染每一个热力图
// ====================================================================
const currentYear = new Date().getFullYear();
for (const config of heatmapsToGenerate) {
// 【已修改】删除了 Emoji
dv.header(3, config.name);
const fakeEntries = generateFakeYearData(
currentYear,
config.color,
config.probability,
config.minIntensity,
config.maxIntensity
);
// 【已修改】在调色板中加入了新的颜色主题
const calendarData = {
year: currentYear,
colors: {
green: ["#c6e48b", "#7bc96f", "#49af5d", "#2e8840", "#196127"],
// 新增:专业且冷静的青色系
teal: ["#99f6e4", "#5eead4", "#2dd4bf", "#14b8a6", "#0f766e"],
// 新增:温暖且沉稳的琥珀色系
amber: ["#fde68a", "#fcd34d", "#fbbf24", "#f59e0b", "#d97706"],
},
entries: fakeEntries,
};
const container = dv.el("div", "");
renderHeatmapCalendar(container, calendarData);
// 【已修改】删除了末尾的统计文字
}
}