Building a Powerful Habit Tracker in Obsidian: A Complete Guide

Hey Everyone, Stole so much from these forums over the years of using Obsidian and I wanted to give back.

I made a fully interactive and powerful habit tracker that uses analytics, trends, as well as a user friendly UI to help add some pizazz to our notes. check out the sample vault and article detailing how it works and how to customize it here:

13 Likes

I will download and try it just because it is so beautiful and polished! :smiley:

It is not something that I’m using Obsidian for, but if I were, I would definitely build on your project. It looks impressive and valuable. Good job! :clap:

Cheers, Marko :nerd_face:

First of all, thanks for this fantastic piece of work leveraging yet more Datacore!

The following is just some advice for people who want to use your script:

Stylistic changes

1

I was fiddling around with it and have found that despite modifying the colors as per your guide, the cards remained white.
So I replaced all instances of 'white' in your script with my choice of colour ('#1F1F1F').

Now, for other stylistic issues:

2

If the editor font size is too large (if you are zoomed out too much on PC and you set it too large), there are some misalignment issues here and there.

Quick fix:
Save and activate the following CSS snippet with any filename:

.markdown-preview-view.medium-font {
    font-size: 16px;
}

Add to the frontmatter of the md file housing the script:

---
cssclasses:
  - medium-font
---

3

I decided my dark mode was now more or less all right, but what about light mode?
Again, I applied a rather hackish quick fix:
Save and activate this CSS snippet:

.theme-light .habit-tracker-invert-colors {
  filter: invert(90%) sepia(95%) contrast(90%);
}
  • Users may want to adjust these numbers for a more seamless blend.
  • EDIT: This was enough in Windows, but not on Linux, for some reason.

So your frontmatter must look like:

---
cssclasses:
  - medium-font
  - habit-tracker-invert-colors
---

Frontmatter key changes

Currently, nested properties are used, which practice is currently not supported by Obsidian (ver. 1.7.7 at the time of this post).
We can see that in the Properties view: we cannot manipulate the individual keys and values.
The community plugin Linter also cannot lint the file, and if one wants to add Meta Bind buttons to the daily note to update the property values, the prop keys will be inaccessible.

FIx with ChatGPT

These following changes seem to have done the trick:
To adjust the script logic for flat keys in the frontmatter, we will need to make the following changes:

Key Adjustments

  1. Change Frontmatter Key Format: Update the script logic to use flat keys like habits-reading, habits-run_duration, etc., instead of nested properties.
  2. Modify Frontmatter Parsing: Adjust how the script reads and writes to frontmatter to account for the flat key format.
  3. Refactor Habit Update Logic: Ensure the habit tracking and duration updating logic works seamlessly with the flat key format.

Steps to Update the Script

1. Update getHabitStatus Function

Modify how habit statuses are retrieved from frontmatter.

Old:

const getHabitStatus = (entry, habitId) => {
  const habits = entry?.value('habits');
  return habits?.[habitId] ?? false;
};

New:

