diff --git a/.env b/.env index e8c8eb1e26..95d07a2547 100644 --- a/.env +++ b/.env @@ -3,6 +3,7 @@ BASE_THEME=dark dgHomeLink=true dgShowBacklinks=true dgShowLocalGraph=true +dgShowGraphDepthControl=false dgShowInlineTitle=true dgShowFileTree=true dgEnableSearch=true diff --git a/src/helpers/constants.js b/src/helpers/constants.js index e7cea580ee..8b32216f3a 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -1,12 +1,13 @@ -exports.ALL_NOTE_SETTINGS= [ - "dgHomeLink", - "dgPassFrontmatter", - "dgShowBacklinks", - "dgShowLocalGraph", - "dgShowInlineTitle", - "dgShowFileTree", - "dgEnableSearch", - "dgShowToc", - "dgLinkPreview", - "dgShowTags" -]; \ No newline at end of file +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 8a275649cd..a75bd58aa7 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,27 +42,43 @@ 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)); + // 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); } - const nodes = Object.values(allNodes).filter(n => { - if (n.hide) return false; - return neighbourhood.has(n.url); - }); - - // If current node is not home, exclude the home node unless it's a neighbor - if (!currentNode.home && currentNode.neighbors) { - const homeNode = nodes.find(n => n.home); - if (homeNode && !currentNode.neighbors.includes(homeNode.url)) { - const idx = nodes.indexOf(homeNode); - if (idx > -1) nodes.splice(idx, 1); + // BFS out from the current node up to `depth` hops + 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 => !n.hide && visited.has(n.id)); const ids = nodes.map(n => n.id); return { nodes, @@ -430,6 +447,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 +461,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 +488,7 @@ function openFullGraph() { } function openExpandedLocal() { - openModal(filterLocalGraphData(graphDataCache)); + openModal(filterLocalGraphData(graphDataCache, localGraphDepth)); } function closeFullGraph() { @@ -477,7 +500,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 +510,16 @@ document.addEventListener('forestry:css-changed', function() { init(); -