DataviewJS Snippet Showcase

this sort of does the job:

TABLE file.tasks[0].text as task
FROM #your_tag
WHERE file.tasks
LIMIT 10

replace the from tag with whatever tag unifies your notes, or use some other source/add your filtering to the where block. Limit is just there in case of a mistake to prevent it blowing up, can be deleted.

One thing that occurs to me is it wouldn’t filter out completed tasks. js is probably required for a more complete solution. I might try later if I get some time.

as usual, the problem bothers me until I can solve it.


// grab first non-completed task from #test pages
let ftasks = dv.pages("#test").map(t => t.file.tasks.where(t => t.completed==false).sort(t.line).first() ) 

// filter undefined
let first_tasks = ftasks.filter(item => item !== undefined)

// render result
dv.taskList(first_tasks)
1 Like

Hey! That looks like a winner!

Hi, I’m trying to retrive all tasks which contains a due date of today or upcoming. The date are written as part of the task text like in: - [ ] Review paper on Hunger [[2023.02.27]]
I’m trying this one, but the result is always empty. Can anybody help?

dv.taskList(dv.pages().file.tasks 
  .where(t => !t.completed)
  .where(t => t.text.includes(dv.date('today'), 'YYYY-MM-DD')))

I’ve written a new version of the commands and hotkey script, see here, and with that you could also achieve the same by changing the following at the top:

const showDefaultKeys = false
const showCustomKeys = true
const showCommandsWithoutKeys = false

It’ll also show any duplicates you’ve set in your hotkeys, if there are any.

I’ve revised the script, see here, and now it’s default settings is showing the module/plugin in a separate column.

If you want to, you can also play around with the setting of showAllModules, and changing which modules/plugins you want to display or exclude from the list. I’ve listed in a comment section all the various modules related to Obsidian, so if you want to exclude them that’s easily done. Other plugins can be handled by viewing the list, and then adding them to either list of choice.

Hope this helps in your specific use case.

Thanks for this, really useful to me. I made one change to my version to add a link to the Obsidian search for each of the entries. Useful for cleanup for me.
I changed the last map line to:

	.map(k => ["["+k.fieldName+"](obsidian://search?query="+k.fieldName+")",k.count]));
1 Like

DVJS creates a table that shows the headings and the links below each heading


Sample note:


[[Mathematical foundations]]

[[Coding theory]] – Useful in networking, programming, system development, and other areas where computers communicate with each other.

[[Game theory]] - Useful in artificial intelligence and cybernetics.

[[Discrete Mathematics]]

[[Graph theory]] – Foundations for data structures and searching algorithms.

[[Mathematical logic]] – Boolean logic and other ways of modeling logical queries; the uses and limitations of formal proof methods

[[Number theory]] – Theory of the integers. Used in cryptography as well as a test domain in artificial intelligence.

[[Algorithms and data structures]]

[[Algorithms]] – Sequential and parallel computational procedures for solving a wide range of problems.

[[Data structures]] – The organization and manipulation of data.



DVJS code:


// Read the current file
const input = await dv.io.load(dv.current().file.path);

// Initialize the table array
const table = [];

// Split the input file into lines
const lines = input.split('\n');

let currentSection = null;
let currentSectionIndex = -1;
for (const line of lines) {
  const match = line.match(/^#+\s+(\[\[.*?\]\])/);
  if (match) {
    // This is a new section
    currentSection = match[1];
    currentSectionIndex++;
    table[currentSectionIndex] = [currentSection];
  } else {
    const match = line.match(/^\s*(\[\[.*?\]\])/);
    if (match) {
      // This is a sub-item of the current section
      if (currentSection !== null) {
        if (table[currentSectionIndex].length === 1) {
          table[currentSectionIndex][1] = [];
        }
        table[currentSectionIndex][1].push(match[1]);
      }
    }
  }
}

//console.log(table);
dv.table(["Topic", "Sub-topic"], table)


Result:


IMPORTANT:

  1. “Topic” (headings) can be at any level (ie. #, ##, ### etc…)
  2. “Sub-topic” links must be at the beginning of each line without any preceding character
  3. Both “Topic” and “Sub-topic” must be links (it also accepts links to notes that don’t exist yet

Pros
No longer need to use tasks’ notation - [ ] in order to grab elements (if are links themselves) (ie. Sub-Topic) below headings (ie. Topic)


#get-help #share-showcase

1 Like

Is there a way to use customJS with inline dataviewjs? I am working on adding data summaries to my daily note and wanted to store all of the methods in a customJS class and use this class to generate the Talley?

For example is there a way to use the defined class (ie const {DataHelper} = customJS which I put in a dataviewjs code block at the top of the note, later in the same note inline

const {DataHelper} = customJS

ADDITIONAL TEXT OF NOTE

Coffee: $= DataHelper.sumFunction(‘coffee’)

When I do this, I receive an error that DataHelper is undefined. Curious if there is a way to ‘persist’ a variable defined in on dataviewjs code block throughout a given note?

Thanks in advance for any clarity on how to do this, or if it is even possible.

Don

So the notation of const {DataHelper} = customJS is really just a shortcut for storing the customJS.DataHelper into a variable of its own. Knowing that, we can redefine to not use this extraction at all and simply do:

Coffee:: `$= customJS.DataHelper.sumFunction('coffee') `

Some other variants which also should work, but personally I don’t think looks as nice:

`$= const DH = customJS.DataHelper; DH.sumFunction('coffee') `
`$= const {DataHelper} = customJs; DataHelper.sumFunction('coffee') `
`$= const {DataHelper:DH} = customJS; DH.sumFunction('coffee') `

For more information on how to destructure objects, see Destructuring assignment - JavaScript | MDN

Also note that as long as you can use semicolon to indicate the line shift, you can have as many “lines” of code within the inline dataviewjs query as you feel like.

One final note: When you do coffee:: `$= … `, you don’t set the inline field to the value of that inline query, you set the field to the query itself, so if you use this in another context it can cause issues if that’s not a place where that query can be evaluated.

For example, if you use the first variant above, and then do `$= console.log(dv.current().coffee) `, the output you’ll get in the console is `$= customJS.DataHelper.sumFunction(‘coffee’) `.

Thank you very much for that clarification. I appreciate the details and now understand how curly brackets work within the context of customJS! Thanks again for this

Don

1 Like

Amazing code, just tried it.
Is there a way to only pull in the commands that have a hotkey?

when I run the snippets, I got an error like:

Evaluation Error: TypeError: Cannot read properties of undefined (reading ‘toString’)
at eval (eval at (plugin:dataview), :122:67)
at DataviewInlineApi.eval (plugin:dataview:18370:16)
at evalInContext (plugin:dataview:18371:7)
at asyncEvalInContext (plugin:dataview:18381:32)
at DataviewJSRenderer.render (plugin:dataview:18402:19)
at DataviewJSRenderer.onload (plugin:dataview:17986:14)
at e.load (app://obsidian.md/app.js:1:869278)
at DataviewApi.executeJs (plugin:dataview:18921:18)
at DataviewPlugin.dataviewjs (plugin:dataview:19423:18)
at eval (plugin:dataview:19344:124)

how to fix it?

I have a “hack” that I created to allow me to update frontmatter using dvjs and meta-bind. I store this in a file called client-menu.js that I access from a template via dv.view.
image

// Script to find all clients from frontmatter (2 flavours)
let clients1 = dv.pages().file.frontmatter.clients.values; // where 'clients' is populated in frontmatter YAML
clients1.unshift("ALL"); // add "ALL" option because I always want that, at top
let clients2 = dv.pages().file.where(function(b){if(Array.isArray(b.frontmatter.type)){if (b.frontmatter.type.includes("client")){return true;}}return (b.frontmatter.type == "client")? true : false;}).name; // where a note frontmatter type = 'client', had to handle empty & non-array
let uniqueclients = [...new Set([...clients1,...clients2])]; // concatenate & dedupe via a Set
let optionlist = [];
// nasty code to build meta-bind code block...but it works!
uniqueclients.forEach((currentElement)=>{optionlist.push("option(" + currentElement + ")")});
dv.paragraph("```meta-bind\nINPUT[multi_select("+optionlist+"):clients]\n```")

If anyone has a cleaner way to do this I’d love to hear it!

First off, thanks for this snippet! It has saved me tons of time…but I had a problem with it being really slow, so I modified the script to add in memory caching of the command list since that isn’t something that changes very often. This prevents it from having the super slow load time when you have alot of commands where the commands wouldn’t show up for a few seconds when I folded and unfolded the header where the dataview query was nested under. This also occurs when you go to a new tab and come back.

The optimized version that includes the in memory caching code, fixes this issue and it will only reload the query and pull data from the Obsidian command list every 10 minutes.

This is the code.

function Cache(config) {
    config = config || {};
    config.trim = config.trim || 600;
    config.ttl = config.ttl || 3600;

    var data = {};
    var self = this;

    var now = function() {
        return new Date().getTime() / 1000;
    };

    /**
     * Object for holding a value and an expiration time
     * @param expires the expiry time as a UNIX timestamp
     * @param value the value of the cache entry
     * @constructor ¯\(°_o)/¯
     */
    var CacheEntry = function(expires, value) {
        this.expires = expires;
        this.value = value;
    };

    /**
     * Creates a new cache entry with the current time + ttl as the expiry.
     * @param value the value to set in the entry
     * @returns {CacheEntry} the cache entry object
     */
    CacheEntry.create = function(value) {
        return new CacheEntry(now() + config.ttl, value);
    };

    /**
     * Returns an Array of all currently set keys.
     * @returns {Array} cache keys
     */
    this.keys = function() {
        var keys = [];
        for(var key in data)
            if (data.hasOwnProperty(key))
                keys.push(key);
        return keys;
    };

    /**
     * Checks if a key is currently set in the cache.
     * @param key the key to look for
     * @returns {boolean} true if set, false otherwise
     */
    this.has = function(key) {
        return data.hasOwnProperty(key);
    };

    /**
     * Clears all cache entries.
     */
    this.clear = function() {
        for(var key in data)
            if (data.hasOwnProperty(key))
                self.remove(key);
    };

    /**
     * Gets the cache entry for the given key.
     * @param key the cache key
     * @returns {*} the cache entry if set, or undefined otherwise
     */
    this.get = function(key) {
        return data[key].value;
    };

    /**
     * Returns the cache entry if set, or a default value otherwise.
     * @param key the key to retrieve
     * @param def the default value to return if unset
     * @returns {*} the cache entry if set, or the default value provided.
     */
    this.getOrDefault = function(key, def) {
        return self.has(key) ? data[key].value : def;
    };

	/**
	 * TODO: Add JSDoc to this function
	 */
	this.getOrCreate = function(key, fn) {
        if (self.has(key)) {
            return self.get(key);
        }
        const value = fn();
        self.set(key, value);
        return value;
    }
    
    /**
     * Sets a cache entry with the provided key and value.
     * @param key the key to set
     * @param value the value to set
     */
    this.set = function(key, value) {
        data[key] = CacheEntry.create(value);
    };

    /**
     * Removes the cache entry for the given key.
     * @param key the key to remove
     */
    this.remove = function(key) {
        delete data[key];
    };

    /**
     * Checks if the cache entry has expired.
     * @param entrytime the cache entry expiry time
     * @param curr (optional) the current time
     * @returns {boolean} true if expired, false otherwise
     */
    this.expired = function(entrytime, curr) {
        if(!curr)
            curr = now();
        return entrytime < curr;
    };

    /**
     * Trims the cache of expired keys. This function is run periodically (see config.ttl).
     */
    this.trim = function() {
        var curr = now();
        for (var key in data)
            if (data.hasOwnProperty(key))
                if(self.expired(data[key].expires, curr))
                    self.remove(key);
    };

    // Periodical cleanup
    setInterval(this.trim, config['trim'] * 1000);

    //--------------------------------------------------------
    // Events

    var eventCallbacks = {};

    this.on = function(event, callback) {
        // TODO handle event callbacks
    };
}
const cache = new Cache({ trim: 600, ttl: 3600 });

const getNestedObject = (nestedObj, pathArr) => {
    const cacheKey = `nestedObject-${JSON.stringify({ nestedObj, pathArr })}`;
    return cache.getOrCreate(cacheKey, () => {
        return pathArr.reduce((obj, key) =>
            (obj && obj[key] !== 'undefined') ? obj[key] : undefined, nestedObj);
    });
}

function hilite(keys, how) {
    if (keys && keys[1][0] !== undefined) {
        return how + keys.flat(2).join('+').replace('Mod', 'Ctrl') + how;
    } else {
        return how + '–' + how;
    }
}

function getHotkey(arr, highlight=true) {
    const cacheKey = `hotkey-${JSON.stringify({ arr, highlight })}`;
    return cache.getOrCreate(cacheKey, () => {
        let hi = highlight ? '**' : '';
        let defkeys = arr.hotkeys ? [[getNestedObject(arr.hotkeys, [0, 'modifiers'])],
        [getNestedObject(arr.hotkeys, [0, 'key'])]] : undefined;
        let ck = app.hotkeyManager.customKeys[arr.id];
        var hotkeys = ck ? [[getNestedObject(ck, [0, 'modifiers'])], [getNestedObject(ck, [0, 'key'])]] : undefined;
        return hotkeys ? hilite(hotkeys, hi) : hilite(defkeys, '');
    });
}

let cmds = dv.array(Object.entries(app.commands.commands))
    .sort(v => v[1].name, 'asc');

dv.paragraph(cmds.length + " commands currently enabled; " +
    "non-default hotkeys <strong>bolded</strong>.<br><br>");

dv.table(["Command ID", "Name in current locale", "Hotkeys"],
  cmds.map(v => [
    v[1].id,
    v[1].name,
    getHotkey(v[1]),
    ])
);
6 Likes

Hey @Moonbase59 ! Thanks a lot for sharing this code. It has been conducive to me.
I had to make a minor change to remove the Luxon dependency, which was breaking for me.
Also, as I use a different folder structure for my daily notes, I added some code to search the pages from the daily notes configured folder. The relevant lines of code I changed:

var folder = app['internalPlugins']['plugins']['daily-notes']['instance']['options']['folder'] || dv.current().file.folder
var p = dv.pages('"'+folder+'"').where(p => p.file.day).map(p => [p.file.name, p.file.day.toISODate()]).sort(p => p[1]);
var t = dv.current().file.day ? dv.current().file.day.toISODate() : dv.date("now").toFormat("yyyy-MM-dd");

Cheers,
Gilmar

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