DataviewJS Snippet Showcase

Please keep posts in this thread for “share and showcase”, and ask questions in Help

Noted with thanks.

Wanted a drop down menu for my dashboard so I used chat gpt to create this query and a css snippet to style it. I’m sure the query could be simplified or there is a better way to do this but it works and links to the pages AND it’s all updated automatically.

function formatDate(dateString) {
  let date = new Date(dateString);
  let options = { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: true };
  return date.toLocaleString("en-US", options);
}

let folderTree = new Map();

function addToFolderTree(path, content) {
  let parts = path.split("/");
  let currentLevel = folderTree;
  for (let i = 0; i < parts.length; i++) {
    let part = parts[i];
    if (!currentLevel.has(part)) {
      currentLevel.set(part, { folders: new Map(), files: "" });
    }
    if (i === parts.length - 1) {
      currentLevel.get(part).files = content;
    } else {
      currentLevel = currentLevel.get(part).folders;
    }
  }
}

for (let group of dv.pages('"----01. PROJECTS"').groupBy(p => p.file.folder)) {
  let folderName = group.key;
  let rows = group.rows
    .map(
      k => `| [${k.file.name.replace(/\|/g, "\\|")}](/${k.file.path.replace(/ /g, "%20")}) | ${formatDate(k.file.ctime)} | ${formatDate(k.file.mtime)} |`
    )
    .join("\n");

  let tableContent = `
| Name | Created | Modified |
| ---- | ------- | -------- |
${rows}
`;

  addToFolderTree(folderName, tableContent);
}

function renderFolderTree(folderMap, level = 0) {
  let content = "";
  for (let [folder, data] of folderMap.entries()) {
    let subcontent = data.folders.size > 0 ? renderFolderTree(data.folders, level + 1) : "";
    let folderContent = data.files ? data.files : "";
    if (level > 0) {
      content += `### ${folder}\n<details><summary>Click to expand</summary>\n\n${subcontent}${folderContent}\n\n</details>\n`;
    } else {
      content += `${subcontent}${folderContent}`;
    }
  }
  return content;
}

dv.header(6, renderFolderTree(folderTree));

1 Like

Hello!

There’s an error somewhere in this piece of code:

let pages = dv.pages("#Q_A").where(p => p.file.outlinks = "[[consultation]]")

or

