Obsidian Habit Tracking
Table of Contents
The Goal
For New Year’s resolutions, I’ve found that I can “get back on the wagon” better if my goals aren’t daily. Some days, there are more obligations, so I don’t need to make those hurdles for other goals. With that I create weekly goals that need progress aggregated from multiples days. For example, I task myself with exercising 150 hours a week. I also need to track the number of occurrences of an event (publishing blog posts that aren’t drafts) over a week. Finally, the good old weight watching doesn’t need to be grouped weekly but simply viewed over time.
Pulling the Data
For simple tracking, I started with putting properties in my daily notes’ frontmatter. They’re prefixed with habit.
in the hopes of preventing future collisions, but it doesn’t affect the query. For tracking blog entries, I decided to track each blog entry’s file with it’s frontmatter date. That would require no extra information than what I needed for my publishing workflow.
Habits Query
Obsidian’s DataView query language may look like standard SQL, but it has some deviations. Aggregate values (i.e., those calculated from grouping) require to use the Non-SQL Flatten keyword after the grouping. For each FLATTEN, you can do an aggregate function (sum, count, etc) and declare a value like you would in the SELECT of a regular SQL query. Those values can then be used in the main projection part of the query to make further calculations. In this case, I generate the html for the progress bar with the value set.
The final piece was how to group by week of the year. Using YY-WW
as the format string yielded an entry for “24-01” which was a year ago. After looking at Luxon’s documentation, one sees that kk
is the 2 digit “week year” or the year corresponding to WW
, the 2 digit week of year.
TABLE WITHOUT ID
Week,
"<progress value='" + ExerciseCalc + "' max='150' />" as "Exercise (150)",
"<progress value='" + ReadingCalc + "' max='150' />" as "Reading (150)",
"<progress value='" + MusicCalc + "' max='150' />" as "Music (150)",
"<progress value='" + OldNorseCalc + "' max='105' />" as "Old Norse (105)"
FROM "06 - Daily"
GROUP BY dateformat(date(file.name, "yyyy-MM-dd"), "kk-WW") as Week
FLATTEN sum(rows.file.frontmatter["habit.exercise"]) as ExerciseCalc
FLATTEN sum(rows.file.frontmatter["habit.reading"]) as ReadingCalc
FLATTEN sum(rows.file.frontmatter["habit.music"]) as MusicCalc
FLATTEN sum(rows.file.frontmatter["habit.old-norse"]) as OldNorseCalc
SORT Week ASC
Blog Query
With all the information from the first query, it was simple to iterate through all my blog posts and count the number for each week that were not marked as drafts.
TABLE WITHOUT ID
Week,
"<progress value='" + BlogCalc + "' max='3' />" as "Blogs (3)"
FROM "04 - Permanent/Blog/posts"
GROUP BY dateformat(date(file.frontmatter["date"]), "kk-WW") as Week
FLATTEN sum(choice(rows.file.frontmatter.draft = "true", 0, 1)) as BlogCalc
SORT Week ASC
Weight Line Graph
Line graphs require an additional Obsidian plugin that brings the Chart.js library into Obsidian. After a few examples, some back and forth with ChatGPT, and fighting to match the data structure in the Chart.js documentation (Give me some static times, please!), I finally had a graph from the value in my daily notes.
// Note: changes to the plugin code is not reflected to the chart, because the plugin is loaded at chart construction time and editor changes only trigger an chart.update().
const plugin = {
id: 'customCanvasBackgroundColor',
beforeDraw: (chart, args, options) => {
const {ctx} = chart;
ctx.save();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = options.color || '#ffffff';
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
};
// Get all pages in the "06 - Daily" folder with the "habit.weight" field in frontmatter
const pages = dv.pages('"06 - Daily"') .where(p => p.file.frontmatter["habit.weight"] && p.file.frontmatter["habit.weight"] !== 0) .sort(p => p.file.name).array();
// Sort the pages by filename or date
// Prepare data for Chart.js
let labels = pages.map(p => p.file.name);
// Use file names as the labels
let weights = pages.map(p => p.file.frontmatter["habit.weight"]);
// Extract the habit.weight field
// Chart.js data format
let data = {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Weight',
data: weights,
backgroundColor: ['#000000']
}]
},
options: {
plugins: {
customCanvasBackgroundColor: {
color: 'white',
}
}
},
plugins: [plugin],
};
window.renderChart(data, this.container);
Combing all those query together gave me this page, which had some display issues. Data on dashboards should be seen at a glance and not having to scroll.
!
Fixing the Width To See Everything
Following this post, I added some css classes to Obsidian. When I added the page-100
value in the cssclasses frontmatter field, the width of the document goes to the full width of the tab. 4 progress bars now have almost too much room on an HD monitor.
!
Wrap Up
Hopefully, this will be an easy system to input my progress and see where I stand. If I create the daily note in the morning, I’m able to make a commit in GitHub to update the frontmatter through out the day on my phone. Maybe my eyes are bigger than my stomach on this New Year’s resolution plate, but at least I can see and remember what I’m eating.