This note outlines my personal Pomodoro workflow, adapted from a setup I found. It uses DataviewJS queries within Obsidian to parse a log file and generate useful statistics.

The log data is stored in pomodoro-log.

Logging Template

I use the following template to log each Pomodoro session. This is designed for the Obsidian Pomodoro Plugin or a similar tool that can output structured data.

- πŸ… (pomodoro:: ${log.mode}) (duration:: ${log.duration}m) (session:: ${log.session}m) (finished:: ${log.finished}) (begin:: ${log.begin.format()}) - (end:: ${log.end.format()}) (note:: ${log.task?.name ?? ''})

DataviewJS Queries

These queries are for use with the Dataview plugin in Obsidian. They will not render on the public Quartz website but are documented here for reference.

Detailed Session Log

This query lists all individual Pomodoro sessions from the log file, sorted by completion time.

const pages = dv.pages('"Personal/pomodoro-log"');
const table = dv.markdownTable(
    ['Pomodoro', 'Duration', 'Begin', 'End', 'Finished', 'Focused'],
    pages.file.lists
        .filter(item => item.pomodoro)
        .sort(item => item.end, 'desc')
        .map(item => [
            item.pomodoro,
            `${item.duration.as("minutes")} m`,
            item.begin,
            item.end,
            item.finished,
            item.note
        ])
);
 
dv.paragraph(table);

Daily Summary

This query groups all β€œWORK” pomodoros by date and provides a daily summary of the total number of sessions and focused time.

const pages = dv.pages('"Personal/pomodoro-log"');
const emoji = "πŸ…";
dv.table(
    ["Date", "Pomodoros", "Total"],
    pages.file.lists
        .filter((item) => item?.pomodoro == "WORK")
        .groupBy((item) => {
            if (true) {
                let dateObject = new Date(item.end);
                return dateObject.toISOString().substring(0, 10);
            } else {
                return "Unknown Date";
            }
        })
        .map((group) => {
            let sum = 0;
            group.rows.forEach((row) => (sum += row.duration.as("minutes")));
            return [
                group.key,
                group.rows.length > 5
                    ? `${emoji} ${group.rows.length}`
                    : `${emoji.repeat(group.rows.length)}`,
                `${sum} min`,
            ];
        })
)

Overall Statistics

Here are queries to see the bigger picture.

Total Pomodoros

This query calculates the total number of pomodoros and the total focused time.

const pages = dv.pages('"Personal/pomodoro-log"');
const workItems = pages.file.lists.filter(item => item?.pomodoro === "WORK");
const totalPomodoros = workItems.length;
let totalMinutes = 0;
workItems.forEach(item => totalMinutes += item.duration.as("minutes"));
const totalHours = Math.floor(totalMinutes / 60);
const remainingMinutes = totalMinutes % 60;
 
dv.paragraph(`You have completed a total of **${totalPomodoros}** pomodoros, for a total of **${totalHours} hours and ${remainingMinutes} minutes** of focused work.`);

Weekly Summary

This query groups pomodoros by week.

const pages = dv.pages('"Personal/pomodoro-log"');
const emoji = "πŸ…";
const dateRegex = /\(end::\s*(.*?)\)/;
 
dv.table(
    ["Week", "Pomodoros", "Total"],
    pages.file.lists
        .filter((item) => item?.pomodoro == "WORK" && item.text.includes("(end::"))
        .groupBy((item) => {
            const match = item.text.match(dateRegex);
            if (match && match[1]) {
                // dv.date() is strict and requires a 'T' separator for ISO 8601 dates.
                const dateString = match[1].replace(' ', 'T');
                const endDate = dv.date(dateString);
                if (endDate) {
                    return endDate.toFormat("kkkk-'W'WW");
                }
            }
            return "Invalid Date";
        })
        .sort(group => group.key, 'desc')
        .map((group) => {
            if (group.key === "Invalid Date") return [group.key, group.rows.length, "N/A"];
 
            let sum = 0;
            group.rows.forEach((row) => (sum += row.duration.as("minutes")));
 
            const firstItemMatch = group.rows[0].text.match(dateRegex);
            const firstItemDateString = firstItemMatch[1].replace(' ', 'T');
            const firstItemDate = dv.date(firstItemDateString);
 
            const startOfWeek = firstItemDate.startOf('week');
            const endOfWeek = firstItemDate.endOf('week');
 
            const formatDate = (dt) => {
                const month = dt.toFormat('MMM');
                const day = dt.day;
                let suffix = 'th';
                if (day % 10 === 1 && day % 100 !== 11) {
                    suffix = 'st';
                } else if (day % 10 === 2 && day % 100 !== 12) {
                    suffix = 'nd';
                } else if (day % 10 === 3 && day % 100 !== 13) {
                    suffix = 'rd';
                }
                return `${month} ${day}${suffix}`;
            }
 
            const weekLabel = `${group.key} (${formatDate(startOfWeek)} - ${formatDate(endOfWeek)})`;
 
            return [
                weekLabel,
                group.rows.length > 5
                    ? `${emoji} ${group.rows.length}`
                    : `${emoji.repeat(group.rows.length)}`,
                `${sum} min`,
            ];
        })
)

Monthly Summary

This query groups pomodoros by month.

const pages = dv.pages('"Personal/pomodoro-log"');
const emoji = "πŸ…";
dv.table(
    ["Month", "Pomodoros", "Total"],
    pages.file.lists
        .filter((item) => item?.pomodoro == "WORK")
        .groupBy((item) => {
            let dateObject = new Date(item.end);
            return dateObject.toISOString().substring(0, 7); // YYYY-MM
        })
        .sort(group => group.key, 'desc')
        .map((group) => {
            let sum = 0;
            group.rows.forEach((row) => (sum += row.duration.as("minutes")));
            return [
                group.key,
                group.rows.length > 5
                    ? `${emoji} ${group.rows.length}`
                    : `${emoji.repeat(group.rows.length)}`,
                `${sum} min`,
            ];
        })
)

Yearly Summary

This query groups pomodoros by year.

const pages = dv.pages('"Personal/pomodoro-log"');
const emoji = "πŸ…";
dv.table(
    ["Year", "Pomodoros", "Total"],
    pages.file.lists
        .filter((item) => item?.pomodoro == "WORK")
        .groupBy((item) => {
            let dateObject = new Date(item.end);
            return dateObject.getFullYear();
        })
        .sort(group => group.key, 'desc')
        .map((group) => {
            let sum = 0;
            group.rows.forEach((row) => (sum += row.duration.as("minutes")));
            return [
                group.key,
                group.rows.length > 5
                    ? `${emoji} ${group.rows.length}`
                    : `${emoji.repeat(group.rows.length)}`,
                `${sum} min`,
            ];
        })
)