let pages = dv.pages("#Q_A").where(p => p.file.outlinks.includes("[[consultation]]")

I don’t understand how to write it correctly to make it work.

Whole code:

```dataviewjs
let pages = dv.pages("#Q_A").where(p => p.file.outlinks === "[[консультация]]")


for (let group of pages.groupBy(p => p.status)) {
    dv.header(3, group.key);
    dv.table(["Column1", "Column2"],
        group.rows  
          .map(k => [k.file.link, k["created"]]
))
}

Help me, please.

Thank you so much!


Updated: In discord helped River :+1::

dv.pages("[[consultation]]")
  .where(p => p.file.tags.includes("#Q_A"))
1 Like

SEARCH FOR AND DISPLAY LIST OF CALLOUTS (WITH LINKS TO NOTES) IN A SPECIFIED FOLDER WITH A SPECIFIED TAG:

I scrolled through a fair amount of this thread looking for a way to list ‘todo’ callouts with a link, but only in a specific folder with a specified tag. I could not find it, so I am posting this for posterity’s sake. Credit to this question thread which allowed me to form a base to build this.

```dataviewjs
// Replace TAG with whatever tag you wish.
const pages = dv.pages('#TAG')

//Use the regular expression to define what kind of Callout you want to check
//Replace !todo with whatever callout type you wish.
const regex = />\s\[\!todo\]\s(.+?)((\n>\s.*?)*)\n/

//check for more types of callouts by adding new const regex# where # is a number
const regex2 = />\s\[\!TODO\]\s(.+?)((\n>\s.*?)*)\n/

const rows = []
for (const page of pages) {
    const file = app.vault.getAbstractFileByPath(page.file.path);
    //get file path as string
    var checkMe = "" + page.file.path;
    // Read the file contents
    const contents = await app.vault.read(file)
    // Extract the summary via regex
	for (const callout of contents.match(new RegExp(regex, 'sg')) || []) {

        //check if the file path is in your designated file path.
        //change FILEPATH to any keyword, folder name, or file path
        //You can also change to exclude a file path by changing "==true" to "!=true"
	    if(checkMe.includes('FILEPATH') == true){
		    const match = callout.match(new RegExp(regex, 's')) 
		    rows.push([match[1], page.file.link])
		}
    }

     //duplicate the for loop for each const regex# you created
     //adjust regex to the appropriate name
    for (const callout of contents.match(new RegExp(regex2, 'sg')) || []) {
	    if(checkMe.includes('FILEPATH') == true){
		    const match = callout.match(new RegExp(regex2, 's')) 
		    rows.push([match[1], page.file.link])
		}
    }
}

dv.table(['Term', 'Link'], rows)
```

I am still playing with this, and if I update it, I will edit this post or reply to it.
Hope this helps someone.

2 Likes

SEARCH FOR AND DISPLAY LIST OF CALLOUTS (WITH LINKS TO NOTES) IN A SPECIFIED FOLDER WITH A SPECIFIED TAG (UPDATE):
I went and tried to edit the post I made to include the regex Array, but I was unable to do so.

```dataviewjs
// Replace TAG with whatever tag you wish.
const pages = dv.pages('#TAG')

//Use the regular expression to define what kind of Callout you want to check
//Replace !todo with whatever callout type you wish.
//check for more types of callouts by adding new entries in the array.
//Remember: every key in the Array has to have a comma at the end, except for the last one
const regex = {
	0: />\s\[\!todo\]\s(.+?)((\n>\s.*?)*)\n/,
	1: />\s\[\!TODO\]\s(.+?)((\n>\s.*?)*)\n/
}
const rows = []
for (const page of pages) {
    const file = app.vault.getAbstractFileByPath(page.file.path);
    //get file path as string
    var checkMe = "" + page.file.path;
    // Read the file contents
    const contents = await app.vault.read(file)
    //cycle through the callout type array. 
    //Be sure to change the 'i<2' to be one more than your highest array key.
    for(var i = 0; i < 2; i++){
        // Extract the summary via regex
		for (const callout of contents.match(new RegExp(regex[i], 'sg')) || []) {
            //check if the file path is in your designated file path.
            //change FILEPATH to any keyword, folder name, or file path
            //You can also change to exclude a file path by changing "==true" to "!=true"
		    if(checkMe.includes('FILEPATH') == true){
			    const match = callout.match(new RegExp(regex[i], 's')) 
			    rows.push([match[1], page.file.link])
			}
	    }
	}
}

dv.table(['Term', 'Link'], rows);
```

How do you use this?

This is great and I would love to implement this, however I can’t get it to work.

In the People note I have added this frontmatter:
tags: [family, friends]
birthday: 2021-08-17

In the Birthdays note I have this frontmatter:
#searchterm:#family or #friends
searchterm: ‘“People”’
duration: 1 year
dateformat: “YYYY-MM-DD”

In this note I have placed the dataviewjs code.
It displays a Dataview query with no results.

Any help would be greatly appreciated.

Not the OP, but the following works in a local diagnostic (see screenshot below).

Person File:

---
birthday: 2021-08-17
tags: family
---

Birthdays File

---
searchterm: '#family'
duration: 1 year
---

```dataviewjs
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 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(
    ["Name", "Birthday", "Turns"],
    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)
        ])
);
```

2 Likes

Thank you so much! I must have misunderstood how to add the frontmatter to the person’s note. How can I query note’s tagged family or friend?

Any idea how to query friends or family with birthday month and day same as today? Would like to show today’s birthdays in a daily note.

how can you change the duration?

Hi Sam,

thanks for your work which works fine on my desktop device. Unfortunately it’s not working on my mobile (iOS) device. I added the same script folder as on my desktop but I get the following error:

Evaluation Error: eval
code@
eval@[native code]
@
...

I created the script file “journal.js”:

class Journal {
    add_life_progress_tracker(){
    }
}

And I execute the code in a dataviewjs block:

const {Journal} = customJS
Journal.add_life_progress_tracker()

Am I doing something wrong? For me it seems that the iOS device can’t find the script but it seems to be stored in my local Obsidian Vault.

Thanks
Dennis

I found the solution. By default Obsidian does not sync all file types within your Vault. You need to explicitly allow the synchronization of unknown files in the sync settings on all devices. After a restart of Obsidian everything works fine.
Maybe you could add this as hint to the customJS plugin description.

Hey, folks. I’m sure I’m missing something obvious: I’m working with Metadata Menu and in a lookup field, I want to query only the files that link to the page the field is on. How would I limit them in dv.pages() ? Thank you!

@Thecookiemomma , this is a thread mainly for showcasing solutions, not asking for help. Please repost your question in Help .

I have a code like this:

dv.header(2, 'Project');

dv.taskList(dv.pages().file.tasks 
  .where(t => !t.completed)
  .where(t => t.text.includes("[[Project]]")), false )

with this result:
10598_Obsidian_NvuTzksz

Could you please tell me how to remove Project from each task?
I want it to show without project, like this:
10599_Obsidian_38NtfC7L

THANK YOU VERY MUCH!

Please post new questions in Help , @friendlmickle , not as a showcase example…

2 Likes


image
I’ve fixed embeds. both dv.sectionLink as well as dv.fileLink work now after this pull is merged:
Fix: markdown render api change due to deprecation by GottZ · Pull Request #2266 · blacksmithgu/obsidian-dataview (github.com)

![[file]] will be fixed then.

1 Like

Does anyone have an example of creating a normal HTML table with dataviewjs? Preferably not using literal HTML tag strings? I’m having trouble getting createElement working?

I modified the original GTD script by @AlanG to perform GTD on Daily notes. More details here

tasksTodayDaily.js


/*
 * ---------------------
 * CONFIGURATION OPTIONS
 * ---------------------
 *
 * Add the folders, tags, and headings which should be excluded.
 * Tasks found on pages with these tags or paths will not appear in the Tasks page.
 * Headings will to exclude tasks that fall under a heading with this name.
 */

const globalExclude = {
  folders: [
    'Extras'
  ],
  tags: [
    '#exclude-master-tasklist',
    // '#completed'
  ],
  headings: [
    '🌱 Daily Habits'
  ]
}
/*
 * When displaying tasks from a project, tasks that fall under headings with these
 * names won't have these headings displayed when showing the project info
 */
const hideSubsectionName = [
  'Todo',
  'Tasks'
]

/*
 * ----------------------------
 * END OF CONFIGURATION OPTIONS
 * ----------------------------
 */

// Set up the variables to store the tasks and projects
const tasks = []
const noNextAction = []
const excludeItems = ['', ...globalExclude.tags]
globalExclude.folders.forEach(folder => excludeItems.push(`"${folder}"`))
const globalExcludeString = excludeItems.join(' AND -')
// console.log("globalExcludeString", globalExcludeString)


// Define groups for Daily tasklist page
/*
    For the daily view we have so far these groups:

    * Tasks started today
    * Tasks completed today
    * Pending tasks carrying over from previous days
    * Tasks due today, which already exists but doesn't properly work

*/
const Groups = {
  Started: 0,
  Completed: 1,
  Pending: 2,
  Due: 3,
  Normal: 4
}

const self = dv.current();
const path = require('path');

/**
  isStartedToday()
  Shows only tasks that were started on the day of the Daily note

    CRITERIA
    --------
 * Task text must not be empty
 * Task must NOT be completed today
 * Task header path corresponds to today's daily note and is not completed
 * Task with plus-sign emoji and done emoji must not show as Started but Completed
 * Completed tasks have four ways of marking: 
    * Done emoji with date with dashes
    * Done emoji with date with no dashes
    * The `completion `property` and a date with dashes
    * The completion circle filled with a check mark
    
  @param   {task} t - An Obsidian task
  @returns {boolean} true if matching all conditions, false otherwise
