A Graph for Zettelkasten: Measuring Growth in Obsidian

A picture of a flower growing from bud to full bloom.

In the last year, I created 1,500+ notes in my Zettelkasten.

That’s an average of four notes per day, every day of the year.

And these aren’t small meaningless notes. Sometimes a note will contain a single link, but I also have notes that contain thousands of words.

Perhaps even more surprisingly, on most days I spend no more than ten minutes creating or maintaining notes.

How do I do it? It’s all about momentum and motivation.


When I started building my Zettelkasten, I was ecstatic about it.

I write for at least a few minutes every day, and Zettelkasten makes it effortless in a way I have never known before. Taking notes is not only fun, it is a joy.

But I knew that this feeling wouldn’t last. All humans are subject to chasing rainbows. We too often prefer “new and novel” over “old and reliable”, and I feared that this too would become a hobby that I would someday drop and forget.

So I planned for that. I wanted to make sure I kept my momentum up long enough to turn Zettelkasten into a habit rather than a hobby. I have a far better track record of keeping up with habits than hobbies.

To do that, I built a system to keep myself accountable. My system “keeps an eye on me”, and ensures that I am creating new notes every day, and that my Zettelkasten is always growing.

The system I came up with is simple, but effective.

This solution showcases the growth of my Zettelkasten over time, and makes it clear whether I’m growing or falling behind.

I embed this document inside my home note, so I see it every day, and it reminds me not only of how far I’ve come, but also that I can always push a little further. That document looks like this:

A screenshot of my Recently Created view, including a table and two graphs displaying different data about my Zettelkasten.

How to Track Your Own Notes

I named the above document “Recently Created”, so I can always find it from the Quick Switcher. If you want to create your own Recently Created document, then keep reading!

The graphs above are created using my data, which means if you implement this in your vault, your graphs will look different.

Requirements

In order to create graphs in your own vault, there are a couple of community plugins that you need to install. If you don’t already have these plugins installed, search for and install them before moving forward (click the links to open in Obsidian):

Then check your Dataview settings and make sure that “Enable JavaScript Queries” is ON. We’ll need that soon.

There’s one more plugin that I personally use for consistency sake, and that is the Update Time on Edit plugin. This plugin adds and keeps up-to-date two pieces of metadata inside your vault: the created and updated fields. If you use this plugin, you won’t have to worry about adding metadata to your templates.

If you don’t want to use this plugin, you can always create new Zettels from a template, and use this snippet inside your template:

created: {{date:YYYY-MM-DD}}T{{time:HH:mm}}

This doesn’t give you an updated time, but it does at least give you that created time, which is all you need for this script. I did this for a long time, before I learned about the Update Time on Edit plugin.

Note: If you haven’t used community plugins before, you might want to read How to use Community Plugins and a Beginner’s Guide to Dataview.

Creating a Recently Created view

Once you have the above plugins installed, create a note named “Recently Created” (or something similar). To that note, add this code:

---
location: ''
daysback: 2
chartlength: 50
---
```dataviewjs
const start = moment();
const daysback = parseInt(dv.current().daysback) || 2;
const location = dv.current().location || '';
const dateField = 'created';
const chartColor = '#4e9a06';
const chartlength = parseInt(dv.current().chartlength) || 50;
const docs = dv.pages(location);

function getRecentDocs(numDays) {
    return docs
        .where(f => {
            if (!f[dateField] || !f[dateField].toISO) return false;
            var startDate = moment(start);
            var pastDate = startDate.subtract(numDays, 'days');
            var docDate = moment(f[dateField].toISO());
            return docDate.isAfter(pastDate) && docDate.isBefore(start);
        });
}

// creating the table
var ztDocs = getRecentDocs(daysback);

    dv.table(['link', 'date'], ztDocs
        .sort(a => a[dateField], 'desc')
        .map(b => [b.file.link, moment(b[dateField].toISO()).format('YYYY-MM-DD hh:mm')]));

// creating the charts
var ztLastWeek = getRecentDocs(chartlength).sort(f => f[dateField]);
var daysData = [];
var totalcount = 0;

// formatting the data
for (var i=0; i<=ztLastWeek.length; i++) {
    var f = ztLastWeek[i];
    if (f && f[dateField] && f[dateField].toISO) {
        var itemDate = moment(f[dateField].toISO());
        var newDate = itemDate.format('MM-DD');
        var index = daysData.findIndex(d => d.label === newDate);
        if (index !== -1) {
            daysData[index].num += 1;
        } else {
            daysData.push({label: newDate, num: 1});
        }
        totalcount += 1;
    }
};

var labels = [],
    data = [],
    aggData = [];

daysData.map(el => {
    labels.push(el.label);
    data.push(el.num);

    if (aggData.length) {
        var lastNum = aggData[aggData.length - 1];
        aggData.push(el.num + lastNum);
    } else {
        aggData.push(el.num);
    }
});

const lineChart = {
    type: 'line',
    data: {
    labels: labels,
    datasets: [{
        label: 'Docs created',
        data: data,
        backgroundColor: [
            chartColor
        ],
        borderColor: [
            chartColor
        ],
        borderWidth: 1
    }]
   }
}

const aggregateChart = {
    type: 'line',
    data: {
        labels: labels,
        datasets: [{
            label: 'Aggregate Docs Created',
            data: aggData,
            backgroundColor: [
                chartColor
            ],
            borderColor: [
                chartColor
            ],
            borderWidth: 1
        }]
    }
}

window.renderChart(lineChart, this.container);
window.renderChart(aggregateChart, this.container);

dv.paragraph('Total: ' + totalcount);
```

A few notes about this code:

  • You can control the filter with the location property. If your Zettelkasten is inside a folder, for instance, you can add location: "Folder/to/my/Zettelkasten". This will make this note load a little faster if you have a lot of notes.
  • The table at the top of the note is controlled by the daysback property. By default the table pulls notes from two days in the past, but you can turn that number up as much as you like.
  • The chartlength property controls the length of the graphs. By default the graphs show the last 50 days of notes created, but you can turn that up or down as much as you like.
  • If this code doesn’t return any results for you, it might be because the script looks for the created metadata in your notes. Make sure you’re tracking the correct property. If you use a different verb for your metadata, you can adjust the datefield variable within the code.

If all goes well, you should see a table and the accompanying graphs, except with your own notes!

Conclusion

Zettelkasten as a practice is challenging, but building good systems makes it easier. We hope this tool will be helpful in your own practice of Zettelkasten.

If you have any troubles with the above script, or you want any more information, feel free to contact us or drop a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *