From 2792e865460ba94903ae204ccc7ddaa5bbe618b2 Mon Sep 17 00:00:00 2001 From: Daniel LaForce Date: Sat, 26 Apr 2025 22:14:27 -0600 Subject: [PATCH] refactor(MiniGraph): Improve data accuracy and fullscreen interactions --- src/components/MiniGraph.astro | 505 ++++++++++++++++++--------------- 1 file changed, 278 insertions(+), 227 deletions(-) diff --git a/src/components/MiniGraph.astro b/src/components/MiniGraph.astro index bf1b5ca..618d0a4 100644 --- a/src/components/MiniGraph.astro +++ b/src/components/MiniGraph.astro @@ -30,6 +30,9 @@ const relatedPostsTags = relatedPosts .flatMap(post => post.data.tags || []) .filter(tag => !tags.includes(tag)); // Exclude current post tags to avoid duplicates +// Create a set of all Level 1 nodes' tags for filtering Level 2 tags +const level1TagsSet = new Set([...tags, ...relatedPostsTags]); + // Get Level 2 posts: posts related to Level 1 tags (excluding current post and Level 1 posts) const level2PostIds = new Set(); const level2Posts = []; @@ -38,21 +41,22 @@ const level2Posts = []; tags.forEach(tag => { allPosts.forEach(post => { // Skip if post is current post or already in related posts - if (post.slug === slug || relatedPosts.some(rp => rp.slug === post.slug)) { + if (post.slug === slug || relatedPosts.some(rp => rp.slug === post.slug) || level2PostIds.has(post.slug)) { return; } - // If post has the tag and isn't already added - if (post.data.tags?.includes(tag) && !level2PostIds.has(post.slug)) { + // If post has the tag, add it to Level 2 + if (post.data.tags?.includes(tag)) { level2PostIds.add(post.slug); level2Posts.push(post); } }); }); -// Get Level 2 tags from Level 2 posts +// Only collect Level 2 tags that are directly linked to Level 1 posts +// This fixes the issue where unrelated Level 2 tags were being included const level2Tags = new Set(); -level2Posts.forEach(post => { +relatedPosts.forEach(post => { (post.data.tags || []).forEach(tag => { // Only add if not already in Level 0 or Level 1 tags if (!tags.includes(tag) && !relatedPostsTags.includes(tag)) { @@ -70,14 +74,16 @@ const nodes = [ type: "post", level: 0, category: category, - tags: tags + tags: tags, + url: `/posts/${slug}/` }, // Level 1: Tag nodes ...tags.map(tag => ({ id: `tag-${tag}`, label: tag, type: "tag", - level: 1 + level: 1, + url: `/tag/${tag}/` })), // Level 1: Related post nodes ...relatedPosts.map(post => ({ @@ -86,30 +92,35 @@ const nodes = [ type: "post", level: 1, category: post.data.category || "Uncategorized", - tags: post.data.tags || [] + tags: post.data.tags || [], + url: `/posts/${post.slug}/` })), - // Level 2: Related tags nodes + // Level 2: Related tags nodes (Tags from Level 1 posts) ...relatedPostsTags.map(tag => ({ id: `tag-${tag}`, label: tag, type: "tag", - level: 2 + level: 2, + url: `/tag/${tag}/` })), - // Level 2: Posts related to tags + // Level 2: Posts related to tags (Posts connected to Level 1 tags) ...level2Posts.map(post => ({ id: post.slug, label: post.data.title, type: "post", level: 2, category: post.data.category || "Uncategorized", - tags: post.data.tags || [] + tags: post.data.tags || [], + url: `/posts/${post.slug}/` })), - // Level 2: Tags from Level 2 posts + // Level 2: Tags from Level 1 posts (only tags directly connected to Level 1 posts) + // This was the corrected logic for level2Tags Set ...[...level2Tags].map(tag => ({ id: `tag-${tag}`, label: tag.toString(), type: "tag", - level: 2 + level: 2, + url: `/tag/${tag.toString()}/` })) ]; @@ -132,7 +143,7 @@ const edges = [ (post.data.tags || []).map(tag => ({ source: post.slug, target: `tag-${tag}`, - type: "post-tag" + type: "post-tag" // Re-using post-tag type for simplicity })) ), // Level 1 to Level 2: Tags to related posts @@ -140,11 +151,12 @@ const edges = [ tags.filter(tag => post.data.tags?.includes(tag)).map(tag => ({ source: `tag-${tag}`, target: post.slug, - type: "tag-post" + type: "tag-post" // New type for tag -> post connection })) ) ]; + // Prepare graph data object const graphData = { nodes, edges }; @@ -162,7 +174,7 @@ const predefinedColors = {

Post Connections

- +
- +
- +
@@ -207,29 +219,29 @@ const predefinedColors = {
- +
Type: Post
- +
Category: Category
- +
Tags:
- +
Connections:
    - - View Content + + View Content
    @@ -258,7 +270,7 @@ const predefinedColors = { border: 1px solid var(--card-border, rgba(56, 189, 248, 0.2)); background: rgba(15, 23, 42, 0.2); } - + /* Fullscreen toggle button */ .fullscreen-toggle { position: absolute; @@ -277,22 +289,28 @@ const predefinedColors = { z-index: 2; transition: all 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + opacity: 0; /* Hidden by default, shown on hover */ } - + + .knowledge-graph-wrapper:hover .fullscreen-toggle { + opacity: 0.7; /* Show on hover */ + } + .fullscreen-toggle:hover { + opacity: 1; background: rgba(30, 41, 59, 0.9); transform: translateY(-2px); } - + .fullscreen-icon { width: 16px; height: 16px; } - + .hidden { display: none; } - + /* Fullscreen container */ .fullscreen-container { display: none; /* Hidden by default */ @@ -306,11 +324,11 @@ const predefinedColors = { flex-direction: column; overflow: hidden; } - + .fullscreen-container.active { display: flex; } - + .fullscreen-header { display: flex; justify-content: space-between; @@ -320,13 +338,13 @@ const predefinedColors = { background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(8px); } - + .fullscreen-title { font-size: 1.25rem; font-weight: 600; color: var(--text-primary, #e2e8f0); } - + .exit-fullscreen { width: 36px; height: 36px; @@ -340,26 +358,26 @@ const predefinedColors = { cursor: pointer; transition: all 0.2s ease; } - + .exit-fullscreen:hover { background: rgba(30, 41, 59, 0.9); } - + .fullscreen-content { flex: 1; display: flex; overflow: hidden; } - + .fullscreen-graph { flex: 1; height: 100%; border-right: 1px solid var(--border-primary, rgba(56, 189, 248, 0.2)); } - + /* Info panel */ .info-panel { - width: 0; + width: 0; /* Hidden by default */ height: 100%; background: var(--bg-secondary, #1e293b); transition: width 0.3s ease; @@ -367,11 +385,11 @@ const predefinedColors = { display: flex; flex-direction: column; } - + .info-panel.active { - width: 350px; + width: 350px; /* Show panel when active */ } - + .info-panel-header { display: flex; justify-content: space-between; @@ -379,14 +397,14 @@ const predefinedColors = { padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-primary, rgba(56, 189, 248, 0.1)); } - + .info-title { font-size: 1.2rem; margin: 0; color: var(--text-primary, #e2e8f0); font-weight: 600; } - + .close-info { background: none; border: none; @@ -399,29 +417,29 @@ const predefinedColors = { justify-content: center; transition: all 0.2s ease; } - + .close-info:hover { color: var(--text-primary, #e2e8f0); background: rgba(255, 255, 255, 0.1); } - + .info-content { padding: 1.5rem; overflow-y: auto; flex: 1; } - + .info-type, .info-category, .info-tags, .info-connections { margin-bottom: 1.25rem; } - + .info-label { display: block; color: var(--text-secondary, #94a3b8); font-size: 0.85rem; margin-bottom: 0.5rem; } - + .type-value { display: inline-block; padding: 0.25rem 0.75rem; @@ -429,30 +447,30 @@ const predefinedColors = { font-size: 0.85rem; font-weight: 500; } - + .post-type { background-color: rgba(59, 130, 246, 0.15); color: #3B82F6; } - + .tag-type { background-color: rgba(16, 185, 129, 0.15); color: #10B981; } - + .category-value { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.85rem; } - + .tags-container { display: flex; flex-wrap: wrap; gap: 0.5rem; } - + .tag { background: rgba(16, 185, 129, 0.1); color: #10B981; @@ -462,12 +480,12 @@ const predefinedColors = { cursor: pointer; transition: all 0.2s ease; } - + .tag:hover { background: rgba(16, 185, 129, 0.2); transform: translateY(-2px); } - + .connections-list { padding-left: 0; list-style: none; @@ -475,24 +493,25 @@ const predefinedColors = { max-height: 150px; overflow-y: auto; } - + .connections-list li { color: var(--text-secondary, #94a3b8); font-size: 0.9rem; margin-bottom: 0.5rem; } - + .connections-list a { color: var(--accent-primary, #38bdf8); text-decoration: none; transition: color 0.2s ease; + cursor: pointer; } - + .connections-list a:hover { color: var(--accent-secondary, #06b6d4); text-decoration: underline; } - + .info-link { display: block; background: linear-gradient(90deg, var(--accent-primary, #38bdf8), var(--accent-secondary, #06b6d4)); @@ -506,32 +525,32 @@ const predefinedColors = { box-shadow: 0 4px 10px rgba(6, 182, 212, 0.2); margin-top: 1rem; } - + .info-link:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(6, 182, 212, 0.3); } - + /* Media queries for mobile responsiveness */ @media screen and (max-width: 768px) { .fullscreen-content { flex-direction: column; } - + .fullscreen-graph { height: 60%; border-right: none; border-bottom: 1px solid var(--border-primary, rgba(56, 189, 248, 0.2)); } - + .info-panel { width: 100%; - height: 0; + height: 0; /* Start hidden */ } - + .info-panel.active { width: 100%; - height: 40%; + height: 40%; /* Take remaining space */ } } @@ -578,13 +597,14 @@ const predefinedColors = { const fullscreenExitIcon = document.getElementById(`${graphId}-fullscreen-exit`); const infoPanel = document.getElementById(`${graphId}-info-panel`); const closeInfoBtn = document.getElementById(`${graphId}-close-info`); - + // State variables let isFullscreen = false; let cy = null; // Mini graph instance let cyFullscreen = null; // Fullscreen graph instance let originalStyles = {}; // To restore page layout after exiting fullscreen - + let selectedNode = null; // Currently selected node for fullscreen interactions + try { // Check if we have any nodes if (graphData.nodes.length === 0) { @@ -595,22 +615,22 @@ const predefinedColors = { // Generate node colors based on type and category const nodeElements = graphData.nodes.map(node => { let nodeColor = '#3B82F6'; // Default blue for posts - + if (node.type === 'tag') { nodeColor = '#10B981'; // Green for tags } else if (node.type === 'post' && node.category) { // Use category color for posts if available nodeColor = predefinedColors[node.category] || '#3B82F6'; } - + // Adjust opacity based on level - const levelOpacity = node.level === 0 ? 1 : + const levelOpacity = node.level === 0 ? 1 : node.level === 1 ? 0.85 : 0.7; - + // Calculate node size based on level - const nodeSize = node.level === 0 ? 30 : + const nodeSize = node.level === 0 ? 30 : node.level === 1 ? 22 : 16; - + return { data: { id: node.id, @@ -621,11 +641,12 @@ const predefinedColors = { tags: node.tags || [], color: nodeColor, opacity: levelOpacity, - size: nodeSize + size: nodeSize, + url: node.url || '#' } }; }); - + // Generate edge elements const edgeElements = graphData.edges.map((edge, index) => ({ data: { @@ -635,10 +656,10 @@ const predefinedColors = { type: edge.type } })); - + // Combine nodes and edges const elements = [...nodeElements, ...edgeElements]; - + // Define common style array to be used by both graph instances const graphStyles = [ // Base node style @@ -754,7 +775,7 @@ const predefinedColors = { } } ]; - + // Initialize mini graph with cose layout cy = cytoscape({ container, @@ -783,115 +804,138 @@ const predefinedColors = { maxZoom: 2, wheelSensitivity: 0.2 }); - + // Fit graph to container cy.fit(undefined, 20); - + // Add event listeners cy.on('tap', 'node', function(evt) { const node = evt.target; highlightNode(node, cy); + // Navigate on click in mini-graph + const url = node.data('url'); + if (url && url !== '#') { + window.location.href = url; + } }); - + // Add fullscreen toggle functionality if the button exists if (fullscreenToggle && fullscreenContainer) { fullscreenToggle.addEventListener('click', function() { enterFullscreenMode(); }); } - + // Exit fullscreen when button is clicked if (exitFullscreenBtn) { exitFullscreenBtn.addEventListener('click', function() { exitFullscreenMode(); }); } - + // Close info panel when close button is clicked if (closeInfoBtn && infoPanel) { closeInfoBtn.addEventListener('click', function() { infoPanel.classList.remove('active'); }); } - + // Toggle fullscreen mode by keyboard ESC document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && isFullscreen) { exitFullscreenMode(); } }); - + // Function to enter fullscreen mode function enterFullscreenMode() { if (isFullscreen) return; - + isFullscreen = true; - + // Show fullscreen container fullscreenContainer.classList.add('active'); - + // Update fullscreen toggle icons fullscreenEnterIcon.classList.add('hidden'); fullscreenExitIcon.classList.remove('hidden'); - + // Store original styles of important page elements to restore later saveOriginalStyles(); - + // Initialize fullscreen graph if not already done if (!cyFullscreen && fullscreenGraph) { initFullscreenGraph(); + } else if (cyFullscreen) { + // If already initialized, just resize and fit + setTimeout(() => { + cyFullscreen.resize(); + cyFullscreen.fit(undefined, 30); + cyFullscreen.center(); + }, 50); // Small delay for transition } - + // Prevent body scroll document.body.style.overflow = 'hidden'; } - + // Function to exit fullscreen mode function exitFullscreenMode() { if (!isFullscreen) return; - + isFullscreen = false; - + // Hide fullscreen container fullscreenContainer.classList.remove('active'); - + // Update fullscreen toggle icons fullscreenEnterIcon.classList.remove('hidden'); fullscreenExitIcon.classList.add('hidden'); - + // Close info panel if (infoPanel) { infoPanel.classList.remove('active'); } - + // Restore original page layout restoreOriginalStyles(); - + // Allow body scroll again document.body.style.overflow = ''; + + // Resize mini graph after exit + setTimeout(() => { + if (cy) { + cy.resize(); + cy.fit(undefined, 20); + } + }, 50); } - + // Save original styles of page elements before entering fullscreen function saveOriginalStyles() { originalStyles = { bodyOverflow: document.body.style.overflow, // Add other elements that need to be restored later }; - + // Check for sidebar, content containers, etc. and save their styles - const sidebar = document.querySelector('.sidebar, aside, nav'); - const mainContent = document.querySelector('main, .content, article'); - + const sidebar = document.querySelector('.post-sidebar'); // Use specific class + const mainContent = document.querySelector('.post-main-column'); // Use specific class + if (sidebar) { originalStyles.sidebar = { element: sidebar, display: sidebar.style.display, visibility: sidebar.style.visibility, position: sidebar.style.position, - zIndex: sidebar.style.zIndex + zIndex: sidebar.style.zIndex, + width: sidebar.style.width, // Save width too + flex: sidebar.style.flex, + gridColumn: sidebar.style.gridColumn }; } - + if (mainContent) { originalStyles.mainContent = { element: mainContent, @@ -901,33 +945,40 @@ const predefinedColors = { }; } } - + // Restore original styles when exiting fullscreen function restoreOriginalStyles() { document.body.style.overflow = originalStyles.bodyOverflow || ''; - + // Restore sidebar if it exists if (originalStyles.sidebar && originalStyles.sidebar.element) { const sidebar = originalStyles.sidebar.element; - sidebar.style.display = originalStyles.sidebar.display; - sidebar.style.visibility = originalStyles.sidebar.visibility; - sidebar.style.position = originalStyles.sidebar.position; - sidebar.style.zIndex = originalStyles.sidebar.zIndex; + Object.assign(sidebar.style, originalStyles.sidebar); // Restore all saved styles + sidebar.style.removeProperty('position'); // Ensure position is reset if it was static } - + // Restore main content if it exists if (originalStyles.mainContent && originalStyles.mainContent.element) { const mainContent = originalStyles.mainContent.element; - mainContent.style.marginLeft = originalStyles.mainContent.marginLeft; - mainContent.style.width = originalStyles.mainContent.width; - mainContent.style.maxWidth = originalStyles.mainContent.maxWidth; + Object.assign(mainContent.style, originalStyles.mainContent); } + + // Force reflow to ensure styles apply correctly + setTimeout(() => { + const contentArea = document.querySelector('.post-content'); + if (contentArea) { + const display = contentArea.style.display; + contentArea.style.display = 'none'; + void contentArea.offsetHeight; // Trigger reflow + contentArea.style.display = display; + } + }, 0); } - + // Initialize the fullscreen graph function initFullscreenGraph() { if (!fullscreenGraph) return; - + // Create a new cytoscape instance for fullscreen with the same data cyFullscreen = cytoscape({ container: fullscreenGraph, @@ -956,34 +1007,36 @@ const predefinedColors = { maxZoom: 3, wheelSensitivity: 0.3 }); - + // Fit graph to container cyFullscreen.fit(undefined, 30); - + // Add fullscreen interactions addFullscreenInteractions(); } - + // Add interactions for fullscreen mode function addFullscreenInteractions() { // Click on node to show info cyFullscreen.on('tap', 'node', function(evt) { const node = evt.target; + selectedNode = node; // Store the selected node const nodeData = node.data(); - + // Highlight the selected node highlightNode(node, cyFullscreen); - + // Show info panel showNodeInfo(nodeData); }); - + // Click on background to reset cyFullscreen.on('tap', function(evt) { if (evt.target === cyFullscreen) { // Remove highlights cyFullscreen.elements().removeClass('highlighted faded'); - + selectedNode = null; + // Hide info panel if (infoPanel) { infoPanel.classList.remove('active'); @@ -991,129 +1044,127 @@ const predefinedColors = { } }); } - + // Highlight a node and its connections function highlightNode(node, cyInstance) { // Remove previous highlights cyInstance.elements().removeClass('highlighted faded'); - + // Highlight selected node and its neighborhood node.addClass('highlighted'); node.neighborhood().addClass('highlighted'); - + // Fade the rest cyInstance.elements().difference(node.neighborhood().union(node)).addClass('faded'); } - + // Show node info in the panel function showNodeInfo(nodeData) { if (!infoPanel) return; - - // Set node title - document.getElementById(`${graphId}-info-title`).textContent = nodeData.label; - - // Set node type - const typeEl = document.getElementById(`${graphId}-info-type`); - typeEl.textContent = nodeData.type.charAt(0).toUpperCase() + nodeData.type.slice(1); - typeEl.className = `info-value type-value ${nodeData.type}-type`; - - // Set category if it's a post - const categoryContainer = document.getElementById(`${graphId}-info-category-container`); + + // Get elements inside the panel + const infoTitleEl = document.getElementById(`${graphId}-info-title`); + const infoTypeEl = document.getElementById(`${graphId}-info-type`); + const categoryContainerEl = document.getElementById(`${graphId}-info-category-container`); const categoryEl = document.getElementById(`${graphId}-info-category`); - - if (nodeData.type === 'post' && nodeData.category) { - categoryContainer.style.display = 'block'; - categoryEl.textContent = nodeData.category; - - // Use category colors - const catColor = predefinedColors[nodeData.category] || '#A0AEC0'; - categoryEl.style.backgroundColor = `${catColor}33`; // Add alpha - categoryEl.style.color = catColor; - } else { - categoryContainer.style.display = 'none'; - } - - // Set tags - const tagsContainer = document.getElementById(`${graphId}-info-tags-container`); + const tagsContainerEl = document.getElementById(`${graphId}-info-tags-container`); const tagsEl = document.getElementById(`${graphId}-info-tags`); - - if (nodeData.type === 'post' && nodeData.tags && nodeData.tags.length > 0) { - tagsContainer.style.display = 'block'; - tagsEl.innerHTML = ''; - - nodeData.tags.forEach(tag => { - const tagEl = document.createElement('span'); - tagEl.className = 'tag'; - tagEl.textContent = tag; - tagEl.addEventListener('click', () => { - // Try to find and highlight the tag node - const tagNode = cyFullscreen.getElementById(`tag-${tag}`); - if (tagNode.length > 0) { - highlightNode(tagNode, cyFullscreen); - showNodeInfo(tagNode.data()); - } - }); - tagsEl.appendChild(tagEl); - }); - } else { - tagsContainer.style.display = 'none'; - } - - // Set connections const connectionsEl = document.getElementById(`${graphId}-info-connections`); - connectionsEl.innerHTML = ''; - - const neighbors = cyFullscreen.getElementById(nodeData.id).neighborhood('node'); - if (neighbors.length > 0) { - neighbors.forEach(neighbor => { - const neighborData = neighbor.data(); - const li = document.createElement('li'); - const a = document.createElement('a'); - a.href = '#'; - a.textContent = neighborData.label; - a.addEventListener('click', (e) => { - e.preventDefault(); - highlightNode(neighbor, cyFullscreen); - showNodeInfo(neighborData); - }); - li.appendChild(a); - connectionsEl.appendChild(li); - }); - } else { - const li = document.createElement('li'); - li.textContent = 'No connections'; - connectionsEl.appendChild(li); - } - - // Set link based on node type const linkEl = document.getElementById(`${graphId}-info-link`); - if (nodeData.type === 'post') { - linkEl.textContent = 'Read Post'; - linkEl.href = `/posts/${nodeData.id}/`; - } else if (nodeData.type === 'tag') { - linkEl.textContent = 'View Tag'; - linkEl.href = `/tag/${nodeData.label}/`; - } else { - linkEl.textContent = 'View Content'; - linkEl.href = '#'; + + // Set node title + if (infoTitleEl) infoTitleEl.textContent = nodeData.label; + + // Set node type + if (infoTypeEl) { + infoTypeEl.textContent = nodeData.type.charAt(0).toUpperCase() + nodeData.type.slice(1); + infoTypeEl.className = `info-value type-value ${nodeData.type}-type`; } - + + // Set category if it's a post + if (categoryContainerEl && categoryEl) { + if (nodeData.type === 'post' && nodeData.category) { + categoryContainerEl.style.display = 'block'; + categoryEl.textContent = nodeData.category; + const catColor = predefinedColors[nodeData.category] || '#A0AEC0'; + categoryEl.style.backgroundColor = `${catColor}33`; + categoryEl.style.color = catColor; + } else { + categoryContainerEl.style.display = 'none'; + } + } + + // Set tags + if (tagsContainerEl && tagsEl) { + if (nodeData.tags && nodeData.tags.length > 0) { + tagsContainerEl.style.display = 'block'; + tagsEl.innerHTML = ''; + nodeData.tags.forEach(tag => { + const tagEl = document.createElement('span'); + tagEl.className = 'tag'; + tagEl.textContent = tag; + tagEl.addEventListener('click', () => { + const tagNode = cyFullscreen.getElementById(`tag-${tag}`); + if (tagNode.length > 0) { + selectedNode = tagNode; // Update selected node + highlightNode(tagNode, cyFullscreen); + showNodeInfo(tagNode.data()); + } + }); + tagsEl.appendChild(tagEl); + }); + } else { + tagsContainerEl.style.display = 'none'; + } + } + + // Set connections - now with interactive links + if (connectionsEl) { + connectionsEl.innerHTML = ''; + const currentNode = cyFullscreen.getElementById(nodeData.id); // Get current node from fullscreen instance + const neighbors = currentNode.neighborhood('node'); + + if (neighbors.length > 0) { + neighbors.forEach(neighbor => { + const neighborData = neighbor.data(); + const li = document.createElement('li'); + const a = document.createElement('a'); + a.href = '#'; + a.textContent = neighborData.label; + a.addEventListener('click', (e) => { + e.preventDefault(); + selectedNode = neighbor; // Update selected node + highlightNode(neighbor, cyFullscreen); + showNodeInfo(neighborData); + }); + li.appendChild(a); + connectionsEl.appendChild(li); + }); + } else { + const li = document.createElement('li'); + li.textContent = 'No connections'; + connectionsEl.appendChild(li); + } + } + + // Set link based on node type - dynamic text and URL + if (linkEl) { + if (nodeData.type === 'post') { + linkEl.textContent = 'Read Post'; + linkEl.href = nodeData.url || `/posts/${nodeData.id}/`; + } else if (nodeData.type === 'tag') { + linkEl.textContent = 'View Tag'; + linkEl.href = nodeData.url || `/tag/${nodeData.label}/`; + } else { + linkEl.textContent = 'View Content'; + linkEl.href = nodeData.url || '#'; + } + } + // Show the panel infoPanel.classList.add('active'); } - - // Make nodes clickable in mini graph - cy.on('tap', 'node[type="tag"]', function(evt) { - const node = evt.target; - const tagName = node.data('label'); - window.location.href = `/tag/${tagName}`; - }); - cy.on('tap', 'node[type="post"][level!=0]', function(evt) { - const node = evt.target; - const postSlug = node.id(); - window.location.href = `/posts/${postSlug}/`; - }); } catch (error) { console.error('[MiniGraph] Error initializing graph:', error); container.innerHTML = '
    Error loading graph
    ';