DataviewJS Snippet Showcase

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

My friend @asauceda94 - noted… you’re not an expert, but you’ve definitely got skills and generosity - many thanks for your reply!

My progress: I have played around with your JS snippets, I have started a coding course to help, and I have tried really, really hard haha :sweat_smile:… But I feel like a toddler playing with a 1000 piece jigsaw puzzle.

Maybe I’ll have the skills someday in the future. It might be the instructional to see your code as a complete block, rather than walking me through step by step. What do you think?

But specifically I am having trouble with

  1. Insert Date Button code - I don’t know what goes into .map(t => at buttonMaker('FIELD', 'INSERTED TEXT', t.file.path)
  2. despite your explanations about DataviewJS style and arrays being outside of dv.table() call, I can’t find the right place for the “let” functions and filters. When I put them

Thanks!

1 Like

Considering that daily notes name has the format 2021-11-23, the following snippet lists tasks from previous daily notes and future 3 days, excluding the current day.

let pages = dv.pages('"Daily note"');

dv.taskList(
	pages
	.where(p => (dv.date(p.file.name) <= dv.date(dv.current().file.name) + dv.duration('3 days')) && !dv.equal(dv.date(p.file.name),dv.date(dv.current().file.name)))
	.sort(p => p.file.name, 'asc')
	.file
	.tasks
	.where(t => !t.completed))
1 Like

I created three sets of task queries for using in Daily/Weekly/Monthly Notes. In the following examples, Daily Note, Weekly Note and Month Note have the formats 2021-01-01, 2021-W1 and 2021-11, respectively, and they can be managed by using the Periodic Notes plugin: liamcain/obsidian-periodic-notes: Create/manage your daily, weekly, and monthly notes in Obsidian.

For instance, the three sets of queries can be inserted into template Daily/Weekly/Monthly Notes in the following way:

  • In the Daily Note template, one can use the query for Daily, Weekly and Monthly Note tasks.
  • In the Weekly Note template, one can use the query for Weekly and Monthly Note tasks.
  • In the Monthly Note template, one can use the query for Monthly Note tasks.

Daily Note tasks:

==Due before and on = date({{date:YYYY-MM-DD}}), and Due in 3 days:==

let pages = dv.pages('"Daily note"');

dv.taskList(
	pages
	.where(p => (dv.date(p.file.name) <= dv.date(dv.current().file.name) + dv.duration('3 days')) && !dv.equal(dv.date(p.file.name),dv.date(dv.current().file.name)))
	.sort(p => p.file.name, 'desc')
	.file
	.tasks
	.where(t => !t.completed))

Weekly Note tasks:

==Due before and in Week = date({{date:YYYY-MM-DD}}).weekyear, and Due in Week = date({{date:YYYY-MM-DD}}).weekyear + 1:==

let pages = dv.pages('"Weekly note"');
let dateToday = dv.date('{{date:YYYY-MM-DD}}');

dv.taskList(
	pages
	.where(p => (p.file.name <= dateToday.year.toString() + '-W' + (dateToday.weekNumber + 1).toString()))
	.sort(p => p.file.name, 'desc')
	.file
	.tasks
	.where(t => !t.completed))

Monthly Note tasks:

==Due before and in Month = date({{date:YYYY-MM-DD}}).month, and Due in Month = date({{date:YYYY-MM-DD}}).month + 1:==

let pages = dv.pages('"Monthly note"');
let dateToday = dv.date('{{date:YYYY-MM-DD}}');

dv.taskList(
	pages
	.where(p => (p.file.name <= dateToday.year.toString() + '-' + (dateToday.month + 1).toString()))
	.sort(p => p.file.name, 'desc')
	.file
	.tasks
	.where(t => !t.completed))
2 Likes

Sure !
The first line is the header, like Paper, Title, Year etc…
All my files are in the folder Zettelkasten.

The line "[[" + b.file.name + "|" + (b.file.aliases[1] || b.file.aliases[0]) + "]]", means to create a link for the first column, you can skip it.
The lines

b.file.aliases[0],
b.Year,
b.completion_reading,
b.completion_note,

are the informations I have added in the header of the files, like this

image

Here is what my header looks like.

2 Likes

Howdy again @dryice ! Sorry for the delay - holidays and all.

Let’s see if I can help! I’ll paste the first code block below, but first, a disclaimer - you should never, EVER, blindly paste code into dataviewjs if you don’t understand it. Bad actors could utilize it to wreak havoc on your system. This isn’t that but good practice moving forward :slight_smile:

Now, the full code block:

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;
}

