DataviewJS Snippet Showcase

What can I say? I have inserted the code into my table code. And: it works just like that! This is a really great thing, finally I have an overview of my very long tables. Thank you!

I created a small snippet to filter tasks on a page based on inclusion/exclusion of multiple tags. It’s pretty generic, so I thought I might share it. :slight_smile:

%%
include:: tag1
exclude:: tag2
onlyUncompleted:: false
%%

```dataviewjs
const source = dv.current();
const include = source.include;
const exclude = source.exclude;
const onlyUncompleted = source.onlyUncompleted;
let tasks = source.file.tasks;

// include
if (include != undefined) {
    let includeTags = include.replace(/[,#]/g,"").split(" ");
    let includedTasks = [];
    includeTags.forEach (tag => {
        let filteredTasks = tasks.filter (t => t.text.includes ("#" + tag));
        includedTasks.push (...filteredTasks);
    });
    tasks = includedTasks;
}

// exclude
if (exclude != undefined) {
    let excludeTags = exclude.replace(/[,#]/g,"").split(" ");
    excludeTags.forEach (tag => {
        tasks = tasks.filter (t => !t.text.includes ("#" + tag));
    });
}

//completed
if (onlyUncompleted) tasks = tasks.filter (t => !t.completed)

dv.taskList(tasks, false);
```
6 Likes

Still trying to get the hang of DataView and DataViewJS is over my head. However, I am struggling to create a simple query of “Incomplete Tasks in the current note”

I want to be able to identify all the tasks in one place that exist in the note I have open - no hardcoded values - just this note.

If this give me all incomplete tasks in all notes:

dv.taskList(dv.pages().file.tasks.where(t => !t.completed))

How do I get only the incomplete tasks in the current note?

1 Like

you can access the current file with dv.current() so just replace dv.pages() with dv.current().

1 Like

Thank you. However, with:

\```dataviewjs
dv.taskList(dv.current().file.tasks.where(t => !t.completed));
```\

I get an error:

Evaluation Error: TypeError: dv.current(...).file.tasks.where is not a function
    at eval (eval at <anonymous> (eval at <anonymous> (app://obsidian.md/app.js:1:1215379)), <anonymous>:1:75)
    at DataviewInlineApi.eval (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:12599:16)
    at evalInContext (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:12600:7)
    at eval (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:12611:36)
    at Generator.next (<anonymous>)
    at eval (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:26:71)
    at new Promise (<anonymous>)
    at __awaiter (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:22:12)
    at asyncEvalInContext (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:12606:12)
    at DataviewJSRenderer.eval (eval at <anonymous> (app://obsidian.md/app.js:1:1215379), <anonymous>:13133:23)

for some reason, where does not seem to work anymore, but the equivalent filter does:

dv.taskList(dv.current().file.tasks.filter(t => !t.completed));
2 Likes

Fantastic. Thank you so much!

Hi all,

I am new to Obsidian and dataview.
I am currently investigating using dataviewjs to extract the notes contains a certain string in a field.
For example, I have to extract all the notes with Name field contains string “John”.
It might be Name :: John 123/ Name :: John abc/ Name :: Johnny

I am currently using

dv.list(dv.pages("").where(k => k.name.includes("John")))

But it prompted me the error:
Evaluation Error: TypeError: Cannot read property ‘includes’ of undefined at eval

May I have your help on this?
Thank You so much

I only use dataview (not in JS), but do you need JS for it? I think the following may do it:

list from ""
where contains(Name, "John")
1 Like

Thanks for the recommendation @atiz .
Yes I would like to use JS as I want to do the grouping with another fields into different sections with headings using let group of.

I have also tried the dataview groupby with the row.Name but it can only be in the same section.

1 Like

This script is fantastic, but it did have some trouble if there were multiple tables next to each other. For anyone else with that problem, here is the small edit to fix that.

 // Source https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript
        const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

        const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
            v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, undefined, { numeric: true })
        )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

        document.querySelectorAll('table').forEach(table => {

            // do the work...
            Array.from(table.querySelectorAll('th')).forEach(th => th.style.cursor = "pointer");
            
            table.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
                const tbody = table.querySelector('tbody');
                Array.from(tbody.querySelectorAll('tr'))
                    .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
                    .forEach(tr => tbody.appendChild(tr));
            })));
        })

wow, this is really impressive!
I’ve also seen your other posts about the inline countdown feature

<%+* let edate = moment(“12-24”, “MM-DD”); let from = moment().startOf(‘day’); edate.diff(from, “days”) >= 0 ? tR += edate.diff(from, “days”) : tR += edate.add(1, “year”).diff(from, “days”) %>

Can this be also included in this table?

Dear Moonbase,

I edited your code to allow an extra column of countdowns to the next birthday

var start = moment().startOf('day');
var end = moment(start).add(dv.current().duration);
var dateformat = "YYYY-MM-DD";
if (dv.current().dateformat) { dateformat = dv.current().dateformat; }

