Turn any dated note into a beautiful, interactive heatmap — habits, workouts, mood, finances, work logs, anything.
Install · Quick start · Configuration · Export · FAQ · Example vault
You already write things down in Obsidian. Heatmap Tracker reads those notes and turns them into a year-at-a-glance grid — the kind you know from GitHub's contribution graph, but for your data, in your vault, with no account and no cloud.
Add steps: 8420 to a daily note and you get a heatmap. Add mood: 4 and you get another. Every filled square is clickable and opens the note behind it; every empty square offers to create it.
- Zero-config for frontmatter. A three-line codeblock is enough — no JavaScript required.
- Fully scriptable when you need it. A
dataviewjsescape hatch gives you complete control over the dataset. - Everything stays local. Your notes are the database.
- Install
- Quick start
- Use cases
- Codeblock usage
- Advanced usage (
dataviewjs) - Configuration reference
- Export a report
- Features
- FAQ & troubleshooting
- How it compares
- Development
- Contributing
- Support the project
- License & credits
- Open Settings → Community plugins and make sure Restricted mode is off.
- Click Browse, search for “Heatmap Tracker”.
- Click Install, then Enable.
Or open this link from inside Obsidian: Install Heatmap Tracker.
Heatmap Tracker uses Dataview to read data out of your notes. Install and enable it the same way. For the dataviewjs examples below, also enable Dataview → Settings → Enable JavaScript Queries.
- Download
main.js,styles.cssandmanifest.jsonfrom the latest release. - Put them in
<your-vault>/.obsidian/plugins/heatmap-tracker/. - Reload Obsidian and enable the plugin in Community plugins.
Install BRAT, then run BRAT: Add a beta plugin for testing and enter mokkiebear/heatmap-tracker.
Requirements: Obsidian 0.1.0+, desktop and mobile, Dataview plugin.
1. Put a value in a daily note. In 2026-08-04.md:
---
steps: 8420
---Numbers work (steps: 8420), and so do booleans — meditated: true simply counts as 1.
2. Add the heatmap. Run the command Insert Heatmap Tracker from the command palette and fill in the modal — this is the easiest path and writes the codeblock for you.
Or write it by hand:
```heatmap-tracker
property: steps
```3. That's it. Switch years with the arrows, hover a square for details, click one to open the note.
Tip
The Example Vault is a full working vault you can open in Obsidian — copy-pasteable examples for every parameter, plus ready-made trackers for habits, mood, sleep, water intake and projects. It's updated often.
| Track | Frontmatter | Why a heatmap helps | |
|---|---|---|---|
| 🏃 | Fitness | exercise: 45 |
Spot the weeks you skipped, not just the total |
| 🧘 | Habits | meditated: true |
Streaks become visible — and hard to break |
| 😴 | Sleep | hours-slept: 7.5 |
See the slow drift before it becomes a problem |
| 🙂 | Mood | mood: 4 |
Correlate bad stretches with what else was happening |
| 💰 | Finance | spent: 120 |
A year of spending on one screen |
| 📚 | Reading | pages-read: 30 |
Small daily numbers add up visibly |
| 💧 | Water intake | water: 6 |
Zero-effort adherence check |
| 💼 | Work log | hours: 8 |
Export it as a report for your manager |
| 🤕 | Symptoms | headache: 3 |
Bring a real timeline to your doctor |
| ✅ | Projects | tasks-done: 5 |
Momentum, not just a burn-down |
Every one of these has a working example in the Example Vault.
The heatmap-tracker codeblock handles frontmatter tracking out of the box.
```heatmap-tracker
property: exercise
```This looks for exercise in your notes and lights up a square wherever it's set.
```heatmap-tracker
property: [running, cycling, swimming]
```Values from all listed properties are aggregated into one heatmap.
path, tags and filters are all optional and can be combined:
```heatmap-tracker
property: exercise
path: "daily notes"
tags: [journal]
filters:
- property: status
operator: equals
value: done
```| Parameter | Description |
|---|---|
property |
Frontmatter key (or array of keys) to read. Required. |
path |
Folder to search in. Unset → falls back to your Daily Notes folder, then the whole vault. |
tags |
Only include notes with at least one of these tags (leading # optional). |
filters |
Extra frontmatter conditions. All must match. |
Each filters entry takes:
property— the frontmatter key to check.operator—equals,contains, ornotEmpty.value— compared against the property's value (not needed fornotEmpty).
When you need full control over the dataset — computed values, external sources, custom colors per entry — use a dataviewjs codeblock:
```dataviewjs
// Update this object
const trackerData = {
entries: [],
separateMonths: true,
heatmapTitle: "This is the title for your heatmap",
heatmapSubtitle: "This is the subtitle for your heatmap. You can use it as a description.",
}
// Path to the folder with notes
const PATH_TO_YOUR_FOLDER = "daily notes preview/notes";
// Name of the parameter you want to see on this heatmap
const PARAMETER_NAME = 'steps';
// You need dataviewjs plugin to get information from your pages
for (let page of dv.pages(`"${PATH_TO_YOUR_FOLDER}"`).where((p) => p[PARAMETER_NAME])) {
trackerData.entries.push({
date: page.file.name,
// Use absolute file path so clicks open the exact note
// (matters when several notes share a name)
filePath: page.file.path,
intensity: page[PARAMETER_NAME],
});
}
// Optional: set base path so new files are created here if missing
trackerData.basePath = PATH_TO_YOUR_FOLDER;
renderHeatmapTracker(this.container, trackerData);
```- If the entry has
filePath(page.file.path), that exact file opens. Missing? The plugin offers to create it at the same path. - Otherwise, if
trackerData.basePathis set, it proposes creating/openingbasePath/YYYY-MM-DD.md. - Otherwise it falls back to your Daily Notes settings (folder + format) via the Daily Notes API.
Note
dataviewjs requires Dataview → Settings → Enable JavaScript Queries. The plugin also works standalone with any JavaScript that can build an entries array — Dataview is just the most convenient source.
The authoritative reference for every trackerData parameter. Each one links to a copy-pasteable example in the Example Vault.
| Parameter | Type | Default |
|---|---|---|
year |
number |
current year |
heatmapTitle |
string | number |
undefined |
heatmapSubtitle |
string | number |
undefined |
colorScheme |
object |
{ paletteName: "default", customColors: [] } |
entries |
array |
[] |
showCurrentDayBorder |
boolean |
true |
intensityConfig |
object |
see below |
basePath |
string |
undefined |
separateMonths |
boolean |
true |
disableFileCreation |
boolean |
false |
insights |
array |
[] |
layout |
"default" | "monthly" |
"default" |
monthsToShow / daysToShow / startDate + endDate |
number / string |
undefined |
- Type:
number - Default: Current year (
new Date().getFullYear()) - Description: The year the heatmap displays by default.
- Example: year
- Type:
string | number - Default:
undefined - Description: Title displayed above the heatmap. Supports HTML for custom styling.
- Example: heatmapTitle
- Type:
string | number - Default:
undefined - Description: Subtitle/description displayed under the title. Supports HTML for custom styling.
- Example: heatmapSubtitle
- Type:
object - Default:
{
paletteName: "default",
customColors: []
}- Description: The color scale used to represent intensity levels. Each color maps to a range of data intensity. Use
paletteNameto reference a palette defined in plugin settings, orcustomColorsto pass your own array of colors inline. - Example: colorScheme
- Type:
string - Default:
undefined - Description: An entry property (set on an item inside
entries, not ontrackerDataitself). Sets the color for that specific entry, overridingcolorScheme.
- Type:
array - Default:
[
{ date: "1900-01-01", customColor: "#7bc96f", intensity: 5, content: "" }
]- Description: The list of data points. Each entry supports:
| Field | Description |
|---|---|
date |
Date of the entry (ISO string, YYYY-MM-DD) |
intensity |
Data intensity for that date |
content |
Optional tooltip / note text |
customColor |
Overrides the color for this entry |
filePath |
Absolute path to the file opened on click |
customHref |
Custom URL to open on click (takes precedence over filePath) |
- Example: entries
- Type:
boolean - Default:
true - Description: Highlights today's square with a border.
- Example: showCurrentDayBorder
- Type:
object - Default:
{
scaleStart: undefined,
scaleEnd: undefined,
defaultIntensity: 4,
showOutOfRange: true,
excludeFalsy: undefined
}- Description: Configures how entry values map to colors.
| Field | Description |
|---|---|
scaleStart / scaleEnd |
Min/max of the intensity scale. Useful for a custom range — e.g. tracking reading time only between 30 minutes and 2 hours. |
defaultIntensity |
Intensity assigned to entries that don't specify one. |
showOutOfRange |
Whether entries outside the scale are still shown (clamped) or hidden. |
excludeFalsy |
When true, entries with falsy intensity (0, undefined, null, false) are excluded and don't break streaks. |
- Example: intensityConfig
Important
Migrating from defaultEntryIntensity / intensityScaleStart / intensityScaleEnd: these top-level parameters are removed from the API described here. Old codeblocks keep working (they're folded into intensityConfig automatically), but new heatmaps should use intensityConfig directly.
- Type:
string - Default:
undefined - Description: Base folder used to collect entries. When set, the plugin proposes creating new files here when you click an empty square.
- Example: basePath
- Type:
boolean - Default:
true - Description: Whether months are visually separated within the heatmap layout.
- Example: separateMonths
- Type:
boolean - Default:
false - Description: When
true, clicking an empty square will not offer to create a new file. - Example: disableFileCreation
- Type:
array - Default:
[] - Description: Define your own calculated metrics, displayed in the Statistics tab — most productive day, longest streak, total pages read, average sleep, and anything else you can compute.
- Example: insights · 8 ready-made insights
- Type:
"default" | "monthly" - Default:
"default" - Description: Controls the grid arrangement.
"default"renders the traditional GitHub-style week-column grid."monthly"renders one row per month with days 1–31 as columns — a compact, calendar-style view. - Example: layout
These four parameters narrow which dates are displayed instead of the full year. Only one wins when several are set — they resolve in this order (highest priority first):
monthsToShow(number, defaultundefined) — current month plus the N previous months.monthsToShow: 3displays 4 rows (current month + 3 prior). Best paired withlayout: "monthly".daysToShow(number, defaultundefined) — the last N days ending today.startDate+endDate(string,YYYY-MM-DD, defaultundefined) — an explicit range. Both must be set, andstartDatemust not be afterendDate.
If none are set, the heatmap shows the full year.
This precedence is implemented once, in resolveDateRange — that function's doc comment is the source of truth if this section and the code ever disagree.
- Example: Date range parameters
Every heatmap has an Export tab alongside Heatmap Tracker / Statistics / Legend / Documentation. It turns your tracked data and daily notes into a single shareable report — a status update, a work log for a manager, an end-of-year summary — sitting between a bare calendar (too little context) and a folder of raw notes (too much noise).
Note content, aggregated. Below the calendar and legend, the report walks every week and day in range and pulls in that day's own note content — frontmatter stripped, bullet points kept as written, a day with no note showing - — grouped under "Week of …" and per-day headings. The reader gets everything that actually happened, day by day, in one document instead of clicking through each note.
Layout. "Weeks as columns" or "Weeks as rows", rendered pixel-for-pixel like the heatmap itself — including exact day-level month splitting (with a year label whenever the range crosses a year boundary) and, when a range is too wide or tall for one grid, automatic wrapping into multiple bands, evenly split rather than front-loading one band and leaving a small leftover.
Date range. Pick start/end dates directly, or jump to a range with a preset: Logged (the full span of your tracked data), Last year, Year to date, Last month, or Month to date.
Display options. Which day the week starts on, whether to show each week's start date, splitting the grid by month, month labels, and hiding weekends.
Legend editor. Colors are pre-populated from what's actually used in your data, so you only fill in what each one means. Reorder entries by drag-and-drop and toggle each category's visibility — shown everywhere, summary-only, or hidden entirely. Optionally combine the intensity colors into a single gradient swatch with one shared label and count, instead of a row per color.
Summary line. A compact, customizable breakdown like Workday: 22 · Leave: 1 · Rest day but worked: 1 with a total (e.g. Total hours: 169) — or hide the breakdown, the total, or all values entirely.
Output. Save as a Markdown note or a self-contained HTML file, to whichever folder in your vault you choose.
All of these preferences persist across sessions, so you only set them up once.
Easy switching between years — render a dynamic heatmap for any year
Left and right navigation arrows let you move across years effortlessly, so multi-year data is always one click away.
Customizable colors and intensity — match the heatmap to your data's meaning
Three levels of control, from broad to specific:
- Create your own palette in plugin settings (or use the default).
- Use
customColorsincolorSchemeto set colors for one specific heatmap. - Use
customColoron an individual entry.
User-defined insights — analyze data in the ways that matter to you
Define metrics like:
- The most productive day
- The longest streak without breaks
- The most active month
- Your average daily intensity
See insights documentation and 8 worked examples.
Monthly separation — visually separate months for better clarity
Toggle
separateMonths to add padding between months so month boundaries are unmistakable.
Monthly layout — a compact calendar with one row per month
Set
layout: "monthly" to switch from the GitHub-style grid to a calendar-style view with days 1–31 as columns. Combine with monthsToShow to display only recent months.
Statistics view — track progress with an integrated statistics panel
Totals, streaks and your own custom insights, in a dedicated tab next to the heatmap.
Export a report — turn tracked data and notes into shareable Markdown or HTML
The Export tab renders a grid matching your heatmap exactly, then aggregates each day's note content underneath it, grouped by week — plus a customizable legend, summary line, and date-range presets. See Export a report.
Localization — available in 9 languages
English, German, Russian, Chinese, Hindi, Spanish, French, Portuguese and Polish. See src/localization/locales for the current list, and docs/add-new-language.md if you'd like to contribute a translation.
Week numbers — display week numbers alongside the heatmap
Show all weeks, only even, only odd, or none — configurable in plugin settings.
Insert Heatmap Tracker command — add a tracker without writing YAML
An interactive modal builds the codeblock for you and inserts it at the cursor.
Customizable font — use your favorite typeface
Set the font in plugin settings, and use
HTML in titles and subtitles for further styling.
My heatmap is empty / nothing renders
Work through these in order:
- Is Dataview installed and enabled? Heatmap Tracker reads your notes through Dataview. Without it, there's no data to draw.
- Does the property actually exist in your notes? Property names are case-sensitive.
photo-takingandPhoto-Takingare different keys. - Are your notes in the searched folder? Without
path, the plugin falls back to your Daily Notes folder. If your notes live elsewhere, setpathexplicitly. - Is the year right? The heatmap defaults to the current year. If your data is from last year, use the arrows or set
year. - Check the console.
Ctrl+Shift+I(Windows/Linux) orCmd+Option+I(Mac) opens devtools — a missingpropertyparameter logs a warning there.
My dataviewjs block does nothing
JavaScript queries are off by default. Enable Dataview → Settings → Enable JavaScript Queries, then reload the note.
Dates are off by one day
This is almost always a timezone issue with how dates are parsed. Use plain YYYY-MM-DD strings for date rather than JavaScript Date objects or full ISO timestamps with a time component. If you're pulling from page.file.name on daily notes named YYYY-MM-DD.md, you're already doing the right thing.
Clicking a square opens the wrong note
If several notes share a filename, pass an absolute path so there's no ambiguity:
filePath: page.file.path // not page.file.nameClicking an empty square asks to create a file, and I don't want that
Set disableFileCreation: true on your trackerData.
Boolean properties — how are they counted?
true counts as 1, so meditated: true gives you a filled square. false counts as 0; combine with intensityConfig.excludeFalsy: true if you want those days treated as untracked rather than as zero.
Can I show several properties in one heatmap?
Yes — pass an array: property: [running, cycling, swimming]. Values are aggregated.
All my squares are the same color
Your values probably fall into a single bucket of the intensity scale. Set intensityConfig.scaleStart and scaleEnd to bracket the range you actually care about — e.g. scaleStart: 30, scaleEnd: 120 for reading minutes.
Does this work on mobile?
Yes. The plugin is not desktop-only and works on Obsidian mobile.
Does my data leave my vault?
No. Everything is computed locally from your notes. There's no account, no sync, no telemetry.
Can I use it without Dataview?
The heatmap-tracker codeblock needs Dataview. But renderHeatmapTracker(container, trackerData) accepts any entries array, so any JavaScript that can build that array works — Dataview is simply the most convenient source.
Still stuck? Open an issue — include your codeblock, a sample note's frontmatter, and your Obsidian and plugin versions.
Heatmap Tracker began as a rewrite of the excellent heatmap-calendar-obsidian by Richardsl, and grew from there.
| Heatmap Tracker | heatmap-calendar-obsidian | |
|---|---|---|
| Codeblock without JavaScript | ✅ heatmap-tracker block |
❌ dataviewjs required |
| Interactive insert command | ✅ Modal builder | ❌ |
| Statistics & custom insights | ✅ | ❌ |
| Monthly (calendar) layout | ✅ | ❌ |
| Flexible date ranges | ✅ Days, months, explicit range | ❌ Full year |
| Export to Markdown / HTML | ✅ | ❌ |
| Localization | ✅ 9 languages | ❌ |
| Click to open and create notes | ✅ | Partial |
| Actively maintained | ✅ | Limited |
If all you need is a one-year contribution grid from a dataviewjs script, the original is lighter. If you want tracking, statistics, reports and a no-code path, this is the one.
See ROADMAP.md for what's planned. Have an idea? Open an issue — feature requests genuinely shape this project.
New to the codebase? ARCHITECTURE.md maps how data flows from a codeblock or dataviewjs script through to the rendered heatmap.
git clone https://github.com/mokkiebear/heatmap-tracker.git
cd heatmap-tracker
npm install
npm run devnpm run dev starts the TS→JS transpiler and copies the generated JS/CSS/manifest into the example vault whenever they change. The hot-reload plugin — already installed in EXAMPLE_VAULT — then reloads Obsidian automatically, so you don't restart after every change.
If hot-reload isn't picking up changes, add an empty
.hotreloadfile toEXAMPLE_VAULT/.obsidian/plugins/heatmap-tracker/.
| Command | What it does |
|---|---|
npm run dev |
Watch mode + copy into the example vault |
npm run build |
Production build, ready for distribution |
npm test |
Run the Jest test suite |
npm run test:coverage |
Tests with a coverage report |
npm run test:utc / test:usa |
Run tests under specific timezones |
npm run lint / lint:fix |
ESLint |
npm run type-check |
TypeScript, no emit |
npm run format / format:check |
Prettier |
Stack: TypeScript · Preact · esbuild · Jest · i18next · Zod
Tip: Ctrl+Shift+I opens devtools inside Obsidian.
Further reading: style guide · adding a language · releasing
Contributions are welcome and appreciated — code, docs, translations, bug reports, or just telling me how you use it.
- 🐛 Report a bug
- 💡 Request a feature
- 🌍 Add a translation — a single JSON file
- 📖 Improve the docs or the Example Vault
- 🔧 Open a pull request
Read CONTRIBUTING.md before you start, and note that this project ships a Code of Conduct. Security issues go through SECURITY.md.
Heatmap Tracker is free and open source, built and maintained in my own time. If it's useful to you, the cheapest way to help is a ⭐ on the repo — it's how other people find the plugin.
Licensed under the Apache License 2.0.
Built by Maksim Rubanau and contributors.
Inspired by heatmap-calendar-obsidian by Richardsl. Powered by Obsidian Dataview.