Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ BASE_THEME=dark
dgHomeLink=true
dgShowBacklinks=true
dgShowLocalGraph=true
dgShowGraphDepthControl=false
dgShowInlineTitle=true
dgShowFileTree=true
dgEnableSearch=true
Expand Down
25 changes: 13 additions & 12 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
exports.ALL_NOTE_SETTINGS= [
"dgHomeLink",
"dgPassFrontmatter",
"dgShowBacklinks",
"dgShowLocalGraph",
"dgShowInlineTitle",
"dgShowFileTree",
"dgEnableSearch",
"dgShowToc",
"dgLinkPreview",
"dgShowTags"
];
exports.ALL_NOTE_SETTINGS = [
"dgHomeLink",
"dgPassFrontmatter",
"dgShowBacklinks",
"dgShowLocalGraph",
"dgShowGraphDepthControl",
"dgShowInlineTitle",
"dgShowFileTree",
"dgEnableSearch",
"dgShowToc",
"dgLinkPreview",
"dgShowTags",
];
71 changes: 50 additions & 21 deletions src/site/_includes/components/graphScript.njk
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -465,7 +488,7 @@ function openFullGraph() {
}

function openExpandedLocal() {
openModal(filterLocalGraphData(graphDataCache));
openModal(filterLocalGraphData(graphDataCache, localGraphDepth));
}

function closeFullGraph() {
Expand All @@ -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() {
Expand All @@ -487,10 +510,16 @@ document.addEventListener('forestry:css-changed', function() {
init();
</script>

<div id="graph-component" class="graph" x-data="{ showFullGraph: false }">
<div id="graph-component" class="graph" x-data="{ showFullGraph: false, graphDepth: 1 }">
<div class="graph-title-container">
<div class="graph-title">Connected Pages</div>
<div id="graph-controls">
{% if settings.dgShowGraphDepthControl %}
<div class="ctrl-left">
<label id="graph-depth-label" for="graph-depth-slider">Depth <span x-text="graphDepth"></span></label>
<input type="range" id="graph-depth-slider" min="1" max="3" step="1" x-model.number="graphDepth" x-on:input="window.__graph.setLocalGraphDepth(graphDepth)" />
</div>
{% endif %}
<div class="ctrl-right">
<span id="global-graph-btn" x-on:click="showFullGraph = true; window.__graph.openFullGraph()"><i data-lucide="globe" aria-hidden="true"></i></span>
<span id="graph-fs-btn" x-on:click="showFullGraph = true; window.__graph.openExpandedLocal()"><i data-lucide="expand" aria-hidden="true"></i></span>
Expand Down
16 changes: 16 additions & 0 deletions src/site/styles/digital-garden-base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading