refactor: Update KnowledgeGraph and BaseLayout

This commit is contained in:
Daniel LaForce 2025-04-23 13:13:16 -06:00
parent d10f87e899
commit b392c30aba
2 changed files with 27 additions and 37 deletions

View File

@ -2,9 +2,6 @@
// src/components/KnowledgeGraph.astro // src/components/KnowledgeGraph.astro
// Interactive visualization of content connections using Cytoscape.js // Interactive visualization of content connections using Cytoscape.js
// Assuming Cytoscape is loaded via CDN in BaseLayout or globally
// If not, you might need: import cytoscape from 'cytoscape';
export interface GraphNode { export interface GraphNode {
id: string; id: string;
label: string; label: string;
@ -34,7 +31,7 @@ const { graphData, height = "60vh" } = Astro.props;
// Generate colors based on categories for nodes // Generate colors based on categories for nodes
const uniqueCategories = [...new Set(graphData.nodes.map(node => node.category || 'Uncategorized'))]; const uniqueCategories = [...new Set(graphData.nodes.map(node => node.category || 'Uncategorized'))];
const categoryColors = {}; const categoryColors = {};
const predefinedColors = { /* Colors from previous step */ const predefinedColors = {
'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61', 'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981', 'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1', 'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
@ -67,8 +64,11 @@ graphData.nodes.forEach(node => {
}); });
--- ---
<!-- Include Cytoscape via CDN with is:inline to ensure it loads before the script runs -->
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
<div class="graph-wrapper" style={`--graph-height: ${height};`}> <div class="graph-wrapper" style={`--graph-height: ${height};`}>
{/* Loading Animation */} <!-- Loading Animation -->
<div id="graph-loading" class="graph-loading"> <div id="graph-loading" class="graph-loading">
<div class="loading-spinner"> <div class="loading-spinner">
<div class="spinner-ring"></div> <div class="spinner-ring"></div>
@ -78,10 +78,10 @@ graphData.nodes.forEach(node => {
<div class="loading-text">Initializing Knowledge Graph...</div> <div class="loading-text">Initializing Knowledge Graph...</div>
</div> </div>
{/* Cytoscape Container */} <!-- Cytoscape Container -->
<div id="knowledge-graph" class="graph-container"></div> <div id="knowledge-graph" class="graph-container"></div>
{/* Node Details Panel */} <!-- Node Details Panel -->
<div id="node-details" class="node-details"> <div id="node-details" class="node-details">
<div class="node-details-header"> <div class="node-details-header">
<h3 id="node-title" class="node-title">Node Title</h3> <h3 id="node-title" class="node-title">Node Title</h3>
@ -96,19 +96,19 @@ graphData.nodes.forEach(node => {
<div id="node-tags" class="node-tags"> <div id="node-tags" class="node-tags">
<span class="tags-label">Tags:</span> <span class="tags-label">Tags:</span>
<div class="tags-container"> <div class="tags-container">
{/* Tags populated by JS */} <!-- Tags populated by JS -->
</div> </div>
</div> </div>
<div id="node-connections" class="node-connections"> <div id="node-connections" class="node-connections">
<span class="connections-label">Connections:</span> <span class="connections-label">Connections:</span>
<ul class="connections-list"> <ul class="connections-list">
{/* Connections populated by JS */} <!-- Connections populated by JS -->
</ul> </ul>
</div> </div>
<a href="#" id="node-link" class="node-link" target="_blank" rel="noopener noreferrer">Read Article</a> <a href="#" id="node-link" class="node-link" target="_blank" rel="noopener noreferrer">Read Article</a>
</div> </div>
{/* Graph Controls */} <!-- Graph Controls -->
<div class="graph-controls"> <div class="graph-controls">
<div class="graph-filters"> <div class="graph-filters">
<button class="graph-filter active" data-filter="all" style="--filter-color: var(--accent-primary);">All Topics</button> <button class="graph-filter active" data-filter="all" style="--filter-color: var(--accent-primary);">All Topics</button>
@ -129,7 +129,7 @@ graphData.nodes.forEach(node => {
</div> </div>
</div> </div>
{/* Graph Legend */} <!-- Graph Legend -->
<details class="graph-legend"> <details class="graph-legend">
<summary class="legend-title">Legend</summary> <summary class="legend-title">Legend</summary>
<div class="legend-items"> <div class="legend-items">
@ -141,18 +141,14 @@ graphData.nodes.forEach(node => {
))} ))}
</div> </div>
</details> </details>
</div> </div>
{/* Include Cytoscape via CDN - Ensure this is loaded, perhaps in BaseLayout */}
{/* <script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js"></script> */}
<script define:vars={{ graphData, categoryColors, nodeSizes }}> <script define:vars={{ graphData, categoryColors, nodeSizes }}>
// Initialize the graph when the DOM is ready // Initialize the graph when the DOM is ready
function initializeGraph() { function initializeGraph() {
// Check if Cytoscape is loaded // Check if Cytoscape is loaded
if (typeof cytoscape === 'undefined') { if (typeof cytoscape === 'undefined') {
console.error("Cytoscape library not loaded. Make sure it's included (e.g., via CDN in BaseLayout)."); console.error("Cytoscape library not loaded. Make sure it's included via the script tag.");
const loadingEl = document.getElementById('graph-loading'); const loadingEl = document.getElementById('graph-loading');
if(loadingEl) loadingEl.innerHTML = "<p>Error: Cytoscape library not loaded.</p>"; if(loadingEl) loadingEl.innerHTML = "<p>Error: Cytoscape library not loaded.</p>";
return; return;
@ -202,7 +198,7 @@ graphData.nodes.forEach(node => {
const cy = cytoscape({ const cy = cytoscape({
container: graphContainer, container: graphContainer,
elements: elements, elements: elements,
style: [ /* Styles from your snippet */ style: [
{ selector: 'node', style: { 'background-color': 'data(color)', 'label': 'data(label)', 'width': 'data(size)', 'height': 'data(size)', 'font-size': '10px', 'color': '#E2E8F0', 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': '7px', 'text-background-opacity': 0.7, 'text-background-color': '#0F1219', 'text-background-padding': '3px', 'text-background-shape': 'roundrectangle', 'text-max-width': '120px', 'text-wrap': 'ellipsis', 'text-overflow-wrap': 'anywhere', 'border-width': '2px', 'border-color': '#0F1219', 'border-opacity': 0.8, 'z-index': 10, 'text-outline-width': 1, 'text-outline-color': '#000', 'text-outline-opacity': 0.5 } }, { selector: 'node', style: { 'background-color': 'data(color)', 'label': 'data(label)', 'width': 'data(size)', 'height': 'data(size)', 'font-size': '10px', 'color': '#E2E8F0', 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': '7px', 'text-background-opacity': 0.7, 'text-background-color': '#0F1219', 'text-background-padding': '3px', 'text-background-shape': 'roundrectangle', 'text-max-width': '120px', 'text-wrap': 'ellipsis', 'text-overflow-wrap': 'anywhere', 'border-width': '2px', 'border-color': '#0F1219', 'border-opacity': 0.8, 'z-index': 10, 'text-outline-width': 1, 'text-outline-color': '#000', 'text-outline-opacity': 0.5 } },
{ selector: 'edge', style: { 'width': 'mapData(weight, 1, 10, 1, 4)', 'line-color': 'rgba(226, 232, 240, 0.2)', 'curve-style': 'bezier', 'opacity': 0.6, 'z-index': 1 } }, { selector: 'edge', style: { 'width': 'mapData(weight, 1, 10, 1, 4)', 'line-color': 'rgba(226, 232, 240, 0.2)', 'curve-style': 'bezier', 'opacity': 0.6, 'z-index': 1 } },
{ selector: '.highlighted', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '3px', 'color': '#FFFFFF', 'text-background-opacity': 0.9, 'opacity': 1, 'z-index': 20 } }, { selector: '.highlighted', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '3px', 'color': '#FFFFFF', 'text-background-opacity': 0.9, 'opacity': 1, 'z-index': 20 } },
@ -227,7 +223,7 @@ graphData.nodes.forEach(node => {
clearTimeout(hoverTimeout); clearTimeout(hoverTimeout);
node.addClass('highlighted'); node.addClass('highlighted');
node.connectedEdges().addClass('highlighted'); node.connectedEdges().addClass('highlighted');
graphContainer.style.cursor = 'pointer'; // Use graphContainer graphContainer.style.cursor = 'pointer';
}); });
cy.on('mouseout', 'node', function(e) { cy.on('mouseout', 'node', function(e) {
@ -238,7 +234,7 @@ graphData.nodes.forEach(node => {
node.connectedEdges().removeClass('highlighted'); node.connectedEdges().removeClass('highlighted');
} }
}, 100); }, 100);
graphContainer.style.cursor = 'default'; // Use graphContainer graphContainer.style.cursor = 'default';
}); });
cy.on('tap', 'node', function(e) { cy.on('tap', 'node', function(e) {
@ -282,7 +278,7 @@ graphData.nodes.forEach(node => {
evt.preventDefault(); evt.preventDefault();
cy.$(':selected').unselect(); cy.$(':selected').unselect();
const targetNode = cy.getElementById(connectedData.id); const targetNode = cy.getElementById(connectedData.id);
if (targetNode) { // Check if node exists if (targetNode) {
targetNode.select(); targetNode.select();
cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 }); cy.animate({ center: { eles: targetNode }, zoom: cy.zoom() }, { duration: 300 });
targetNode.trigger('tap'); targetNode.trigger('tap');
@ -345,7 +341,7 @@ graphData.nodes.forEach(node => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
const category = item.dataset.category; const category = item.dataset.category;
const filterButton = document.querySelector(`.graph-filter[data-filter="${category}"]`); const filterButton = document.querySelector(`.graph-filter[data-filter="${category}"]`);
filterButton?.click(); if (filterButton) filterButton.click();
}); });
}); });
@ -355,10 +351,11 @@ graphData.nodes.forEach(node => {
document.getElementById('reset-graph')?.addEventListener('click', () => { document.getElementById('reset-graph')?.addEventListener('click', () => {
cy.fit(null, 30); cy.fit(null, 30);
cy.elements().removeClass('faded highlighted filtered'); cy.elements().removeClass('faded highlighted filtered');
document.querySelector('.graph-filter[data-filter="all"]')?.click(); const allFilterButton = document.querySelector('.graph-filter[data-filter="all"]');
if (allFilterButton) allFilterButton.click();
}); });
// Add mouse wheel zoom controls (already present in original script) // Add mouse wheel zoom controls
cy.on('zoom', function() { cy.on('zoom', function() {
if (cy.zoom() > 1.5) { if (cy.zoom() > 1.5) {
cy.style().selector('node').style({ 'text-max-width': '150px', 'font-size': '12px' }).update(); cy.style().selector('node').style({ 'text-max-width': '150px', 'font-size': '12px' }).update();
@ -435,5 +432,4 @@ graphData.nodes.forEach(node => {
.graph-legend { transform: translateX(-120%); transition: transform 0.3s ease; max-height: calc(100% - 40px); overflow-y: auto; } .graph-legend { transform: translateX(-120%); transition: transform 0.3s ease; max-height: calc(100% - 40px); overflow-y: auto; }
.graph-legend[open] { transform: translateX(0); } .graph-legend[open] { transform: translateX(0); }
.graph-controls { bottom: 10px; } .graph-controls { bottom: 10px; }
} }
</style>

View File

@ -26,7 +26,7 @@ const {
<!-- OpenGraph/Social Media Meta Tags --> <!-- OpenGraph/Social Media Meta Tags -->
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta property="og:image" content={Astro.site ? new URL(image, Astro.site).href : image} /> {/* Use absolute URL */} <meta property="og:image" content={Astro.site ? new URL(image, Astro.site).href : image} /> <!-- Use absolute URL -->
<meta property="og:url" content={Astro.url} /> <meta property="og:url" content={Astro.url} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
@ -34,7 +34,7 @@ const {
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content={title}> <meta name="twitter:title" content={title}>
<meta name="twitter:description" content={description}> <meta name="twitter:description" content={description}>
<meta name="twitter:image" content={Astro.site ? new URL(image, Astro.site).href : image}> {/* Use absolute URL */} <meta name="twitter:image" content={Astro.site ? new URL(image, Astro.site).href : image}> <!-- Use absolute URL -->
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@ -44,6 +44,9 @@ const {
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Cytoscape Library for Knowledge Graph -->
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
<!-- Schema.org markup for Google --> <!-- Schema.org markup for Google -->
<script type="application/ld+json"> <script type="application/ld+json">
{ {
@ -295,10 +298,6 @@ const {
border-top: 1px solid var(--border-primary); border-top: 1px solid var(--border-primary);
margin-top: 4rem; /* Add space above footer */ margin-top: 4rem; /* Add space above footer */
} }
/* Add other global styles from your external file if needed */
/* Or remove conflicting styles from the external file */
</style> </style>
</head> </head>
<body> <body>
@ -312,11 +311,10 @@ const {
<div class="floating-shape shape-3"></div> <div class="floating-shape shape-3"></div>
</div> </div>
{/* Use slots for Header and Footer */}
<slot name="header" /> <slot name="header" />
<main> <main>
<slot /> {/* Default slot for page content */} <slot /> <!-- Default slot for page content -->
</main> </main>
<slot name="footer" /> <slot name="footer" />
@ -345,10 +343,6 @@ const {
} else { } else {
console.warn("Element with class 'neural-nodes' not found."); console.warn("Element with class 'neural-nodes' not found.");
} }
// Terminal typing effect (if needed globally, otherwise keep in component)
// const typingElements = document.querySelectorAll('.terminal-typing');
// typingElements.forEach(typingElement => { ... });
}); });
</script> </script>
</body> </body>