From c7682a9939810485e11033d9eac454d578e71e25 Mon Sep 17 00:00:00 2001 From: Utsob Roy Date: Sun, 26 Jul 2026 18:03:54 +0600 Subject: [PATCH 1/4] feat: Graph depth --- src/site/_includes/components/graphScript.njk | 56 ++++++++++++++----- src/site/styles/digital-garden-base.scss | 16 ++++++ 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/site/_includes/components/graphScript.njk b/src/site/_includes/components/graphScript.njk index 8a275649cd..9e420ac640 100644 --- a/src/site/_includes/components/graphScript.njk +++ b/src/site/_includes/components/graphScript.njk @@ -23,8 +23,9 @@ function filterFullGraphData(graphData) { }; } -function filterLocalGraphData(graphData) { +function filterLocalGraphData(graphData, depth) { if (!graphData) return null; + depth = depth || 1; const allNodes = JSON.parse(JSON.stringify(graphData.nodes)); const allLinks = JSON.parse(JSON.stringify(graphData.links)); let currentLink = decodeURI(window.location.pathname); @@ -41,22 +42,39 @@ function filterLocalGraphData(graphData) { currentNode.current = true; - // Depth 1: collect immediate neighbors (neighbors array contains URLs) - const neighbourhood = new Set(); - neighbourhood.add(currentNode.url); - if (currentNode.neighbors) { - currentNode.neighbors.forEach(n => neighbourhood.add(n)); + // Build an id-based adjacency map so we can walk out to arbitrary depth + const adjacency = {}; + for (const l of allLinks) { + (adjacency[l.source] = adjacency[l.source] || new Set()).add(l.target); + (adjacency[l.target] = adjacency[l.target] || new Set()).add(l.source); + } + + // BFS out from the current node up to `depth` hops + const directNeighborIds = adjacency[currentNode.id] || new Set(); + const visited = new Set([currentNode.id]); + let frontier = new Set([currentNode.id]); + for (let i = 0; i < depth && frontier.size > 0; i++) { + const next = new Set(); + for (const id of frontier) { + for (const neighborId of (adjacency[id] || [])) { + if (!visited.has(neighborId)) { + visited.add(neighborId); + next.add(neighborId); + } + } + } + frontier = next; } const nodes = Object.values(allNodes).filter(n => { if (n.hide) return false; - return neighbourhood.has(n.url); + return visited.has(n.id); }); - // If current node is not home, exclude the home node unless it's a neighbor - if (!currentNode.home && currentNode.neighbors) { + // If current node is not home, exclude the home node unless it's a direct neighbor + if (!currentNode.home) { const homeNode = nodes.find(n => n.home); - if (homeNode && !currentNode.neighbors.includes(homeNode.url)) { + if (homeNode && !directNeighborIds.has(homeNode.id)) { const idx = nodes.indexOf(homeNode); if (idx > -1) nodes.splice(idx, 1); } @@ -430,6 +448,7 @@ let graphDataCache = null; let fullGraphDataCache = null; let localGraphCleanup = null; let globalGraphCleanup = null; +let localGraphDepth = 1; async function init() { const { graphData, fullGraphData } = await fetchGraphData(); @@ -443,11 +462,16 @@ function renderLocal() { localGraphCleanup(); localGraphCleanup = null; } - const data = filterLocalGraphData(graphDataCache); + const data = filterLocalGraphData(graphDataCache, localGraphDepth); const cleanup = renderGraph(data, 'link-graph', false); localGraphCleanup = cleanup; } +function setLocalGraphDepth(depth) { + localGraphDepth = parseInt(depth, 10) || 1; + renderLocal(); +} + function openModal(data) { if (globalGraphCleanup) { globalGraphCleanup(); @@ -465,7 +489,7 @@ function openFullGraph() { } function openExpandedLocal() { - openModal(filterLocalGraphData(graphDataCache)); + openModal(filterLocalGraphData(graphDataCache, localGraphDepth)); } function closeFullGraph() { @@ -477,7 +501,7 @@ function closeFullGraph() { } // Expose to Alpine -window.__graph = { openFullGraph, closeFullGraph, openExpandedLocal, rerenderLocal: renderLocal }; +window.__graph = { openFullGraph, closeFullGraph, openExpandedLocal, rerenderLocal: renderLocal, setLocalGraphDepth }; // Re-render when the style editor changes CSS variables document.addEventListener('forestry:css-changed', function() { @@ -487,10 +511,14 @@ document.addEventListener('forestry:css-changed', function() { init(); -
+
Connected Pages
+
+ + +
diff --git a/src/site/styles/digital-garden-base.scss b/src/site/styles/digital-garden-base.scss index 83882b5029..fb3c02b361 100644 --- a/src/site/styles/digital-garden-base.scss +++ b/src/site/styles/digital-garden-base.scss @@ -1383,6 +1383,22 @@ body.has-navbar .content { } } + .ctrl-left { + display: flex; + gap: 6px; + align-items: center; + + label { + white-space: nowrap; + } + + input[type="range"] { + width: 60px; + accent-color: var(--text-accent); + cursor: pointer; + } + } + .ctrl-right { display: flex; gap: 6px; From 9b091553e7a97ca4e12b73c21f951fdffac32736 Mon Sep 17 00:00:00 2001 From: Utsob Roy Date: Sun, 26 Jul 2026 17:54:10 +0600 Subject: [PATCH 2/4] fix: fix hiding nodes --- src/site/_includes/components/graphScript.njk | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/site/_includes/components/graphScript.njk b/src/site/_includes/components/graphScript.njk index 9e420ac640..ff65757bda 100644 --- a/src/site/_includes/components/graphScript.njk +++ b/src/site/_includes/components/graphScript.njk @@ -42,15 +42,27 @@ function filterLocalGraphData(graphData, depth) { currentNode.current = true; + // Hidden nodes (home page, index/hub pages like the-shelf or entity-index) + // are never rendered, but they're linked from a huge number of pages. If we + // let BFS walk through them as ordinary hops, they silently bridge otherwise + // unrelated parts of the graph together — the hidden hop then gets dropped + // from the output, leaving nodes that appear with no visible path to the + // current node. So a hidden node can only ever be a traversal *endpoint* + // (reached from, never walked through), except the current node itself, + // whose own direct links must still count even if it happens to be hidden + // (e.g. viewing the local graph on the home page). + const hiddenIds = new Set(Object.values(allNodes).filter(n => n.hide).map(n => n.id)); + const canBridge = id => id === currentNode.id || !hiddenIds.has(id); + // Build an id-based adjacency map so we can walk out to arbitrary depth const adjacency = {}; for (const l of allLinks) { + if (!canBridge(l.source) || !canBridge(l.target)) continue; (adjacency[l.source] = adjacency[l.source] || new Set()).add(l.target); (adjacency[l.target] = adjacency[l.target] || new Set()).add(l.source); } // BFS out from the current node up to `depth` hops - const directNeighborIds = adjacency[currentNode.id] || new Set(); const visited = new Set([currentNode.id]); let frontier = new Set([currentNode.id]); for (let i = 0; i < depth && frontier.size > 0; i++) { @@ -66,20 +78,7 @@ function filterLocalGraphData(graphData, depth) { frontier = next; } - const nodes = Object.values(allNodes).filter(n => { - if (n.hide) return false; - return visited.has(n.id); - }); - - // If current node is not home, exclude the home node unless it's a direct neighbor - if (!currentNode.home) { - const homeNode = nodes.find(n => n.home); - if (homeNode && !directNeighborIds.has(homeNode.id)) { - const idx = nodes.indexOf(homeNode); - if (idx > -1) nodes.splice(idx, 1); - } - } - + const nodes = Object.values(allNodes).filter(n => !n.hide && visited.has(n.id)); const ids = nodes.map(n => n.id); return { nodes, From 6dd47585ed70d065e7cba42133144ebe65dbf734 Mon Sep 17 00:00:00 2001 From: Utsob Roy Date: Wed, 29 Jul 2026 12:53:14 +0600 Subject: [PATCH 3/4] perf: make graph depth control optional --- .env | 1 + src/helpers/constants.js | 3 ++- src/site/_includes/components/graphScript.njk | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env b/.env index e8c8eb1e26..2af86931a9 100644 --- a/.env +++ b/.env @@ -9,3 +9,4 @@ dgEnableSearch=true dgShowToc=true dgLinkPreview=true dgShowTags=true +dgGraphDepthControl=false diff --git a/src/helpers/constants.js b/src/helpers/constants.js index e7cea580ee..013377af27 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -9,4 +9,5 @@ exports.ALL_NOTE_SETTINGS= [ "dgShowToc", "dgLinkPreview", "dgShowTags" -]; \ No newline at end of file + "dgGraphDepthControl", +]; diff --git a/src/site/_includes/components/graphScript.njk b/src/site/_includes/components/graphScript.njk index ff65757bda..82a909c5e1 100644 --- a/src/site/_includes/components/graphScript.njk +++ b/src/site/_includes/components/graphScript.njk @@ -514,10 +514,12 @@ init();
Connected Pages
+ {% if settings.dgGraphDepthControl %}
+ {% endif %}
From 0407f1cd4dc1d86114fb2e5f3d575d2277d4ac5f Mon Sep 17 00:00:00 2001 From: Utsob Roy Date: Wed, 29 Jul 2026 13:04:13 +0600 Subject: [PATCH 4/4] fix: rename variable --- .env | 2 +- src/helpers/constants.js | 24 +++++++++---------- src/site/_includes/components/graphScript.njk | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.env b/.env index 2af86931a9..95d07a2547 100644 --- a/.env +++ b/.env @@ -3,10 +3,10 @@ BASE_THEME=dark dgHomeLink=true dgShowBacklinks=true dgShowLocalGraph=true +dgShowGraphDepthControl=false dgShowInlineTitle=true dgShowFileTree=true dgEnableSearch=true dgShowToc=true dgLinkPreview=true dgShowTags=true -dgGraphDepthControl=false diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 013377af27..8b32216f3a 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -1,13 +1,13 @@ -exports.ALL_NOTE_SETTINGS= [ - "dgHomeLink", - "dgPassFrontmatter", - "dgShowBacklinks", - "dgShowLocalGraph", - "dgShowInlineTitle", - "dgShowFileTree", - "dgEnableSearch", - "dgShowToc", - "dgLinkPreview", - "dgShowTags" - "dgGraphDepthControl", +exports.ALL_NOTE_SETTINGS = [ + "dgHomeLink", + "dgPassFrontmatter", + "dgShowBacklinks", + "dgShowLocalGraph", + "dgShowGraphDepthControl", + "dgShowInlineTitle", + "dgShowFileTree", + "dgEnableSearch", + "dgShowToc", + "dgLinkPreview", + "dgShowTags", ]; diff --git a/src/site/_includes/components/graphScript.njk b/src/site/_includes/components/graphScript.njk index 82a909c5e1..a75bd58aa7 100644 --- a/src/site/_includes/components/graphScript.njk +++ b/src/site/_includes/components/graphScript.njk @@ -514,7 +514,7 @@ init();
Connected Pages
- {% if settings.dgGraphDepthControl %} + {% if settings.dgShowGraphDepthControl %}