Recursively List a tree of outgoing links using dataview or dataviewjs?

Things I have tried

I’ve searched in Dataview Docs, this forum and Discord and couldn’t find a solution. I’d appreciate if someone could point me to documentation or offer any suggestions on how to achieve it.

Many thanks :pray:

What I’m trying to do

I want to retrieve a recursive list of all outgoing links from a specific note.
Generating a list/table of outgoing links from a “root” page and, in the same result list, include all outgoing links from the previous level of outgoing links.

Ideally, the query would include unlimited depth (listing outgoing of outgoing links indefinitely) similarly to what obsidian graph does.

Given the graph below, I’d like to use dataview to return outgoing links recursively as in the pseudo-dataview queries provided as examples below:

Example 1

dataview
list
FROM [[category]] -> Recursively.Outgoing

Results 1

Cat A
Cat B
Cat AA
Cat BB
Cat AB
CAT BA
Cat AB-A
Cat AB-B
Cat ABBA A


Example 2

dataview
list
FROM [[Cat A]] -> Recursively.Outgoing

Results 2

Cat AA
Cat AB
Cat AB-A
Cat AB-B
Cat ABBA A


Example 3

dataview
list
FROM [[Cat B]] -> Recursively.Outgoing

Results 3

Cat BB
CAT BA
Cat ABBA A


Example 4

list
FROM [[Project]] -> Recursively.Outgoing

Results 4

PA
PA A
Cat BA
Cat ABBA A
PA B
PAPB A
PB
PB A
PB B
PC
PC A


Example 5

dataview
list
FROM [[PA]] -> Recursively.Outgoing

Results 5

PA A
Cat BA
Cat ABBA A
PA B
PAPB A


Example 6

dataview
list
FROM [[PB]] -> Recursively.Outgoing

Results 6

PAPB A
PB A
PB B

1 Like

I reckon you can do that using DataviewJS. Build the query function, and then call it with your root page, and potentially a max depth indicator to avoid eternal recursion.

If you go down this rabbit holepath, be sure to use a dictionary or set to write result in, so that you don’t traverse in circles.

How do you plan to present your result?

Alternatively, how does your request differ from the graph view?

Thanks @holroy

I was hoping there was a built-in way of achieving that out of the box.
I’m not familiar with Javascript.

I guess I’ll reserve a day to see how far I can get.

Cheers

Without not knowing the JS the “reserve” a day for this is big overestimation of yourself.

Hi @holroy and @den ,

Thank you for your comments.

I figured out how to achieve what I wanted and I’ll leave some guidance here for future reference.

My interest was in recursively listing only “downstream” (children) of pages whose direct or indirect relationship is explicitly determined by the “up” metadata field.

Although my implementation only requires inlinks, I decided to leave both inlinks and outlinks in the code shared here so it can be used as a baseline by those who (like me) are not familiar with dataviewjs.

The implementation supports multiple parents for the same page. E.g:
up:: [[Parent A]], [[Parent B]]

There are two versions:

Not-Flattened

  • When a page has multiple parents, it displays one row for the page.
  • Parents are displayed as bulleted list in the same row.

Flattened

  • When a page has multiple parents, it repeats the page in multiple rows, as many times as there are parents.
  • Each parent is displayed in a separate row.

The screenshot demonstrates the expected results from a data sample based on the diagram originally posted when I created the topic.

Code: Not-Flattened version


//---------------------------
// Render a table containing the children notes as defined by the
// "up::" metadata. (Where up:: value denotes a "Parent" relationship, and multple parents are acceptable)
// Notes are retrieved recursively. So children, grandchildren, etc.... are retrieved
// In this non-flattened version, when multiple parents exist, all parents are listed as 
// bulleted list items in the same row (multiple-parents per row)
// --------------------------
// Author: Renato Mendonca
// Date: 2022-12-23
//---------------------------

let page = dv.current().file.path;
let pages = new Set();

//Add the dv.current() page to the stack as the starting point.
let stack = [page];

