Dataview: how can i get group of backlinks of a backlink

As title described, i trying to get backlinks of a backlink in dataview since it gonna help me display links between note a lot better

What I’m trying to do

  • Example:
    Note C link to note A
    Note D link to note A
    Note E link to note A
    Note F link to note B
    Note G link to note B
    Note H link to note B
  • And
    Note L link to note C
    Note J link to note C
    Note K link to note G
    Note O link to note G
  • And then the pattern continues on.
  • What i’m trying to achieve is a table with
  1. column 1 is Highest order notes (Group: A, B)
  2. column 2 is Group of Second order notes (Group: C, D, E belong to A and F, G, H belong to B)
  3. Column 3 is “Group of Third order notes” (Group: “L, J” belong to C and “K, O” belong to G)
  • I’m really sorry for this confusing explanation since i hadn’t fought a better idea to present it.

Things I have tried

  • I have searched through obsidian forum and haven’t fought an answer yet.
  • I have no clue about how to find backlinks of a backlink since there are very few answer on this topic (that i know of)
  • Please help me, i’m pretty new to dataview and really fascinating about how powerful this plugin has along side with excalidraw

Here is an attempt at a visual representation of your example:

The intuitive variant for solving this would be to FLATTEN file.inlinks for the various levels, as in the following query:

```dataview
TABLE WITHOUT ID level1, level2, level3
WHERE file.folder = this.file.folder
  AND file = this.file
FLATTEN file.inlinks as level1
FLATTEN level1.file.inlinks as level2
FLATTEN level2.file.inlinks as level3
```

Sadly this requires for there to be third order backlinks to list out that line at all, so the output was:

To counter this, we need to introduce something for those not having inlinks at that level, so here is another attempt:

```dataview
TABLE WITHOUT ID level1, level2, level3
WHERE file.folder = this.file.folder
  AND file = this.file
FLATTEN flat(list("none", file.inlinks)) as level1
FLATTEN flat(list("none", level1.file.inlinks)) as level2
FLATTEN flat(list("none", level2.file.inlinks)) as level3
SORT [level1, level2, level3]
```

With the output of:

This does however include some extra lines like the third line with “C and none” and the second to last line with “G and none”. And I’m not sure on how to remove those without causing other effects… Bummer…


On a totally unrelated sidenote, but still with regards to this request, here is one alternative query which doesn’t try to group the different order of inlinks:

```dataview
TABLE WITHOUT ID
  flat(file.inlinks,0) as level1,
  flat(file.inlinks.file.inlinks, 10) as level2,
  flat(file.inlinks.file.inlinks.file.inlinks, 10) as level3
WHERE file = this.file
```

Which has this output:

With my specifically named file you can see why they belong in there, but you don’t know the secondary or tertiary group/order, but it’s a given that the level3 links does have a link path to the origin note… (Note also that for this test (and some of the last attempts before this) I added the “P → none” file which links to my origin, but nothing links to it).

I wasn’t satisfied with not making the DQL version do what I intended for it to do, so I went about and made a dataviewjs version instead which delivers the result I was aiming for.

This version uses dv.view() to trigger a script, so given that the script below is stored within the vault in the file _js/backlintable.js, you can write the following to get the backlinks table for your current file:

`$= await dv.view("_js/backlinktable") `

And it could produce output like the following:

Other startup variants

You can also choose where to start or how many levels you want, by doing either of the following combinations:

dv.view("_js/backlinktable", { levels: 4} ) `
dv.view("_js/backlinktable", { start: dv.fileLink("Startpage")} ) `
dv.view("_js/backlinktable", { levels: 2, start: dv.fileLink("Startpage")} ) `

A few comments on the script

Basically this scripts loops through all the inlinks to the start file and for each of them it goes recursively down one level and repeats the same for all inlinks to that file. If none are found, it returns none for all those levels. The way to read this table is that a link in the table links to the page in its left column.

So for the first row we can deduce that “J → C” has a link to “C → A” which has a link to “A → none” which doesn’t link to anything besides the current file. Sorry for confusing names, but I needed to have something to help me keep track of which note linked where… :slight_smile:

The script does not handle if you make stuff loop around itself. Not in the current version at least. It could be handled by doing a lookup table to see if a given file has been seen before, and if so bail out possibly with a note on stating which loop is detected.

Installing the script

To make a dv.view() script work it needs to be stored in a .js file. These needs to be created outside of Obsidian (unless you’ve got some plugin allowing for these files to be created, like the Plaintext plugin). And it’s preferable to gather these script in some dedicated folder like _js.

Below I’ve provided both the textual version here in the forum post, and a downloadable zipped version of the same script, which should be ready to be inserted into the aforementioned folder.

The source script used by the dv.view()
// Set debug to 0 for no debug output
// 1 for some debug related to the stack
// 2 for row information too
// 3 for Page information as well
const debug = 0 // Set to 0 for no debug output

let allRows = [] // Holds all rows of backlinks
let stack = []  // Intermediate stack for building the row

// Lift some information from the input
let start = input?.start ?? dv.fileLink(dv.current().file.name)
let levels = input?.levels ?? 3


/**** Start of main run ****/
if (debug) console.log("\n\nNew run")

// Initiate the backlink table build
traverseBacklinks(stack, levels, start, 0, allRows)

if (debug>1)console.log(allRows)

// Build column headers
let headers = []
for (let i = 1; i <= levels; i++) {
  headers.push("Level " + i)
}

// Output the resulting table
dv.table(headers, allRows)

/**** End of main run ***/


/* traverseBacklinks
 *   When entered it pushes the current page (or "none") to the
 * stack, and checks which level we're on. If we're at bottom 
 * level we push the entire row to allRows and bails out after
 * pushing the last value on the stack.
 *   Otherwise we handle three different cases:
 *    1) Previous level was "none" so no more inlinks
 *    2) Current level doesn't have any inlinks
 *    3) Loop through all inlinks
 * In each of the cases we call ourselves with a suitable current
 * and start over again. At the end of each call, make sure to 
 * pop the last value of the stack. 
 */
  
function traverseBacklinks(stack, lev, current, colorIdx, allRows) {
  if (debug) console.log("-->", lev, typeof current == "string" ? current: current.path)
  stack.push(current)
  if ( lev == 0 ) {
    // Reached the required depth, so add current stack to 
    // the allRows array, and return
    if (debug>1) console.log("Adding row: ", ...stack)
    allRows.push([...stack.slice(1)])
    if (debug) console.log("<++ row")
    stack.pop()
    return
  } 

  // Go down one level, and check 
  const newLevel = lev - 1
  if ( typeof current == "string") {
    traverseBacklinks(stack, newLevel, "none", 0, allRows)
    if (debug) console.log("<-- empty")
    stack.pop()
    return
  }

  const page = dv.page(current.path)
  if (debug > 2) console.log("    Page: ", page)

  let colorIndex = -1
  if ( page.file.inlinks.length == 0) {
    traverseBacklinks(stack, newLevel, "none", 0, allRows)
  } else {

    for (const back of page.file.inlinks.values) {
      colorIndex += 1
      traverseBacklinks(stack, newLevel, back, colorIndex, allRows) 
    }
  }
  if (debug) console.log("<--")
  stack.pop()
}

backlinktable.js.zip (1.2 KB)

I’ve provided some comments within the script, but it’s not meant for educational purposes, so you do need to know some coding in order to understand what’s happening in the script.