[dataview] Progress bar for tasks in current page

Hello, I took inspiration from this forum post to create my own progress bar for the tasks in the current note.

Here’s what it looks like

And here’s the code:

function renderProgressBar() {
    const container = dv.el("div", "", { attr: { style: `
            width: 100%;
            margin: 0 auto;
            padding: 1rem;
            display: flex;
            flex-direction: column;
            gap: 16px;
            align-items: center;
            justify-content: center;
            align: center; 
            background: rgba(255, 255, 255, 0.25);
            border-radius: 4px;
        ` 
    }});

	const progressText = dv.el("text")
	
    const progressBar = dv.el("progress", "", { attr : { max: "100", value: "0",
        style: `
            width: 95%;
        `
    }});

    container.appendChild(progressText);
    container.appendChild(progressBar);
    
    return { container, progressText, progressBar };
}


function calculateProgress() {
	const tasks = (dv.current()?.file?.lists || []).filter(({task}) => task);
    return Math.round([...tasks].reduce((acc, { checked }) => acc + 
	    Number(checked), 0) / tasks.length * 100 || 0);
}

function updateProgressBar({ container, progressText, progressBar }) {
		const progress = calculateProgress();
		if(typeof progress === 'number') {
		    progressBar.value = progress;
		    progressText.innerHTML = `${progress}%`;
		}
}
	
const progressBar = renderProgressBar();
updateProgressBar(progressBar);

setInterval(updateProgressBar, 500, progressBar);
2 Likes

Interesting approach, and I kind of like it as you just change a little part in the after works. However, I’m also slightly concerned on the performance impact of this variant using setInterval().

It’ll trigger a change every half second, and it seems like it’ll continue to repeat that until you close your Obsidian. And if you re-open the note, it’ll start over repeating it yet another time.

And if you do this in multiple daily notes, you’ll have multiple repeated calls which really isn’t good… I executed a test in my vault, and I had to do a “Force reload” to get rid of them.

Why don’t you just use the dataview refresh on change ability? By default dataview should rerun any query on the page when you tick/untick a task. So a minimal example could look like:

`$= const tasks = dv.current().file.tasks; dv.el("progress", "", { attr:  { max: tasks.length, value: tasks.filter(t => t.completed).length}}) `

In the seldom occasion when it doesn’t update, you could either reopen the note, trigger a insignificant change in the query, or trigger a rebuild of the current view.

1 Like

Hi, thanks for the concern. I am unsure what you are talking about in regards to “dataview refresh on change ability”.

In my testing and reading I was unable to find anything of the like. the only refresh is when changing notes, which I find cumbersome. Unfortunately I can’t seem to find any elegant solution for this, other than adding a button that will recalculate it all but I would have to manually click it. I was under the impression that after the note is closed, the interval would also time out, but it seems each interval continues to run. Maybe there is a way to detect if the note is open, somehow

I realised I have access to localStorage and so I wrote this improvement for optimisation. I did not realise the massive memory leak before :sweat_smile:

const progressBar = renderProgressBar();
updateProgressBar(progressBar);

function loadIntervalIds() {
    const intervalIdsString = localStorage.getItem("intervalIds");
    try {
	    return JSON.parse(intervalIdsString);
    } catch {
	    return {}
    }
}

function saveIntervalId(noteName, intervalId) {
    const intervalIds = loadIntervalIds();
    intervalIds[noteName] = intervalId;
    localStorage.setItem("intervalIds", JSON.stringify(intervalIds));
}

function clearIntervalsAndStartNewOne(noteName) {
    const intervalIds = loadIntervalIds();
    Object.keys(intervalIds).forEach(key => {
		clearInterval(intervalIds[key]);
		delete intervalIds[key];
    });

	const intervalId = setInterval(updateProgressBar, 500, progressBar);
	saveIntervalId(noteName, intervalId);
}

const noteName = `daily-intervals/${dv.current().file.name}`;
clearIntervalsAndStartNewOne(noteName);

You’ve still not answered why the normal update when changing the file (like ticking a task) isn’t sufficient. Have you often experienced the caching issue? For most it’ll not apply in a case like yours…

Well like I said in my previous comment, the only refresh is when changing notes. They do not automatically update when ticking tasks. I would prefer to see the progress bar fill up as I am ticking the boxes, without having to switch notes :slight_smile:

What do you need to get this solution up and running? Did you set the localStorage["intervalIds"] to something specific?

OK, so I wasn’t able to get your localStorage version up and running, but by looking at the code it seems like it’ll run the all startedupdateProgressBar() until the next time you open it, where it’ll shut down the previous one and start a new one.

I think we can do better than that, by looking into which files are currently open. The following is based of the first variant, so it’ll run multiple interval simultaneously related to refreshing the query run and so on, but when you close down the window it’ll stop them all. What I did to achieve this was add the following at the end of your first version:

const progressBar = renderProgressBar();
updateProgressBar(progressBar);
myId = setInterval(updateProgressBar, 2000, progressBar, dv.current().file.name);



function updateProgressBar({ container, progressText, progressBar}, currFileName) {
  // Code to close the function down when window is closed
  console.log("whenOpen: ", myId, currFileName)
  let isOpenFile = false
  for (const leaf of app.workspace.getLeavesOfType('markdown')) {
    if (currFileName == leaf.view.file.basename) {
      isOpenFile = true
    }
  }
  if (!isOpenFile) {
    console.log("window has shut down")
    clearInterval(myId)
  }

  // Your original code
  const progress = calculateProgress();
  if(typeof progress === 'number') {
    progressBar.value = progress;
    progressText.innerHTML = `${progress}%`;
  }
}

I believe that if this was combined with something from your localStorage version, then we’d finally be getting a handle on not letting these interval methods eat up precious system resources. Until then, I’d be weary to use stuff like this as they tend to run their own lives…


That sounds strange, and I totally didn’t read that in your earlier post. Sorry about that. It’s very understandable you want something like this going, but it should really be handled by dataview itself, as each time you tick anything it causes a change to the file which should cause the query to rerun, and thusly update itself without the need for doing setInterval().

In my vault, this do happen but I’ve Settings > Dataview > Automatic View Refreshing enabled, with a Refresh Interval of 2500 ms, which occasionally causes my query to run twice. But they’re are executed (mostly) when a file changes, so I’ve not needed to do the trickery which we’ve been talking about.

Nice, I thought of a similar solution but couldn’t find the way to implement it as I didn’t know how to get the currently open note. Thanks for providing this example :smiley:

I found the issue of why it wasn’t working for you and why it was working for me:

function loadIntervalIds() {
    const intervalIdsString = localStorage.getItem("intervalIds");
    try {
	    return JSON.parse(intervalIdsString) || {};
    } catch {
	    return {}
    }
}

I already had intervalIds “correctly” set in localStorage from testing and writing this code, but I found that on my laptop it didn’t work. Strangely enough, it did on my phone.

This should fix the loading of the object from localStorage