// Iterate through linked pages. Starting from the current page (dv.current)
while (stack.length > 0) {
	
	// Get the from the stack to check its in/out links
	let elem = stack.pop();
    
	// Meta is this iteration's page that is the source of outlinks and target of inlinks
	let meta = dv.page(elem);
    
	// If there isn't a page in the stack. Leave the loop.
	if (!meta) continue;

	// Iterate through all in and out links from this iteration's page (meta)
    for (let inlink of meta.file.inlinks.concat(meta.file.outlinks).array()) {
	    // Iterate through in/out links
		// Declare current in/out link as a page
		let link_up = dv.page(inlink.path)
	    
		// Retrieve the "up::" value for the current link
		// If linked file is not yet created (just a placeholder link), skip and don't try to get "up::" value
		if (link_up === undefined) continue;
	    
		// if linked page contains an "up::" one or more values, store them in an Array.
		// If linked page don't have an "up::" field, PathArray is empty.
		let link_up_PathArray = (dv.page(inlink.path).up === undefined ? [""] : dv.array(dv.page(inlink.path).up));
		let link_up_Path = (dv.isArray(link_up_PathArray))? link_up_PathArray.join(", ") : [""];
		
		// if this iteration's source page contains an "up::" one or more values, store them in an Array.
		// If this iteration's source page doesn't have an "up::" field, PathArray is empty.
		let this_up_PathArray = (meta.up !== undefined)? dv.array(meta.up) : undefined;
		let this_up_Path = (meta.up !== undefined)? this_up_PathArray.join(", ") : [""];

        
        // If the linked page is already in the Set or if it is equal to the this source page parent ("up::"), skip
		// As we don't want duplicates and we want to ignore any upstream links for now.
		if (pages.has(inlink.path) || inlink.path == this_up_Path) continue;
		
		// If the this source page is one of the values in the "up::" field of the linked page then:
		// Add to the pages Set and push to the stack
		if (link_up_Path.indexOf(meta.file.path)>=0) {
		    pages.add(inlink.path);
			stack.push(inlink.path);
		}
    }
}
// Data is now the file metadata for every page that directly OR indirectly links to the current page.
let data = dv.array(Array.from(pages)).map(p => dv.page(p));

//Render a Header and a Table

dv.header(1,"Downstream notes | Recursive | Not-Flattened")
dv.table(['Note','Created',"up"], data
		 .sort(p => p.file.mtime,"desc")
		 .map(p => [p.file.link,p.file.ctime,p.up])
		 );

Code: Flattened version

dataviewjs

//---------------------------
// Render a table containing the children notes as defined by the
// "up::" metadata. (Where up:: value denotes a "Parent" relationship, and multple parents are acceptable)
// Notes are retrieved recursively. So children, grandchildren, etc.... are retrieved
// In this Flattened version, when multiple parents exist, each parent record
// is represented in a separate row (1 parent per row)
// --------------------------
// Author: Renato Mendonca
// Date: 2022-12-23
//---------------------------

let page = dv.current().file.path;
let pages = new Set();

//Add the dv.current() page to the stack as the starting point.
let stack = [page];

// Iterate through linked pages. Starting from the current page (dv.current)
while (stack.length > 0) {
	
	// Get the from the stack to check its in/out links
	let elem = stack.pop();
    
	// Meta is this iteration's page that is the source of outlinks and target of inlinks
	let meta = dv.page(elem);
    
	// If there isn't a page in the stack. Leave the loop.
	if (!meta) continue;

	// Iterate through all in and out links from this iteration's page (meta)
    for (let inlink of meta.file.inlinks.concat(meta.file.outlinks).array()) {
	    // Iterate through in/out links
		// Declare current in/out link as a page
		let link_up = dv.page(inlink.path)
	    
		// Retrieve the "up::" value for the current link
		// If linked file is not yet created (just a placeholder link), skip and don't try to get "up::" value
		if (link_up === undefined) continue;
	    
		// if linked page contains an "up::" one or more values, store them in an Array.
		// If linked page don't have an "up::" field, PathArray is empty.
		let link_up_PathArray = (dv.page(inlink.path).up === undefined ? [""] : dv.array(dv.page(inlink.path).up));
		let link_up_Path = (dv.isArray(link_up_PathArray))? link_up_PathArray.join(", ") : [""];
		
		// if this iteration's source page contains an "up::" one or more values, store them in an Array.
		// If this iteration's source page doesn't have an "up::" field, PathArray is empty.
		let this_up_PathArray = (meta.up !== undefined)? dv.array(meta.up) : undefined;
		let this_up_Path = (meta.up !== undefined)? this_up_PathArray.join(", ") : [""];

        
        // If the linked page is already in the Set or if it is equal to the this source page parent ("up::"), skip
		// As we don't want duplicates and we want to ignore any upstream links for now.
		if (pages.has(inlink.path) || inlink.path == this_up_Path) continue;
		
		// If the this source page is one of the values in the "up::" field of the linked page then:
		// Add to the pages Set and push to the stack
		if (link_up_Path.indexOf(meta.file.path)>=0) {
		    pages.add(inlink.path);
			stack.push(inlink.path);
		}
    }
}
// Data is now the file metadata for every page that directly OR indirectly links to the current page.
let data = dv.array(Array.from(pages)).map(p => dv.page(p));

//Render a Header and a Table
dv.header(1,"Downstream notes | Recursive | Flattened")
dv.table(['Note','Created',"up"], data
		 .sort(p => p.file.mtime,"desc")
		 .flatMap(p => dv.array(p.up)
		 .map(item => [p.file.link,p.file.ctime,item]))
		 );

This is also a good example on how to use flatMap() (I didn’t find many examples and struggled a bit with it).

I also found other related topics on this forum, so I’m linking them here:

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.