// info text above table, {0}=duration, {1}=start date, {2}=end date
// parameters can be left out, or the string empty
var infotext = "Upcoming birthdays for {0} from now ({1} – {2})<br><br>";

//======================================================================

function nextBirthday(birthday) {
    // Get person’s next birthday on or after "start"
    // returns a moment
    
    // need to "unparse" because DV has already converted YAML birthday to DateTime object
    // shouldn’t harm if already a string
    var bday = moment(birthday.toString());
    var bdayNext = moment(bday).year(start.year());
    if (bdayNext.isBefore(start, 'day')) {
        bdayNext.add(1, "year");
    }
    return bdayNext;
}

function turns(birthday) {
    // Get the age in years a person will turn to on their next birthday

    // need to "unparse" because DV has already converted YAML birthday to DateTime object
    // shouldn’t harm if already a string
    var bday = moment(birthday.toString());
    return nextBirthday(birthday).diff(bday, 'years');
}

function countdown(birthday){
	var bday = moment(birthday.toString())
	const setTime = new Date(bday);
	const nowTime = new Date();
	const restSec = setTime.getTime() - nowTime.getTime();
	const day = parseInt(restSec / (60*60*24*1000));

	const str = day + "天";
	
	return str;
	
}

function showBirthday(birthday) {
    // Determine if this birthday is in the range to be shown
    // including the start date, excluding the end date
    // because that comes from a duration calculation
    // for use with "where", returns true or false
    
    if (birthday) {
        // need to "unparse" because DV has already converted YAML birthday to DateTime object
        // shouldn’t harm if already a string
        var bday = moment(birthday.toString());
        var bdayNext = nextBirthday(birthday);
        if (bdayNext.isBetween(start, end, 'day', '[)')) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

function sortByNextBirthday(a, b) {
    // comparator function for "sort"
    
  if (nextBirthday(a).isBefore(nextBirthday(b))) {
    return -1;
  }
  if (nextBirthday(a).isAfter(nextBirthday(b))) {
    return 1;
  }
  // they’re equal
  return 0;
}


//======================================================================

dv.paragraph(infotext.format(moment.duration(dv.current().duration.toString()).humanize(), start.format(dateformat), end.format(dateformat)));

dv.table(
    ["姓名", "生日/捡到日", "年龄(虚岁)","倒计时"],
    dv.pages(dv.current().searchterm)
        // use a function to see if this birthday is in range to be shown
        .where(p => showBirthday(p.birthday))
        // use a comparator function to sort by next birthday
        .sort(p => p.birthday, 'asc', sortByNextBirthday)
        .map(p => [
            p.file.link,
            p.birthday ? nextBirthday(p.birthday).format(dateformat) : '–',
            turns(p.birthday),
			countdown(nextBirthday(p.birthday)),
        ])
);
5 Likes

this was a good workaround for the bug with WHERE no longer working as intended. thank you!

1 Like

I’ve been utilizing my daily notes to track my habits as a list under a “Habits” header. I wrote some dataviewjs to help me keep up to date on these habits in a weekly roll-up in such a way that I am not required to define the habits in dataview.
Screenshot 2021-10-17 213102

This in combination with the Templater and Periodic Notes plugins let me automatically track habits in Obsidian with ease. Hopefully this can inspire other uses for dataviewjs.

var start = moment("2021-10-17");
var end = moment("2021-10-24");
var dailies = '"Daily Notes"';
var daily_fmt = 'YY-MM-DD';

let days = dv.pages(dailies).file
         .where(f => moment(f.name, daily_fmt).isBetween(start, end, 'day', '[]'))
         .sort(f => f.day);

let keys = ["Date"]
let habits = {}
days.forEach(day => {
    habits[day.name] = {}
    day.tasks.filter(task => task.link.subpath == "Habits").forEach(habit => {
        if (keys.indexOf(habit.text.toLowerCase()) == -1) {
            keys.push(habit.text.toLowerCase())
        }
        habits[day.name][habit.text.toLowerCase()] = habit.completed
    })
})

dv.table(keys.map(k => k[0].toUpperCase() + k.slice(1)), Object.keys(habits).map(k => {
    return [dv.fileLink(k)].concat(keys.slice(1).map(i => habits[k][i] ? "✔" : ""))
}))

Also, I decode the timestamp from the file name as I’ve found relying on the system for accurate timestamps for creation and modification is unreliable with my syncing solution. ymmv

16 Likes

This is incredible and what I was looking for. Thank you!

I’ve tried to update your code for my YAML format of:

dates:
  birthday: 2000-01-01
  anniversary: 2000-01-01

But for some reason as soon as I change .where(p => showBirthday(p.birthday)) to .where(p => showBirthday(p.dates.birthday)) it completely bugs out. When I update the .sort and .map functions using p.dates.birthday, they work fine. It’s just the where() function.

Do you have any idea what I am missing? Or can it not be done? Thanks again

List of non-existing pages

The following snippet shows a list of all non-existing pages in your vault, i.e. topics which are mentioned [[using wikilinks]], but for which there is no destination.

Each item in the generated list is a link, that creates the note if you click it.

# List of non-existing pages
```dataviewjs
let r = Object.entries(dv.app.metadataCache.unresolvedLinks)
		.filter(([k,v])=>Object.keys(v).length)
		.flatMap(([k,v]) => Object.keys(v).map(x=>dv.fileLink(x)))
dv.list([...new Set(r)])
```

Note: I’m rather new to Obsidian (only one week), its api, and the dataview plugin and I don’t know if there is a simpler way to do this. Critizism is welcome!

10 Likes

Hello,

Last week I shared my amateur task management set up with notes as tasks.

I asked whether a clickable Done button could update the Done field, and thus remove the task from the list.

@asauceda94 helpfully suggested this could be done using MetaEdit and Dataview JS. I was able to get partway to the conversion. But I wonder if any JS experts could help finish the job, thanks in advance.

convert from standard dataview table

TABLE WITHOUT ID 
	file.link as File_________________________________, 
	do-date as "Do_Date", 
	Priority as Priority________, 
	due-date as "Due_Date", 
	recur-length as "Recur Length",
	do-date + default(recur-length, "") as "Next_Date",
	project as Project______,
FROM #✅/S/1_Active  and !"⚙️ System"
WHERE 	done = null,
		do-date < date(tomorrow)
SORT priority asc, do-date asc, file.mtime desc

to DataviewJS table

const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
    const btn = this.container.createEl('button', {"text": "Done"});
    const file = this.app.vault.getAbstractFileByPath(fpath)
    btn.addEventListener('click', async (evt) => {
        evt.preventDefault();
        await update(pn, pv, file);
    });
    return btn;
}
dv.table([
		"Done", 
		"File_________________________________", 
		"Do_Date", 
		"Priority", 
		"Due_Date", 
		"Recur Length",
		"Next_Date",
		"Project______",
		], 
	dv.pages("#✅/S/1_Active")
    .sort(t => t["priority"], 'asc')
	.sort(t => t["do-date"], 'asc')
    .where(t => !t.done)
    .map(t => 
		[buttonMaker('Done', 'Done', t.file.path),
		t.file.link, 
		t["do-date"],
		t.priority,
		t["due-date"],
		t["recur-length"],
		t["NEXT DATE QUERY"],
		t["project"],
    ])
    )
  • Remaining tasks in converting to JS
    1. WHERE command - done = null (where done field is empty) ==> ==Solved== .where(t => !t.done)
    2. WHERE command - WHERE do-date < date(tomorrow) (where do-date is today or before)
    3. FROM command - Exclude files from System Folder
    4. Next Date table query - do-date + default(recur-length, "") as "Next_Date",
    5. JS script - Make the ButtonMaker add the current date - possibly using this script await update('completed-date', DateTime.local().toISODate(), file);
1 Like

Wonderful! Thank you!

it’s lot to ask for, but could you explain a little what happens in the code for us noobs in js?

I want to change the headers, both the names and how many. But little success so far.

Hi again @dryice !

Please note that I am NOT an expert in JS. These are what have worked for me, but I would not be surprised if there was a more optimal way to do this. However, I am here to help how I can. ALSO, one note about my DataviewJS style - I prefer to create a DV DataArray outside of a dv.table() call - I find that it keeps it a tad cleaner, and I am able to add more functionality that way.

  1. Comparing dates can be a little tricky from my experience. If you are using the standard ISO format in your DV fields (e.g. 2021-11-15) as I am:
// Below line of code is outside of the dv.pages() call
let today = DateTime.local().toISODate();

// I like to create my DV DataArrays outside of the dv.table call
let taskList = dv.pages("#tasks")
	.filter(t => t["due-date"] !== null)
	.filter(t => dv.date(t["due-date"]).toISODate() <= today);
  1. To my knowledge, I don’t think you can have two different DV sources in the FROM command. To solve this use case, I just add another filter statement to remove in the pages query:
let projectList = dv.pages("#projects")
	.filter(p => !p.file.folder.contains("INSERT FOLDER NAME HERE]"));
  1. For Next Date, I don’t have anything with this specific use case in my vault, but would probably involve using Luxon Duration. Please note I haven’t actually tested the implementation below, this is just a quick idea I cobbled together.
// All of your code above
.map(t =>
// More Code - rough NEXT DATE QUERY below
t["recur-length"] ? dv.date(t["do-date"]).plus({ days:  t["recur-length"] }).toISODate() : " ",
// Rest of your code
  1. My full button code (should have probs included it in other thread):
const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
    const btn = this.container.createEl('button', {"text": "Finished!"});
    const file = this.app.vault.getAbstractFileByPath(fpath)
    btn.addEventListener('click', async (evt) => {
        evt.preventDefault();
        await update(pn, pv, file);
	await update('completed-date',DateTime.local().toISODate(),file);
    });
    return btn;
}
1 Like