Obsidian 12WY Review Template

It's the 13th week of the year… I'm supposed to use this time to consider my goals, review my intentions, and plan the next twelve weeks.

Instead, I've been staying up till midnight automating the Quarterly template. 🤷

I've been using this template for quite some time… but each quarter required some setup. I had to choose a color, set the start week, and (if required) update the year everywhere it appeared in the code. No more!

Here's the whole thing in a gist, but let's walk through it!

Let's make sure you have all the dependency plugins:

Periodic Notes is configured to enable quarterly notes and weekly notes. The paths for those notes are /journal/quarterly and journal/weekly. You could put them anywhere, but you'd have to rewrite some of the following dataviewjs.

Now let's look at the "bones" of our template in markdown, named {year}-Q{number}.

---
# rate your intention from 1 to 5 in the following roles…
roles:
- Follower:
- Husband:
- Father:
- Friends:
- Work:
- Hobbies:
- Emotions:
- Mental:
- Health:
- Rest:
- Finances:
---

# {{date:YYYY}}-Q{{date:Q}}

## Plan

### Goals

#### Goal 1: _Goal_

#### Goal 2: _Goal_

## Review

### How do you rate your intentions in each area of life?
( dataview visualization of intentions over quarters )

### Scoreboard
( dataview visualization of accomplishing 12WY goals in Weekly notes )

You can remove and add roles from the YAML frontmatter if you want, they are automatically pulled into the visualizations.

Let's look at that first visualization, in a dataviewjs block:

All s/1 does is convert the string representing a quarter into a number: s("2023-Q1") = 20231 which can be easily sorted1… by the next function. Unfortunately when I first wrote the compare function, I used < instead of - and got very unpredictable results. The compare for a sort is supposed to return an integer, not a boolean.

// we have to sort somehow, so we convert the strings to numbers and
// sort the numbers
const s = function(i) {
  return Number(i.replace('-Q', ''));
}

const allQuarters =
  // get all files in "journal/quarterly"
  dv.pages('"journal/quarterly"')
    .array()
    // ensure they are in sequential order, not last-modified
    .sort(function(a, b) {
      return s(a.file.name) - s(b.file.name);
    })

// find the current file in the list
let currentIndex = allQuarters.findIndex((q) => q.file.name == dv.current().file.name);

// slice the list of all quarters into the current and previous three.
const quarters = allQuarters.slice(currentIndex - 3, currentIndex+1);

Now that we have our data sources, it's time to build the actual visualization!

let datasets = [];

// make an rgba color transparent
let trans = function(color) {
  return color.replace(', 1)', ', 0.5)');
}

// declaring some monochrome grays
let colors = [
  'rgba(105,105,105, 1)',
  'rgba(169,169,169, 1)',
  'rgba(211,211,211, 1)',
]

// this purple is the highlight color from the default Obsidian theme.
const purple = 'rgba(139,108,239, 1)';
let highlight = '#fff';

// if we are in light mode, invert the color scheme.
if (document.body.classList.contains('theme-light')) {
  colors.reverse();
  highlight = '#000';
}

colors.push(purple);

for (let i in quarters) {
  let quarter = quarters[i];
  if (quarter.hasOwnProperty('roles')) {
    // this line gets an array of all the roles you specified in this quarter's YAML.
    let values = quarter.roles.map(o => Object.values(o)).flat()

    datasets.unshift({
      label: quarter.file.name,
      data: values,
      fill: true,
      backgroundColor: trans(colors[i]),
      borderColor: colors[i],
      pointBackgroundColor: colors[i],
      pointBorderColor: highlight,
      pointHoverBackgroundColor: highlight,
      pointHoverBorderColor: colors[i]
    })
  }
}

let data   = dv.current().roles
let keys   = data.map(o => Object.keys(o)).flat()

const chartData = {
  type: 'radar',
  data: {
    labels: keys,
    datasets: datasets
  },
  options: {
    scales: {
      r: {
        suggestedMin: 0,
        suggestedMax: 5
      }
    }
  }
}

window.renderChart(chartData, this.container);

Cool!

light mode

The second visualization is a simpler table with more gross JS that shouldn't be stared at too long.

I believe the math computing the quarters holds up… but it hurts my head nonetheless.

// quick function to display the results as emoji
let check = function(bool) {
  if (bool) { return "" };
  return "";
}

let quarter = parseInt(dv.current().file.name.match(/Q\d/)[0].replace('Q', ''));
let year = dv.current().file.name.match(/\d{4}/)[0];
// I'm _pretty_ sure this math works out correctly.
// I work on my goals for 12 weeks, plan/rest for one, repeat.
let start = (quarter-1)*13+1

dv.table(
  ["", "Week", "Goal 1", "Goal 2"], dv.pages('"journal/weekly"')
    .filter(function(f) {
      let num = f.file.name.match(new RegExp(`${year}-W(\\d+)`));

      if (!num) { return false; }
      num = parseInt(num[1]);
      return num >= start && num < start + 12;
    })
    .sort(w => w.file.name, 'asc')
    .map(w => [(parseInt(w.file.name.match(new RegExp(`${year}-W(\\d+)`))[1]) - start)+1, w.file.link, check(w["Goal1"]), check(w["Goal2"])])
)

The resulting visualization looks like this!

I've been a good boy

This is nearly identical to the old version. The main change is using new RegExp instead of the Regex literals to build the regex. By switching to the constructor I can use a literal string to interpolate in the current year so that I don't have to update my template every year.

I hope this inspires you to consider some visual accountability for your next quarter's journaling and planning. As for me, now that I've written this post I can get back to planning my next 12WY.


  1. To explain this, let me show you a wat

     node
    Welcome to Node.js v19.6.0.
    Type ".help" for more information.
    > quarters = ['2022-Q4', '2023-Q2', '2022-Q3', '2023-Q1']
    [ '2022-Q4', '2023-Q2', '2022-Q3', '2023-Q1' ]
    > quarters.sort((a, b) => a > b)
    [ '2022-Q4', '2023-Q2', '2022-Q3', '2023-Q1' ]
    > # wat
    

Changelog
  • 2023-03-31 13:24:26 -0500
    Fix sort

    I've been having all kinds of bugs, turns out the compare function
    passed to `sort()` expects an integer as a return, not a boolean.

  • 2023-03-29 19:01:36 -0500
    Include details about periodic notes configuration

  • 2023-03-28 22:12:28 -0500
    remove typo

  • 2023-03-28 19:34:54 -0500
    Fix typo

    Thank you Mykel.

  • 2023-03-28 18:01:15 -0500
    Publish

  • 2023-03-28 18:00:51 -0500
    Add images and proof

  • 2023-03-28 16:21:59 -0500
    Check in draft of post