Table of Contents
- Introduction
- Random example
- Science Articles example
- Current Features
- Code
- Change log
Introduction
- Hierarchies in Logseq have been a hot topic (e.g. here).
- This is also the case for namespaces, as they have been used to model hierarchies (e.g. here).
- Properties is the obvious alternative in modeling hierarchies, however they have been missing a proper visualization.
- There are various feature requests on the matter (e.g. this).
- In the present post I put together some custom code to that direction.
- It works, but is experimental.
- It follows some conventions that may differ from your approach.
- Feedback is welcome.
- Looking forward to improve and expand.
Random example
Consider a graph under construction that models human relationships (one page per human), by using two complimentary properties parent
and children
:
boy1.md
parent:: [[man1]]
boy2.md
parent:: [[man2]]
girl1.md
parent:: [[man2]]
girl2.md
parent:: [[man1]]
grandmother1.md
children:: [[man1]], [[woman1]]
grandmother2.md
children:: [[man2]], [[woman2]]
man1.md
parent:: [[grandfather1]]
man2.md
parent:: [[grandfather2]]
woman1.md
parent:: [[grandfather1]]
children:: [[boy2]], [[girl1]]
woman2.md
parent:: [[grandfather2]]
children:: [[boy1]], [[girl2]]
Depending on the nature of the property, there are two different perspectives:
- parent to child
- child from parent
Usually only one of two complimentary properties is used, but here both are used for comparison. To get the resulting tree, can use a respective macro (preferably on the right sidebar):
{{pagetree to, children}}
{{pagetree from, parent}}
The result looks like this:
Science Articles example
This is based on @gaxâs illustration here.
FEM.md
broader:: [[Numerics]]
Mechanics.md
broader:: [[Physics]]
Numerics.md
broader:: [[Math]]
SomeArticle.md
This is an article about #Physics
SomeArticle2.md
This is an article about #TensionSims
TensionSims.md
broader:: [[Tension]], [[FEM]]
Tension.md
broader:: [[Mechanics]]
To get the resulting tree, can use this macro (preferably on the right sidebar):
{{pagetree from, broader, is, refs}}
This example uses a second pair of verb, property
, which:
- attaches on each node a special node with its direct children (if any) per the passed pair
- accepts built-in values
has
andis
has
behaves liketo
is
behaves likefrom
- The combination
is, refs
receives special treatment, as instead of a propertyâs values it looks for references.
The result looks like this:
Current Features
- Siblings are sorted alphabetically.
- Click to open the respective page.
- These are custom links, not like Logseqâs.
- Collapse / Expand any node.
+
/-
when hovering
- Prevent too big or circular models from getting out of control.
- Can be customized by setting:
PageTree.max.depth
PageTree.max.siblings
- Can be customized by setting:
- A child with multiple parents appears multiple times in the tree.
- If the treeâs block is not touched, it doesnât auto-update its contents when the model gets updated.
- To update, just click in and out.
Code
- Inside file
config.edn
, insidemacros{}
:
:pagetree "<div class='kit pagetree' data-kit='pagetree' data-node-prep='$1' data-node-prop='$2' data-leaf-prep='$3' data-leaf-prop='$4'>pagetree</div>"
- Inside file
custom.css
:
.pagetree .tree-button {
visibility: hidden;
}
.pagetree:hover .tree-button {
visibility: visible;
}
.pagetree-node {
margin-left: 16px;
}
.tree-button {
width: 16px;
}
- This code requires having kits inside file
custom.js
.- If you prefer putting the whole code inside
custom.js
, you have to either:- remove all mentions to
logseq.kits
- create it yourself with
logseq.kits = {};
- remove all mentions to
- If you prefer putting the whole code inside
- Inside page
PageTree
in Logseq, put the following code in a single javascript code-block, or broken into multiple ones (could use the section comments):
// PageTree
PageTree = logseq.kits.PageTree = {
children: { queries: {}, titles: {} },
max: {},
roots: { queries: {}, titles: {} }
};
PageTree.configFromDiv = function configFromDiv(div){
const nodeprop = div.dataset.nodeProp;
if (!nodeprop) return;
delete div.dataset.nodeProp;
const leafprep = div.dataset.leafPrep;
const leafprop = div.dataset.leafProp;
const leafconfig = {
prep: (leafprep !== "$3") && leafprep,
prop: (leafprop !== "$4") && leafprop,
not: leafprop === "refs" && leafprep === "is" && nodeprop
};
const nodeconfig = {
prep: div.dataset.nodePrep,
prop: nodeprop
};
return {leaf: leafconfig, node: nodeconfig};
}
logseq.kits.pagetree = function PageTreeHandler(div){
const config = PageTree.configFromDiv(div);
if (!config) return;
const nodeconfig = config.node;
const pgRoots = PageTree.roots;
div.textContent = pgRoots.getTitle(nodeconfig);
const roots = pgRoots.get(nodeconfig);
const nofRoots = roots.length;
const buttonHandler = nofRoots && function(e){
e.preventDefault();
e.stopPropagation();
PageTree.toggleChildren(button);
}
const button = PageTree.button((nofRoots) ? "-" : "Ă", buttonHandler);
div.prepend(button);
PageTree.filler(config, div, roots, PageTree.max.depth);
}
// Elements
PageTree.link = function pageLink(name, originalName){
const a = document.createElement("a");
a.classList.add("page-ref");
a.href = "#/page/" + encodeURIComponent(name);
a.textContent = originalName;
return a;
}
PageTree.button = function treeButton(textContent, clickHandler){
const button = document.createElement("button");
button.classList.add("tree-button");
button.textContent = textContent;
if (clickHandler) button.addEventListener("click", clickHandler);
return button;
}
PageTree.toggleChildren = function toggleChildren(button){
const isExpanded = (button.textContent === "-");
button.textContent = (isExpanded) ? "+" : "-";
const newStyle = (isExpanded) ? "none" : "block";
Array.prototype.forEach.call(button.parentElement.children, (child)=>{
if (child.className === "pagetree-node") child.style.setProperty("display", newStyle);
});
}
PageTree.node = function treeNode(button, link){
const div = document.createElement("div");
div.classList.add("pagetree-node");
div.append(button, link);
return div;
}
// Fillers
PageTree.fillMore = function fillMoreChildDivs(config, divParent, children, items, child, isItemsNode, depth){
if (items.length) PageTree.filler(config, divParent, [{items, page: child}], depth);
PageTree.filler(config, divParent, children, (isItemsNode) ? -1 : depth);
}
PageTree.filler = function fillChildDivs(config, divParent, children, depth){
children.forEach( (child)=>{
const page = child.page;
const isItemsNode = (page) ? true : false;
const childName = (isItemsNode) ? page["original-name"] : child["original-name"];
const leafconfig = config.leaf;
const getChildren = PageTree.children.get;
const items = (leafconfig.prop && !isItemsNode && depth > 0) ? getChildren(leafconfig, childName) : [];
const nofItems = items.length;
const children = (depth < 0) ? [] : (isItemsNode) ? child.items : getChildren(config.node, childName);
const nofChildren = children.length + (nofItems ? 1 : 0);
const newDepth = depth - 1;
const small = (newDepth > 0 && nofChildren <= PageTree.max.siblings);
const buttonText = (nofChildren) ? ((small) ? "-" : "...") : " ";
const buttonHandler = nofChildren && function(e){
e.preventDefault();
e.stopPropagation();
if (button.textContent === "...") PageTree.fillMore(config, divChild, children, items, child, isItemsNode, PageTree.max.depth);
PageTree.toggleChildren(button);
};
const button = PageTree.button(buttonText, buttonHandler);
const link = (isItemsNode) ? PageTree.children.getTitle(leafconfig) : PageTree.link(child.name, childName);
const divChild = PageTree.node(button, link);
if (nofChildren && small) PageTree.fillMore(config, divChild, children, items, child, isItemsNode, newDepth);
divParent.append(divChild);
});
}
// Getters
PageTree.getData = function getData(query, ...inputs){
const res = logseq.api.datascript_query(query, ...inputs);
return res.flat().sort( (a, b)=>{ return a.name.localeCompare(b.name) } );
}
PageTree.children.get = function getChildren(config, parent){
const query = PageTree.children.queries[(config.not) ? "refs" : config.prep];
return PageTree.getData(query, ":" + (config.not || config.prop), `"${parent.toLowerCase()}"`);
}
PageTree.children.getTitle = function getTitle(config){
const prep = (config.not) ? "refs" : config.prep;
return PageTree.children.titles[prep].replace("leafprop", config.prop);
}
PageTree.roots.get = function getRoots(config){
const query = PageTree.roots.queries[config.prep];
return PageTree.getData(query, ":" + config.prop);
}
PageTree.roots.getTitle = function getTitle(config){
return PageTree.roots.titles[config.prep].replace("nodeprop", config.prop);
}
// Queries
PageTree.children.queries.refs = `[
:find (pull ?Child [*])
:in $ ?prop ?Parent-name
:where
[?child :block/page ?Child]
[?child :block/refs ?Parent]
[?Parent :block/name ?Parent-name]
[?Parent :block/original-name ?Parent-original-name]
(not
[?child :block/properties ?child-props]
[(get ?child-props ?prop) ?child-from]
[(contains? ?child-from ?Parent-original-name)]
)
]`;
PageTree.children.queries.from = `[
:find (pull ?Child [*])
:in $ ?prop ?Parent-name
:where
[?child :block/page ?Child]
[?child :block/properties ?child-props]
[(get ?child-props ?prop) ?child-from]
[?Parent :block/name ?Parent-name]
[?Parent :block/original-name ?Parent-original-name]
[(contains? ?child-from ?Parent-original-name)]
]`;
PageTree.children.queries.to = `[
:find (pull ?Child [*])
:in $ ?prop ?Parent-name
:where
[?Parent :block/name ?Parent-name]
[?parent :block/page ?Parent]
[?parent :block/properties ?parent-props]
[(get ?parent-props ?prop) ?parent-to]
[?Child :block/original-name ?Child-original-name]
[(contains? ?parent-to ?Child-original-name)]
]`;
PageTree.children.queries.has = PageTree.children.queries.to;
PageTree.children.queries.is = PageTree.children.queries.from;
PageTree.roots.queries.from = `[
:find (pull ?Root [*])
:in $ ?prop
:where
[?child :block/properties ?child-props]
[(get ?child-props ?prop) ?child-from]
[?Root :block/original-name ?Root-original-name]
[(contains? ?child-from ?Root-original-name)]
(not
[?root :block/page ?Root]
[?root :block/properties ?root-props]
[(get ?root-props ?prop) ?root-from]
)
]`;
PageTree.roots.queries.to = `[
:find (pull ?Root [*])
:in $ ?prop
:where
[?root :block/page ?Root]
[?root :block/properties ?root-props]
[(get ?root-props ?prop) ?root-to]
(not
[?parent :block/properties ?parent-props]
[(get ?parent-props ?prop) ?parent-to]
[?Root :block/original-name ?Root-original-name]
[(contains? ?parent-to ?Root-original-name)]
)
]`;
// Settings
PageTree.max.depth = 16;
PageTree.max.siblings = 16;
PageTree.roots.titles.from = "Tree from nodeprop:"
PageTree.roots.titles.to = "Tree to nodeprop:"
PageTree.children.titles.is = "is in leafprop of:"
PageTree.children.titles.has = "has leafprop:"
PageTree.children.titles.refs = "is referred by:"
For example, I have created a page that looks like this:
- The provided button is not needed to be pressed, unless changes are made to the code.
- This full page could be distributed or reused by other graphs as a single
PageTree.md
file (currently one copy per graph).
Changelog
- Encode links.
- Merge macro definitions into one.
- Accept an optional second pair of passed arguments.
- Support references with the special pair
is, refs
.
- Support references with the special pair
- Adopt kits.