*/
function isStartedToday(t) {
  let taskTextNotEmpty = t.text.length > 0;
  // let notCompletedTodayDaily = self.file.path == t.header.path && !t.completed
  let taskInDailyNotCompleted = self.file.path == t.header.path && !t.completed

  let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
  let DateNoDashes = moment(self.file.name).format('YYYYMMDD')
  // let containsDateNoDashes = t.text.match(self.file.name) 
  //                            && typeof(self.file.name) == 'number'
  let matchPlusEmojiDateDashes = t.text.match('➕' + DateWithDashes) //  started today
  let containsDateWithDashes = t.text.match(DateWithDashes)
  let containsDateCompletionEmojiDashes = t.text.match('✅ ' + DateWithDashes) // completed
  let containsDateCompletionEmojiNoDashes = t.text.match('✅ ' + DateNoDashes)   // completed
  let containsDateCompletionColonColon = t.text.match('completion:: ' + DateWithDashes) // completed
  return (
        //  containsDateNoDashes 
        // || containsDateWithDashes 
        matchPlusEmojiDateDashes
        && !containsDateCompletionEmojiDashes
        && !containsDateCompletionEmojiNoDashes
        && !containsDateCompletionColonColon
        && taskInDailyNotCompleted
        && taskTextNotEmpty
  )
}

