Skip to content
Tutorial

How to Add Habit Tracking to Obsidian with JavaScript and Dataview

Walkthrough guide for creating an easily customizable script which keeps track of your habit formation progress using your Obsidian.
Comments

Overview

I was recently looking for a simple solution to add a habit-building progress tracker to my already existing Obsidian daily note practice. An extensive online search, however, turned up no practical solutions. Existing methods were either not sufficiently versatile to be robust over the long term, or required convoluted extra steps in my daily workflow, or worse, bloated my notes with unreadable content. The following is a quick script based on easily modifiable front matter added to the daily note template. It extracts the content and creates a visualization using the existing dataview plugin API.

Obsidian Habit Tracking
Flexible metadata in the front matter is translated into an easily readable table view.

Why Obsidian

Capture Everything

I'm assuming you're reading this because you did an internet search for "Obsidian Habit tracker", but just in case you're not: Obsidian is a markdown text editor that is designed to be your 'digital brain'. The concept being record every idea, every note, every to-do list in one spot to free up your brain from exerting energy reminding you of all the things you need to do. For a more complete understanding of the value of freeing your mind, read David Allen, Getting Things Done.

Customizable

Obsidian can be extended through community-created plugins. This script requires the dataview plugin to be installed and activated. Dataview allows you to perform queries against the files in your Obsidian folder, treating it as a sort of database.

Portability

Unlike other note capture systems—Evernote and Notion, for example—Obsidian keeps all your notes stored on your computer in a nonproprietary file format. Your notes are readable as raw text files. When you stop using Obsidian, and eventually you will, your notes can move with you to your new system rather than being lost to a legacy application.

Following the principle of portability, notes should not be dependent on tools, plugins, or systems for the content to be readable. The data for this is recorded in the easily readable, plain language, front matter of each post so that it's meaningful on its own. And the visualization code is in a discrete file that can be viewed on its own or embedded in other files (following principles of DRY and separation of concerns).

The Code

For the reasons outlined above, data is recorded in post YAML front matter of your daily posts. YAML uses semantic formatting; the following are five properties of Habits due to the indentation. Grab the full code on Github.

Habits:
 Meditate: False
 Steps: 9023
 Read: "Atomic Habits"
 Exercise: Bike Ride
 Vitamins: True

Each can take any value. Truthy values will mark the habit complete, fallsy values remain incomplete (eg. False, 0, Null, or just empty)

The following code creates the visualization. I load it from a separate file which I keep a dedicated snippets folder. You can save it as a js file, or embedded in markdown directly by wrapping it in a dataviewjs codeblock. The Dataview Documentation has more details on embedding options.

```dataviewjs

// Create a daily Habit Tracking visualization
dv.header(2,"Daily Habit Tracker");

// Returns the currently tracked Habits from the most recent daily note
// (assumes files are named by date in "Daily_notes" folder)
let currentHabits = () => dv.array( dv.pages('"Daily_notes"')
		.filter(d => d.Habits))
		.sort(d => d.file.name, 'desc')[0]['Habits']

// Array of Habit titles			
let keys = Object.keys(currentHabits())

// Optional text to icon dictionary for headings
// Modify this to match front matter 
let prettyHeadings = {
	Exercise: "💪",
	Meditate: "🧘",
	Read: "🤓",
	Steps: "🦶",
	Vitamins: "💊"
}

// Returns Array of habit names translated to any icon dictionary matches
let prettifyHeading = (arr) => {
	let newArr = [...arr]
	return newArr.map( v => prettyHeadings[v] ?? v)
}

// Returns array with link to day file and values for that days habits
let dayResults = (d) => {
	let results = [d.file.link,]
	for (let i in keys) {
		results.push( d.Habits[keys[i]] ?  "🟩" : "⬜️" )
	}
	return results
}

// Dataview Array of tracked habits in reverse chronological order
let tabledata = dv.pages('"_Daily/day"') 
		.sort(d => d.file.name, 'desc')
		.filter(d => d.Habits && dv.date(d.file.name) < moment())
		.map(d => dayResults(d))

// Return array with current streak total for each habit
let streaks = (d) => {
  let x = {...keys}
  let t = 0
  let tableArray = d.array()
  let totals = []
  for (let i in x) {
   let t = 0
   for (let j = 0; j < tableArray.length; j++) {
     if (tableArray[j][parseInt(i) + 1] === "🟩") {
       t = t + 1
     } else {
		 if (j > 0) {
	       break;
		 }
     }
   }
   totals.push(t)
  }
  return totals
}

// Output a dv table of 10 most recent days habits
dv.table(
	["Day", ...prettifyHeading(keys)],
	[["Streaks",...streaks(tabledata)],...tabledata.slice(0, 10)]
)
```

Dependencies

For this to work, the Dataview plugin must be installed and activated.

Conclusion

Versatile, Robust, Extensible

This setup allows you to add any number of front-matter habits. As your priorities change, you can add or remove habits and the visualization will update to reflect your most recent focus. All without touching the code.

I like to change the text to icons for the visualization but this is optional and the dictionary can include as many or as few items as desired.

If you're reading this, then you probably already know the value of Obsidian and of Habit development. If you're new to either of these, here are some recommended resources on the topic:

Feel free to expand the code. Let me know if you think of ways to improve or add to it.