Habit tracking in Logseq
I had to move away from Obsidian to Logseq because I learnt I couldn't keep work notes on it without paying an annual subscription fee.(source) Obsidian was the first tool I actually stuck with for a while - so it was a bit of a bummer. But Logseq seems perfectly reasonable as a replacement. While the UX still lacks a little polish, it is just as powerful and has better outlining support.
Goal: Old obsidian setup
I used to add YAML metadata to my daily journals using templates, MetaEdit and Dataview. My dataview query was as follows
TABLE
file.name AS "File",
choice(wake_up_early, "✔", "❌") AS "Wake up Early",
choice(plan_day, "✔", "❌") as "Plan Day",
choice(exercise, "✔", "❌") as "Exercise",
choice(productive, "✔", "❌") as "Productive Day"
FROM "Journal"
WHERE file.name != "Tracker"
SORT file.name DESC
Moving to Logseq
Step 1: Create a property list in your daily journal page
- ## Trackers
- type:: tracker
wake-up-early:: true
exercise:: false
day-plan:: true
productive:: true
Do this for journal pages. If you please you might also add it to a journal template.(learn how)
Step 2: Create the tracker query
Create a page called Tracker, and type < query
into it. It should give you a drop-down with the word query that you can select to create an advanced query. Between the auto-inserted #+BEGIN_QUERY
and #+END_QUERY
, paste the following
{
:title [:b "HabitTracker"]
:query [
:find
(pull ?b [:db/id :block/properties {:block/page [:block/original-name]} :block/content])
:where
[?b :block/page ?p]
[?p :page/journal? true]
[?b :block/properties ?props]
[(get ?props :type) ?type]
[(= "tracker" ?type)]
]
:result-transform
(fn [rows]
(map
(fn [row]
(->
(assoc row :block/content (get-in row [:block/page :block/original-name]))
(update-in [:block/properties :exercise] (fn [b] (if b "✔️" "❌")))
(update-in [:block/properties :wake-up-early] (fn [b] (if b "✔️" "❌")))
(update-in [:block/properties :day-plan] (fn [b] (if b "✔️" "❌")))
(update-in [:block/properties :productive] (fn [b] (if b "✔️" "❌")))))
rows))}
Let us break this query down into small parts.
:title [:b "Habit Tracker"]
simply sets the title for the result table:query
is the list of queries that blocks are filtered through. The syntax used is datalog(learn). I use only two query features:pull
which selects which records of a block to pull in for further filtering, and:where
which lets you constrain and filter through the global blocks list.:from (pull ?b [:db/id :block/properties {:block/page [:block/original-name]}])
.?b
gets bound to blocks in the database. You can think of it as a variable.:db/id
- This is a unique identifier of the block. You always need to pull this in:block/properties
- this is the map we will primarily be filtering with{:block/page [:block/original-name]}
- I pull in the:block/page
property for filtering out non-journal pages. Furthermore the nested:block/original-name
property will come in handy in the query results
:where
. Now let us walk through the constraints[?b :block/page ?p] [?p :page/journal? true]
: Together, these let you filter out non-journal pages (for instance templates that may contain the same properties)[?b :block/properties ?props]
: Bind?props
to the list of block properties- [(get ?props :type) ?type] [(= "tracker" ?type)]
: Find a property called
typeand only retain records that have
:type tracker`
:result-transform
- Now, this is the trickiest part of the puzzle. Primarily because it is poorly documented. Almost all examples online only show you how to sort with it - but I wanted to transformtrue
andfalse
to check and cross emojis and also change the displayed block contents.(fn [rows] ...)
We are expected to provide a function that consumes a seq of rows and transform it into a whatever seq of rows we would like to render.(map (fn [row] ...) rows)
I would like to apply the transform row-by-row. and so I map the transform on to each row in rows(-> ...)
the helps you thread through transformation functions. the result of each function is passed as the first arg to the next.(assoc row :block/content (get-in row [:block/page :block/original-name]))
: Replace :block/content (which is displayed as the first column by default) by a the journal page name(update-in [:block/properties :FOO] (fn [b] (if b "✔️" "❌")))
: Replace true and false by ✔️ and ❌ respectively in the table
This would give you an table that looks like .