let taskList = dv.pages("[[" + dv.current().file.name + "]]")
	.filter(p => p.tags == "tasks")
	.filter(p => p.status == "In Progress");
	
dv.table(["Task","Priority","Due Date","Pending Subtasks","Completed Subtasks"],
	taskList.sort(t => t.priority)
	.map(t => [t.file.link,
		t.priority,
		t["due-date"],
		t.file.tasks.filter(t => !t.fullyCompleted).length,
		t.file.tasks.filter(t => t.fullyCompleted).length,
		buttonMaker('status','Finished',t.file.path)
		])
)

To answer your questions directly:

  1. You might want to read the MetaEdit docs to understand a little more thoroughly, but the ‘FIELD’ is the DataView/YAML field that you want to update (for instance, ‘completion-date’ is a YAML field on my Task notes.

  2. Hopefully the code block above helps, but if it doesn’t, please let me know! I’m not sure I quite understand the issue you might be having here.

Does anyone have an example of the YAML frontmatter for the “people” entries? I am struggling to get the code to work so this would be very helpful.

Hello, all.

Is there anyone who know how to embed the results from dataviewjs?

```dataviewjs
let pages = dv.pages("#math").where(b => b.difficulty >= 1);
for (let group of pages.groupBy(b => b.material)) {
   dv.header(group.key);
   dv.list(group.rows.file.link);
}
Now, I got the list of links to the files.

file1
file2
file3

What I want to get is like these

![[file1]]
![[file2]]
![[file3]]

Thnak you in advance.
1 Like

I’m struggling to figure out how to embed the results from dataviewjs.

With the following script,

dv.paragraph("[[ABC]]")

I got the link to ABC.md in preview.

But, with the following script to embed ABC.md

dv.paragraph("![[ABC]]")

there is only ABC without the exclamation mark and any link.

At first, it embeds ABC.md successfully.
But, after a few seconds, it disappers leaving only ABC.

Can anyone help me solve this problem?

1 Like

i happened on a related question on the obsidian discord this afternoon. This will do what you want, I think:

(enclose this in a set of three tick marks in Obsidian)
dataviewjs
dv.paragraph(dv.fileLink("ABC",1))

I think you should leave the ‘paragraph’ attribute and use a more generic ‘div’ attribute to ensure it runs smoothly independently of your theme:

dv.el("div", dv.fileLink("MRCK",1))

Within a dataviewjs code block

1 Like

I made a snippet to track my daily habits along with a streak and total count.

To make this work, each daily journal entry needs frontmatter that looks like this:

---
habits:
  habit_1:
  habit_2:
  habit_3:
---

To use this frontmatter everyday, you can enable the Templates core plugin in Settings > Core plugins > Templates.

Then go to the options in Settings > Plugin options > Templates, and choose your folder location. In that folder, create a file called Daily_note where you will add the above frontmatter.
Then in Settings > Plugin options > Daily notes, update your Template File Location to the above your_template_folder/Daily_note.md. Now whenever you create a daily note, it will contain that frontmatter.

To see the table view of all your habits like in the first image, you can copy this snippet. You need to get the raw snippet, so if you’re not familiar with how that works, just use this link

In that snippet, the first section tells you what you need to change in order to match your system. Most important, habit_names have to match the exact names of the habits in your frontmatter. And daily_journal_loc needs to point to the folder where your journals are located.

Hope this helps.

8 Likes

I came to the forum because i’ve been hitting this issue as well.

I have an image IMG_20220107_230609.jpg

I have tried

dv.el("div", "![[IMG_20220107_230609.jpg]]")
dv.el("div", "![[IMG_20220107_230609.jpg|200]]")
dv.el("div", dv.fileLink("IMG_20220107_230609.jpg", true))
dv.el("div", dv.fileLink("IMG_20220107_230609.jpg", true, "200"))
dv.paragraph("![[IMG_20220107_230609.jpg]]")
dv.paragraph("![[IMG_20220107_230609.jpg|200]]")
dv.paragraph(dv.fileLink("IMG_20220107_230609.jpg", true))
dv.paragraph(dv.fileLink("IMG_20220107_230609.jpg", true, "200"))

Appears for a few seconds and then disappears again. I’m wondering whether there’s a bug. I had a look through the forum and noticed this thread that raised there was an issue with the Obsidian Plugin API not rendering embeddings… could this be the issue?

I wondered whether maybe the image size was an issue so I tried reducing the image size with the 200