/**
  isCompletedToday()
  Shows only tasks that were completed on the day of the Daily note

    CRITERIA
    --------
 * Task text must not be empty
 * Task must be completed 
 * Task header path corresponds to today's daily note
 * Completed tasks have four ways of marking: 
    * Done emoji with date with dashes: '✅ ' + DateWithDashes
    * Done emoji with date with no dashes: '✅ ' + DateNoDashes
    * `completion` property and date with dashes: 'completion:: ' + DateWithDashes
    * The completion circle filled with a check mark: t.completed
	
  @param   {task} t - An Obsidian task
  @returns {boolean} true if matching all conditions, false otherwise
*/
function isCompletedToday(t) {
  let taskTextNotEmpty = t.text.length > 0;
  // capture tasks that are completed today
  let completedTodayDaily = self.file.path == t.header.path && t.completed
  // date in two formats
  let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
  let DateNoDashes = moment(self.file.name).format('YYYYMMDD')
  // capture tasks which text contains the current date, which could be start or completion
  let containsDateNoDashes = t.text.match(self.file.name)
  let containsDateWithDashes = t.text.match(DateWithDashes)
  // boolean statements
  let containsDateCompletionEmojiDashes = t.text.match('✅ ' + DateWithDashes)
  let containsDateCompletionEmojiNoDashes = t.text.match('✅ ' + DateNoDashes)
  let containsDateCompletionColonColon = t.text.match('completion:: ' + DateWithDashes)

  return (
          containsDateCompletionEmojiDashes
       || containsDateCompletionColonColon
       || containsDateCompletionEmojiNoDashes
       || completedTodayDaily
       && taskTextNotEmpty
  )
}

/**
 isOlderThanThisDailyNote()
 Pending tasks. 
 Shows only older tasks that were not yet completed at the date of the Daily note

   CRITERIA
   --------
 * Task text must not be empty
 * Task must NOT be completed 
 * Task header path corresponds to today's daily note
 * Task must be older than current daily note
 * Task contains a `due` date property
	
  @param   {task} t - An Obsidian task
  @returns {boolean} true if matching all conditions, false otherwise
*/
function isOlderThanThisDailyNote(t) {
  let basename = path.parse(t.header.path).name
  let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
  let mDailyDate = moment(self.file.name, 'YYYYMMDD', true); // Note
  let mAnyTaskDate = moment(basename, 'YYYYMMDD', true);  // task anywehere else
  //  <  0 does not include pending task if they were created today
  //  <= 0 includes pending task if they were created today
  let isOlderNote = mAnyTaskDate.diff(mDailyDate) < 0 // task date - note date
  let containsDateDueColonColon = t.text.match('due:: ' + DateWithDashes)
  return (
            isOlderNote
         && !t.completed
         && !containsDateDueColonColon
  )
}