const getHabitStatus = (entry, habitId) => {
  const statusKey = \`habits-${habitId}\`;
  return entry?.value(statusKey) ?? false;
};

2. Update getHabitDuration Function

Refactor to fetch habit durations using flat keys.

Old:

const getHabitDuration = (entry, habitId) => {
  const habits = entry?.value('habits');
  return habits?.[\`${habitId}_duration\`] ?? null;
};

New:

const getHabitDuration = (entry, habitId) => {
  const durationKey = \`habits-${habitId}_duration\`;
  return entry?.value(durationKey) ?? null;
};

3. Update updateHabit Function

Change the logic to update flat keys in the frontmatter.

Old:

async function updateHabit(entry, habitId) {
  const file = app.vault.getAbstractFileByPath(entry.$path);
  await app.fileManager.processFrontMatter(file, (frontmatter) => {
    if (!frontmatter.habits) frontmatter.habits = {};
    const newStatus = !frontmatter.habits[habitId];
    frontmatter.habits[habitId] = newStatus;
    
    if (newStatus) {
      const habit = HABITS.find(h => h.id === habitId);
      frontmatter.habits[\`${habitId}_duration\`] = habit.defaultDuration;
    }
  });
}

New:

async function updateHabit(entry, habitId) {
  const file = app.vault.getAbstractFileByPath(entry.$path);
  await app.fileManager.processFrontMatter(file, (frontmatter) => {
    const statusKey = \`habits-${habitId}\`;
    const durationKey = \`habits-${habitId}_duration\`;
    
    const newStatus = !frontmatter[statusKey];
    frontmatter[statusKey] = newStatus;

    if (newStatus) {
      const habit = HABITS.find(h => h.id === habitId);
      frontmatter[durationKey] = habit.defaultDuration;
    }
  });
}
  • Here users need to be mindful of adding the two-space indentations carefully.

4. Update updateHabitDuration Function

Ensure durations are updated using the flat key format.

Old:

async function updateHabitDuration(entry, habitId, duration) {
  const file = app.vault.getAbstractFileByPath(entry.$path);
  await app.fileManager.processFrontMatter(file, (frontmatter) => {
    if (!frontmatter.habits) frontmatter.habits = {};
    frontmatter.habits[\`${habitId}_duration\`] = parseInt(duration) || 0;
  });
  setEditingTime(null);
}

New:

async function updateHabitDuration(entry, habitId, duration) {
  const file = app.vault.getAbstractFileByPath(entry.$path);
  await app.fileManager.processFrontMatter(file, (frontmatter) => {
    const durationKey = \`habits-${habitId}_duration\`;
    frontmatter[durationKey] = parseInt(duration) || 0;
  });
  setEditingTime(null);
}

5. Modify calculateCompletedHabits Function

Adjust the logic to count habits using the flat key format.

Old:

const calculateCompletedHabits = (entry) => {
  if (!entry) return 0;
  return HABITS.reduce((count, habit) => 
    count + (getHabitStatus(entry, habit.id) ? 1 : 0), 0);
};

New:

const calculateCompletedHabits = (entry) => {
  if (!entry) return 0;
  return HABITS.reduce((count, habit) => {
    const statusKey = \`habits-${habit.id}\`;
    return count + (entry?.value(statusKey) ? 1 : 0);
  }, 0);
};

Daily note setup

Which means users have to add the following to their daily notes:

habits-reading: false
habits-meditation: false
habits-workout: false
habits-writing: false
habits-run: false
habits-sleep: false
habits-reading_duration:
habits-meditation_duration:
habits-workout_duration:
habits-writing_duration:
habits-run_duration:
habits-sleep_duration:
  • Here, apart from weightlifting, I was using the original habit types but once the user sets their own habit types as per the guide, the logic changes made above will still stick. You just need to add your keys with the habits + dash (-) + habit type fashion. Duration key additions here are optional as these keys are automatically added by manipulating the output. I just added them so they are in proper order and don’t need care about linting the file later on.

Meta Bind plugin use

So now in the properties pane, users can have access to each and every property to update the values if they want to alter from default values.

If they want, they can add to the daily note Meta Bind buttons (again, I was using a more general workout prop key name, and I didn’t bother to align default values from the script so they are all zero here):

**Reading:** `INPUT[inlineSelect(option(true), option(false)):habits-reading]`  
**Reading Duration:** `INPUT[number(defaultValue(0)):habits-reading_duration]`  
**Writing:** `INPUT[inlineSelect(option(true), option(false)):habits-writing]`  
**Writing Duration:** `INPUT[number(defaultValue(0)):habits-writing_duration]`  
**Meditation:** `INPUT[inlineSelect(option(true), option(false)):habits-meditation]`  
**Meditation Duration:** `INPUT[number(defaultValue(0)):habits-meditation_duration]`  
**Workout:** `INPUT[inlineSelect(option(true), option(false)):habits-workout]`  
**Workout Duration:** `INPUT[number(defaultValue(0)):habits-workout_duration]`  
**Run:** `INPUT[inlineSelect(option(true), option(false)):habits-run]`  
**Run Duration:** `INPUT[number(defaultValue(0)):habits-run_duration]`  
**Sleep:** `INPUT[inlineSelect(option(true), option(false)):habits-sleep]`  
**Sleep Duration:** `INPUT[number(defaultValue(0)):habits-sleep_duration]`

Of course, these lines are quite too much in each and every daily note, so maybe it’s better to use the Properties pane on a sidebar to add whatever values one wants at the end of the day.
These buttons would make sense more if on daily note creation (in the morning?) one created a projection to what one wants to achieve that day.
Maybe someone can come up with a CSS to make these lines more compact (Obsidian columns plugin don’t play well with Meta Bind, unfortunately).


Edit:
The part with const StyledCard = ({ children, extraStyles = {} }) => ( also needed some tweaking as backgroundColor: 'var(--background-primary)' might not be a good fit for all themes.

Wow! This looks incredible! And near exactly what I’ve been looking for for a habit tracker. Sadly, it seems that the repo for the sample vault in that blog post seems to go to a 404 :frowning:

Seems to me OP didn’t take my post well (no soup for me) and removed their content.

I managed to find the zip I had downloaded:
Habits Demo Vault.zip (325.3 KB)

3 Likes

Awww shucks :frowning: Well, thank you so much for reuploading it! :slight_smile:

Legend :saluting_face: