Dataview plugin snippet showcase

Messing around with chatGPT’s code generation and had it generate a tag-cloud code using only dataviewjs and standard javascript. Took a bit of fiddling and about 4 hours of work but that is much quicker then I could have done this by hand…

Lists all files and tags, except those in the excluded paths. Determines size and color based on filename, number of tags, and word count (ignoring metadata, code blocks, and latex blocks. It also randomizes the order for good effect. Clicking on a file will open the file, clicking on tags does nothing but could be fixed if you feel like it.

const FromStr = `-"00 meta" AND -"50 logs"`;
const filesQuery = dv.pages(FromStr);

async function backlinksQuery(query) {
  const file = query.split('/').pop().split(".").slice(0, -1).join(".");
  return dv.query(
    `
    LIST
    FROM [[${file}]] AND ${FromStr}
    `
  );
}

/* Dataview query to retrieve all tags and their associated .md files */
async function tagsQuery(query) {
  return dv.query(
    `
    LIST
    FROM "${query}"
    where tags != ""
    `);
}

async function tagFilesQuery(query) { 
  return dv.query(
    `
    LIST
    FROM ${FromStr}
    where tags = "${query}"
    `);
}

/* Dataview query to retrieve the number of words in each .md file */
async function wordCountQuery(query) {
  const fs = require('fs');
  const path = require('path');
  const text = fs.readFileSync(path.join(app.vault.adapter.basePath, query), 'utf-8');
    // remove blocks
  const pattern = /---[\s\S]*?---|```[\s\S]*?```|\$[\s\S]*?\$|\$\$[\s\S]*?\$\$/g;
  const cleanedText = text.replace(pattern, '');
  // count words
  return cleanedText.match(/\S+/g).length;
}

/* Function to calculate the font size for each tag based on the number of backlinks and number of words */
function calculateFontSize(backlinks, wordCount) {
  const backlinksWeight = 0.2;
  const wordCountWeight = 0.8;
  const maxFontSize = 24;
  const minFontSize = 8;
  const fontSize = Math.sqrt((backlinks * backlinksWeight) + (wordCount * wordCountWeight)) * 4;
  return Math.round((fontSize / 100) * (maxFontSize - minFontSize) + minFontSize);
}

/* Function to calculate the color for each tag based on its name */
function calculateColor(tagName) {
  if (tagName == null) {return "#FFFFFF";}
  const colors = {
    "red": "#ff6464",
    "orange": "#ffb364",
    "yellow": "#ffd664",
    "green": "#64ff64",
    "blue": "#64bfff",
    "purple": "#b864ff",
    "ruby-red": "#D62E4D",
    "sunny-yellow": "#F6C026",
    "vivid-orange": "#FF6E1F",
    "bright-pink": "#F1478A",
    "electric-blue": "#0F7DC2",
    "deep-purple": "#5B0F91",
    "teal-green": "#007F86",
    "golden-brown": "#AA8F6A",
    "moss-green": "#8BC34A",
    "navy-blue": "#3F51B5",
    "pale-pink": "#F7B1B1",
    "soft-lilac": "#C9A1E9",
    "pastel-green": "#B3E6C3",
    "sky-blue": "#87CEEB",
    "light-gray": "#D3D3D3",
    "chocolate-brown": "#5F4B32",
    "cream-yellow": "#FFFDD0",
    "peach-orange": "#FFCC99",
    "dusty-rose": "#C4A4A4",
    "seafoam-green": "#71BC9C"
  };
  const tagWords = tagName.split(/\W+/g);
  const colorIndex = tagWords.reduce((total, word) => total + word.charCodeAt(0), 0) % Object.keys(colors).length;
  return colors[Object.keys(colors)[colorIndex]];
}

function shuffleArray(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

// tagData.push(fileData)
/* Create the HTML for the tag cloud */
const tagCloudHtml = async () => {
const tags = new Map();
const files = new Map();

/* Add all .md files to the tags map with their backlinks and word count */
Promise.all(filesQuery.map(async (f) => {
  const file = f.file
  const blq = backlinksQuery(file.path)
  const wcq = wordCountQuery(file.path)

  if (file.tags) {
    await Promise.all(file.tags.map(async (tag) => {
      if (!tags.has(tag)) {
        tags.set(tag, { backlinks: 0, wordCount: 0 });
      }
      const tagInfo = tags.get(tag);
      const res = await blq;
      tagInfo.backlinks += res.value.values.length;
      const wc = await wcq;
      tagInfo.wordCount += wc;
    }));
  }

  const fileInfo = { backlinks: 0, wordCount: 0 };
  const res = await blq;
  fileInfo.backlinks = res.value.values.length;
  const wc = await wcq;
  fileInfo.wordCount = wc;
  files.set(file, fileInfo);
}))
.then(() => {
  const data = []
  /* Calculate the font size, color, and shape for each tag */
  tags.forEach((tagInfo, tagName) => {
    const fontSize = calculateFontSize(tagInfo.backlinks, tagInfo.wordCount);
    const color = calculateColor(tagName);
    data.push({ name: `\\${tagName}`, fontSize, color });
  });

  /* Calculate the font size, color, and shape for each file */
  files.forEach((fileInfo, fileName) => {
    if (fileName == null) {return;}
    const fontSize = calculateFontSize(fileInfo.backlinks, fileInfo.wordCount);
    const color = calculateColor(fileName.name);
    data.push({ name: fileName.name, fontSize, color});
  });
 return shuffleArray(data).map((tag) => {
    return `<a class="tag-cloud-anchor" href="obsidian://open?vault=OBSIDIAN-VAULT&file=${encodeURIComponent(tag.name)}" style="font-size:${tag.fontSize}px; background-color: #4444444f; color: ${tag.color};">${tag.name}</a>`;
  }).join("");
}).then(res => dv.paragraph(res))
.catch(error => {
  console.error(error);
});
}
tagCloudHtml()
7 Likes

Hi, I want to list all files that link to my current note.

LIST
FROM [[]] AND #zettel
WHERE
	contains(source, this.file.link)
	AND "Permanent notes"

and it works fine, but only display files that link to the whole file, but not to a block of that file.

It lists files that have:

source:: [[The Book]]

But not the ones that have

source:: [[The Book#^3cd5dd]]

What do I need to change in that query to get a list of files that link to the whole file OR to a block in that file?

1 Like

Someone else asked a similar question, and then I found this post, and a hack to provide a solution for you as well.

The trick/hack is to use regexreplace() to insert the thousand separator. So do something like the below to achieve the output. I suggest keeping the numbers as numbers, so you can keep using them for calculations.


`= regexreplace("1234567.12", "\B(?=(\d{3})+(?!\d))", ",") `

TABLE regexreplace(someNumberField, "\B(?=(\d{3})+(?!\d))", ",") as "nice number"
....

Hope this helps, and that you’re still in search for this answer… :smiley:

1 Like

List Birthdays within next 30 days (sticking to pure dataview)

If you’re like me preferring pure dataview snippets without using js too much this one might be of interest to you. :wink:

Snippet will show you a list of persons celebrating birthday within next 30 days.

Prerequisite
Ideally you have a list of persons (e.g. VAULT/FAMILY/PERSONS … person1.md, person2.md …). These person-files need to have a YAML entry containing the next birthday:

nextbday: 2023-12-24

To display the list add following snippet to where you want it to be shown:

```dataview
LIST nextbday + " (in " + (round(dur(date("today") - nextbday).day) * (-1)) + " Tagen)"
FROM "ARBEIT/PERSONEN"
WHERE (round(dur(date("today") - nextbday).day) * (-1)) <= 30
SORT nextbday ASC

Just localize for yourself.
Don’t forget to update the field after celebration is done … :wink:

2 Likes

Aaaah … too late to edit.

An example I forgot to add:
image

A variant of your script which would eliminate the double calculation of the days, and avoid the negative multiplication would be:

```dataview
LIST nextbday + " (in " + dayCount + " Tagen)"
FROM "ARBEIT/PERSONEN"
FLATTEN round(dur(nextbday - date("today")).day) as dayCount
WHERE dayCount <= 30
SORT nextbday ASC
```

Update: For a different approach regarding next birthday, see

1 Like

Thank you holroy for your reply. For your approach it is necessary no YAML/nextbday is empty (in my case I don’t know the bdates of all my tracked persons). Otherwise you will get all the empty fields as additional result. Tried to work around with some default() attempts but failed. I am sure I overlooked something. :wink:

Do you require the nextbday to be present, and how do you want it handled when they’re not present?

Well, going with the initial script I posted empty nextbday fields simply are ignored - which is good in this case (there are other cases I really wished there would be something like a left outer join :wink: ). I think that’s because of the first calculation finding no value.
Long story short - empty fields (nextbday)should be ignored / not calculated. I tried that birthday js you posted in addition but for my taste and use it’s much too much. That’s because I tried that “simplier” way …

I failed to address the simple ignoring of your initial query when no nextbday was present, so to correct that just change to WHERE nextbday AND dayCount < 30 in my version, and you’ll get the same output. And that is also including mentioning birthdays of guys in the past or some days ago…

With this correction the full query is:

```dataview
LIST nextbday + " (in " + dayCount + " Tagen)"
FROM "ARBEIT/PERSONEN"
FLATTEN round(dur(nextbday - date("today")).day) as dayCount
WHERE nextbday and dayCount <= 30
SORT nextbday ASC
```

To compensate for the strangeness I’ve introduced you to by omitting the nextbday in the WHERE clause, I’ve also taken the liberty to extend your script to replace the year of nextbday with current year, so that you can keep running this query over and over again (year after year):

```dataview
LIST currbday + " (in " + dayCount + " Tagen) "
FROM "ARBEIT/PERSONEN"
FLATTEN date(today) as today
FLATTEN date(today.year + substring(string(nextbday), 4)) as currbday
FLATTEN round(dur(currbday - today).day) as dayCount
WHERE nextbday AND dayCount > -4 AND dayCount <= 30
SORT nextbday ASC
```

This version also includes a configurable number of birthdays in the last few days, just in case you’ve forgotten it. Adjust to your liking how many days you’d like to keep remembering old birthdays.

Using this last variant of the script, you could actually change the nextbday to the actual birthdate (with correct birthyear) of the person in question, and add stuff like + (currbday.year - today.year) + " Jahre alt" to show you how old they will become.

Update: Changed away from test folder setup using file.folder = this.file.folder

1 Like

Thank you for all your efforts holroy. You are impressing with shooting your ideas like a machine gun. :sweat_smile:
First script now runs like a charm:

Upcoming Birthdays

The second one (your goodie especially for me :slight_smile: thank you again!) don’t want to do as told. Results from “as it is” stays empty. For investigation I tried to do some changes:

cut out WHERE nextbday AND dayCount > -4 AND dayCount <= 30
Result:

change from above + changed WHERE file.folder = this.file.folder to FROM "110 PERSONEN2"
Result:

I suspect the substring to be the guilty one. Dataview don’t like that fooling around with strings …

I use nested tags like #i/DDMDCEKW/123 to mark source of tasks, where DDMDCEKW refers to the source item and 123 refers to the source page.

So i can use this snippet to show all task from the same item and sort by pages.

task
where contains(tags, "#i/DDMDCEKW")
sort filter(tags, (t) => regexmatch("#i/DDMDCEKW/\d+", t))
group by file.name
2 Likes

Dataviewjs help! almost the same query used in templater and in a simple note but the results are different

dv.taskList(dv.pages().file.tasks
.where(t => !t.completed)
.where(t => t.text.includes("2023-06-23")))

this query returns correctly, but when I change “2023-06-23” with today’s date, like: date(today)
but it always returns empty result… I thought it was just a simple snippet but it took me days without any progress, I don’t have any coding background so I had to call for help here…

exactly what i needed! thank you!

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.

DataviewJS Query

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

CSS



details {
  font: 16px "Open Sans", Calibri, sans-serif;
  width: 620px;
}

details > summary {
  padding: 2px 6px;
  background-color: #424741;
  border: none;
  box-shadow: 3px 3px 4px black;
  cursor: pointer;
  list-style: none;
}

details > p {
  border-radius: 0 0 10px 10px;
  background-color: #ddd;
  padding: 2px 6px;
  margin: 0;
  box-shadow: 3px 3px 4px black;
}

details:hover {
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
}

details summary::-webkit-details-marker {
  color: transparent;
  transform: scale(0.01);
}

details summary::before {
  content: '🡵';
  display: inline-block;
  font-size: 0.9em;
  margin-right: 8px;
  transform: translateY(-1px);
}

details[open] {
  background-color: var(--background-secondary);
}

details[open] summary {
  margin-bottom: 8px;
}

details[open] summary::before {
  transform: translateY(-1px) rotate(90deg);
}

details[open] summary:before {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  width: 0;
  height: 0;
  margin-left: -8px;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 8px solid #ccc;
}

summary::marker {
  content: "";
}

details > * {
  padding-left: 12px;
  padding-right: 12px;
}

details summary:hover::before {
  content: "";
  position: absolute;
  z-index: 999999;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  animation: bg-move 2s linear infinite;
  background-size: 100% 8px;
  background-image: linear-gradient(0, rgba(255, 255, 255, 0.05) 10%, transparent 10%, transparent 50%, rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0.05) 60%, transparent 60%, transparent);
}

@keyframes bg-move {
  0% {
    background-position: 0 0;
  }
  100% {
    background-position: 0 -32px;
  }
}

.heading-collapse-indicator,
.cm-fold-indicator .collapse-indicator {
  opacity: 0 !important;
}

details summary::-webkit-details-marker {
  font-size: 0.001px;
}

details > summary {
  list-style: none;
}
6 Likes

There is a way for links from generated tables to be counted in the backlinks?
like auto moc by tag

How do you make these headers with lines separators?

Absolutely brilliant!!!

An account log using “Checking::” entries in my Daily Notes. The Related Docs column links to relevant images/docs inside my Vault or uses a message://<message_id> link to open the relevant email directly. I use a Shortcut on my iPhone, iPad, Mac, &c., to append a properly-formatted bullet point to the end of my current daily note.

The » character can be typed on a Mac or keyboarded iPhone/iPad with ALT+SHIFT+\ or by holding down the single-quote key ' on the touch keyboard. I use » and « for field and row separators for almost everything because it’s a character that I’m highly unlikely to use for any other purpose, not being French. :slight_smile:

Since I’m on a Mac, I have an AppleScript to copy a reformatted link to a selected email:

tell application "Mail"
	set _sel to get selection
	set _links to {}
	repeat with _msg in _sel
		set _messageURL to "message://%3c" & _msg's message id & "%3e"
		set end of _links to _messageURL
	end repeat
	set AppleScript's text item delimiters to return
	set the clipboard to ("[Email Receipt](" & _links & ")" as string)
end tell

Use field: Checking
Row Layout: “Y” if Cleared»Amount»Where»Location»Related Docs

TABLE WITHOUT ID
regexreplace(string(round(sum(number(rows.Checking)) *100)), "(..)$", ".$1") AS "Final Balance"
FROM "Daily Notes"
FLATTEN Checking
WHERE Checking
GROUP BY true
TABLE WITHOUT ID
regexreplace(string(round(sum(number(rows.Checking)) *100)), "(..)$", ".$1") AS "Cleared Balance"
FROM "Daily Notes"
FLATTEN Checking
WHERE split(Checking,"»")[0] = "Y"
GROUP BY true
TABLE
	split(Checking,"»")[0] AS C,
	dateformat(date(split(Checking,"»")[4]), "DDD") AS Date, 
	split(Checking,"»")[2] AS Where, 
	split(Checking,"»")[3] AS Location, 
	regexreplace(string(round(number(split(Checking, "»")[1]) *100)), "(..)$", ".$1") AS Amount,
	split(Checking,"»")[5] AS "Related Docs"
FROM "Daily Notes"
FLATTEN Checking
WHERE Checking
SORT split(Checking,"»")[4] DESC

2 Likes

Since this a showcase thread, I’d thought I show you a potential nicer way to setup such a query as that last one. This way you’ll get easier access to all the bits and pieces, and could continue to do WHERE clauses, or GROUP BY using the predefined version of your Checking.

One vital change in the script below also, is that it starts of by doing the WHERE Checking before it tries to FLATTEN and split it. This causes the query to skip all files not having that field before trying to do something with it. Note that you can have multiple WHERE clauses and so, so there is no problem adding one on the line before the SORT for example.

```dataview
TABLE
	check.C AS C,
	check.LocDate AS Date, 
	check.Where AS Where, 
	check.Location AS Location, 
	check.Amount AS Amount,
	check.Related AS "Related Docs"
FROM "Daily Notes"
WHERE Checking

FLATTEN list(split(Checking, "»")) as tmpC
FLATTEN object(
  "C", tmpC[0],
  "Amount", regexreplace(string(round(number(tmpC[1]) *100)), "(..)$", ".$1"),
  "Where", tmpC[2],
  "Location", tmpC[3],
  "LocDate", dateformat(date(tmpC[4]), "DDD"),
  "Date", date(tmpC[4]),
  "Related", tmpC[5] ) as check
  
SORT check.Date DESC
```

I think this way of doing a split is much better due to a few reasons:

  1. It only does the split once per file, instead of the seven splits you did
  2. It allows for easier access to the split part in other places of your query, like it’s a lot easier to do check.Amount instead of repeating that regex-thingy.
  3. Adding a few blank lines in the query, makes it clearer how the first part handles the display part, and main filtering of which files we’re working with. The second part transforms the data into something meaningful, and the third part (here just the sorting) could further refine which data to display