/**
 tasksDueToday()
 Shows only tasks that are marked due today in the Daily note

   CRITERIA
   --------
 * Task text must not be empty
 * Task must NOT be completed 
 * Task header path corresponds to today's daily note
 * Task must be older than current daily note
	
  @param   {task} t - An Obsidian task
  @returns {boolean} true if matching all conditions, false otherwise
*/
function tasksDueToday(t) {
  let taskTextNotEmpty = t.text.length > 0;
  let containsDateDueColonColon = false;
  let includesDateDueColonColon = false
  let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
  // includesDateDueColonColon    = t.text.includes(DateWithDashes)

  if (t.due) {
    // console.log(DateWithDashes)
    containsDateDueColonColon = t.text.match('due:: ' + DateWithDashes)
    // console.log(containsDateDueColonColon)
  }

  return (
          taskTextNotEmpty
       && t.due
       && !t.completed
      //&& includesDateDueColonColon
       && containsDateDueColonColon
  )
}


/**
 * Take a task and the page it's from, and return a formatted element for the tasks array
 * @param {*} task 
 * @param {*} page 
 * @returns {object}
 */
function generateTaskElement(task, page) {
  // console.log(task)
  let group = Groups.Normal
  // if (task.tags.includes('#someday')) {
  if (isStartedToday(task)) {
    group = Groups.Started
  } else if (isCompletedToday(task)) {
    group = Groups.Completed
  } else if (isOlderThanThisDailyNote(task)) {
    group = Groups.Pending
  } else if (tasksDueToday(task)) {
    group = Groups.Due
  }
  return {
    task: task,
    date: (page.created ? page.created.ts || moment(page.created).valueOf() : null) || page.ctime.ts,
    group: group
  }
}


/*
 * Process tasks
 */
dv.pages("-#exclude" + globalExcludeString)
  .where(p => p.file.tasks.length && !p['kanban-plugin'] && !p['exclude_master_tasklist']).file
  .forEach(page => {
    page.tasks
      .where(t =>
        t.text && // where the task has text (is not blank)
        // !t.completed && // and not completed  // 1063
        // t.completed &&  // 1004
        !t.tags.includes('#exclude') && // and not excluded
        (!t.header.subpath || !t.header.subpath.includes('exclude')) &&
        !globalExclude.headings.includes(t.header.subpath) // and the heading is not excluded
      )
      .forEach(task => tasks.push(generateTaskElement(task, page)))
  })

// Sort tasks into groups, then ascending by created time
tasks.sort((a, b) => a.group - b.group || a.date - b.date)
// console.log(tasks)

/**
 * Output a formatted task list
 * @param {number|null} group - Filter for tasks in a particular group, or null for all tasks
 * @param {string|null} header - The text header for the task list
 */
function taskList(group, header) {
  const list = isNaN(group) ? tasks : tasks.filter(x => x.group === group)
  if (list.length) {
    if (header) dv.header(2, header)
    // dv.taskList(list.map(x => x.task), false)
    dv.taskList(list.map(x => x.task), true)
    return list.length
  } else return 0
}

let todayWithDashes = "*" + moment(self.file.name).format('YYYY-MM-DD') + "*"

// Output the task list
let nStarted   = taskList(Groups.Started, 'Started today: ' + todayWithDashes)
let nCompleted = taskList(Groups.Completed, 'Completed today: ' + todayWithDashes)
let nPending   = taskList(Groups.Pending, 'Still Pending Tasks Today: ' + todayWithDashes)
let nDue       = taskList(Groups.Due, 'Due Today: ' + todayWithDashes)

if (nStarted == 0 && nCompleted == 0 && nPending == 0 && nDue == 0) {
  dv.header(2, "No tasks to report") 
}