Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <link href="https://fonts.googleapis.com/css2?family=Mandali&display=swap" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Varta:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Knowledge Graph Explorer</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #4a5568 0%, #2d3748 50%, #1a202c 100%); | |
| color: white; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| .container { | |
| display: flex; | |
| height: 100vh; | |
| } | |
| /* Sidebar Styles */ | |
| .sidebar { | |
| width: 400px; | |
| background: rgba(0 0 0 0.25); | |
| display: flex; | |
| flex-direction: column; | |
| transition: margin-left 0.3s ease; | |
| position: relative; | |
| z-index: 100; | |
| } | |
| .sidebar.collapsed { | |
| margin-left: -400px; | |
| } | |
| /* Header */ | |
| .sidebar-header { | |
| padding: 1.5rem; | |
| } | |
| .header-controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| /* Menu Toggle - Always Visible */ | |
| .menu-toggle { | |
| position: fixed; | |
| top: 1.5rem; | |
| left: 0.7rem; | |
| z-index: 300; | |
| background: none; | |
| border: none; | |
| color: white; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 0.75rem; | |
| border-radius: 8px; | |
| backdrop-filter: blur(10px); | |
| } | |
| .menu-toggle:hover { | |
| background: rgba(0, 0, 0, 0.8); | |
| } | |
| .home-btn { | |
| position: fixed; | |
| background: none; | |
| border: none; | |
| color: white; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 0.5rem; | |
| border-radius: 4px; | |
| transition: background-color 0.3s ease; | |
| } | |
| .home-btn:hover { | |
| background-color: rgba(0, 0, 0, 0.8); | |
| } | |
| .sidebar-title { | |
| font-size: 2.5rem; | |
| font-weight: 700; | |
| color: #F8F3E7; | |
| line-height: 1.2; | |
| margin-top : 50px; | |
| } | |
| /* Search Section */ | |
| .search-section { | |
| padding: 0 1.5rem 1.5rem; | |
| } | |
| .search-input { | |
| width: 100%; | |
| padding: 0.75rem 1rem; | |
| background: rgb(248 243 231); | |
| border-width: 2px; | |
| border-style: solid; | |
| border-color: #F3E7DD; | |
| border-radius: 15px; | |
| font-size: 0.9rem; | |
| color: #797979; | |
| margin-bottom: 1rem; | |
| } | |
| .search-input::placeholder { | |
| color: #a0aec0; | |
| } | |
| .search-input:focus { | |
| outline: none; | |
| background: rgba(255, 255, 255, 1); | |
| } | |
| .reset-btn { | |
| display: block; /* make it a block so margin works */ | |
| margin: 0 auto; /* this centers it horizontally */ | |
| background: rgb(110 131 131); | |
| border: none; | |
| color: white; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| max-width: 150px; | |
| width: 100%; | |
| box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2); | |
| transition: all 0.3s ease; | |
| } | |
| .reset-btn:hover { | |
| background: rgba(74, 85, 104, 1); | |
| } | |
| /* Instructions Panel */ | |
| .instructions-panel { | |
| margin: 1rem; | |
| background: rgb(248 243 231); | |
| border-radius: 15px; | |
| padding: 1.5rem; | |
| color: #4a5568; | |
| flex: 1; | |
| margin-bottom: 7rem; | |
| box-shadow: inset 0 4px 4px rgba(0,0,0,0.25); | |
| } | |
| .instructions-title { | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| font-family: 'Varta', sans-serif; | |
| color: #485656; | |
| margin-bottom: 1rem; | |
| text-align: center; | |
| } | |
| .instruction-item { | |
| margin-bottom: 1rem; | |
| font-family: 'Varta', sans-serif; | |
| font-size: 1rem; | |
| color: #485656; | |
| line-height: 1.5; | |
| font-weight: 300; | |
| } | |
| .instruction-item:last-child { | |
| margin-bottom: 0; | |
| } | |
| .instruction-action { | |
| font-weight: 700; | |
| color: #485656; | |
| } | |
| /* Main Graph Area */ | |
| .main-content { | |
| flex: 1; | |
| position: relative; | |
| background: rgb(77 83 109); | |
| } | |
| /* Home Button in Top-Right Corner */ | |
| .main-home-btn { | |
| position: absolute; | |
| top: 1.5rem; | |
| right: 1.5rem; | |
| z-index: 200; | |
| background: rgba(0, 0, 0, 0.6); | |
| border: none; | |
| color: white; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 0.75rem; | |
| border-radius: 8px; | |
| transition: background-color 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| } | |
| .main-home-btn:hover { | |
| background: rgba(0, 0, 0, 0.8); | |
| } | |
| /* Remove floating home - not needed anymore */ | |
| .floating-home { | |
| display: none; | |
| } | |
| /* Graph Styles */ | |
| #graph { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .node { | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.3)); | |
| } | |
| .node:hover { | |
| stroke-width: 3px; | |
| filter: drop-shadow(0 0 12px rgba(76, 175, 80, 0.6)); | |
| } | |
| .node.highlighted { | |
| stroke: #4CAF50 ; | |
| stroke-width: 3px ; | |
| filter: drop-shadow(0 0 15px rgba(76, 175, 80, 0.8)); | |
| } | |
| .node.selected { | |
| stroke: #FFD700 ; | |
| stroke-width: 4px ; | |
| filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8)); | |
| } | |
| .node.dimmed { | |
| opacity: 0.2; | |
| filter: none; | |
| } | |
| .link { | |
| stroke: rgba(255, 255, 255, 0.4); | |
| stroke-width: 2px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .link:hover { | |
| stroke: #4CAF50; | |
| stroke-width: 3px; | |
| filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.5)); | |
| } | |
| .link.highlighted { | |
| stroke: #4CAF50 ; | |
| stroke-width: 3px ; | |
| filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); | |
| } | |
| .link.dimmed { | |
| opacity: 0.1; | |
| } | |
| .node-label { | |
| font-size: 11px; | |
| font-weight: 600; | |
| fill: white; | |
| text-anchor: middle; | |
| pointer-events: none; | |
| text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); | |
| transition: all 0.3s ease; | |
| } | |
| .node-label.dimmed { | |
| opacity: 0.2; | |
| } | |
| .node-label.highlighted { | |
| fill: #4CAF50; | |
| font-size: 13px; | |
| text-shadow: 0 0 8px rgba(76, 175, 80, 0.8); | |
| } | |
| .tooltip { | |
| position: absolute; | |
| text-align: left; | |
| padding: 1rem; | |
| font-size: 0.9rem; | |
| background: rgba(0, 0, 0, 0.9); | |
| color: white; | |
| border-radius: 8px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| max-width: 300px; | |
| line-height: 1.5; | |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); | |
| z-index: 1000; | |
| } | |
| .tooltip h4 { | |
| margin: 0 0 0.5rem 0; | |
| color: #4CAF50; | |
| font-weight: 700; | |
| } | |
| .loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 1.1rem; | |
| color: white; | |
| text-align: center; | |
| } | |
| .loading-spinner { | |
| border: 3px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| border-top: 3px solid white; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 1rem; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .error { | |
| color: #ff6b6b; | |
| background: rgba(255, 107, 107, 0.1); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin: 1rem; | |
| border: 1px solid rgba(255, 107, 107, 0.3); | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| width: 100%; | |
| position: absolute; | |
| height: 100%; | |
| z-index: 1000; | |
| } | |
| .sidebar.collapsed { | |
| margin-left: -100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <!-- Fixed Menu Toggle Button --> | |
| <button class="menu-toggle" id="menuToggle">☰</button> | |
| <!-- Sidebar --> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <h1 class="sidebar-title">KNOWLEDGE<br>GRAPH</h1> | |
| </div> | |
| <div class="search-section"> | |
| <input | |
| type="text" | |
| class="search-input" | |
| id="searchInput" | |
| placeholder="Search nodes and relations..." | |
| > | |
| <button class="reset-btn" id="resetBtn">Reset Highlight</button> | |
| </div> | |
| <div class="instructions-panel"> | |
| <h3 class="instructions-title">HOW TO USE</h3> | |
| <div class="instruction-item"> | |
| <span class="instruction-action">Click a node</span> to highlight its connections | |
| </div> | |
| <div class="instruction-item"> | |
| <span class="instruction-action">Hover over nodes and edges</span> for details | |
| </div> | |
| <div class="instruction-item"> | |
| <span class="instruction-action">Drag nodes</span> to reposition them | |
| </div> | |
| <div class="instruction-item"> | |
| <span class="instruction-action">Zoom and pan</span> to explore the graph | |
| </div> | |
| <div class="instruction-item"> | |
| <span class="instruction-action">Search</span> to filter nodes and relations | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Graph Area --> | |
| <div class="main-content"> | |
| <!-- Home Button in Top-Right --> | |
| <button class="main-home-btn" id="mainHomeBtn"> | |
| <img src="Home.png" alt="Home" style="width: 20px; height: 20px;"> | |
| </button> | |
| <div id="loading" class="loading"> | |
| <div class="loading-spinner"></div> | |
| Loading knowledge graph... | |
| </div> | |
| <svg id="graph"></svg> | |
| </div> | |
| </div> | |
| <div class="tooltip" id="tooltip"></div> | |
| <script> | |
| // Configuration | |
| const API_BASE = '/api'; | |
| // Global variables | |
| let graphData = { nodes: [], edges: [] }; | |
| let simulation; | |
| let svg, g; | |
| let currentSearch = ''; | |
| let selectedNode = null; | |
| let highlightedElements = { nodes: new Set(), edges: new Set() }; | |
| let sidebarCollapsed = false; | |
| // Initialize the application | |
| async function init() { | |
| setupEventListeners(); | |
| setupVisualizationSVG(); | |
| await loadGraphData(); | |
| hideLoading(); | |
| } | |
| function setupEventListeners() { | |
| // Sidebar toggle | |
| document.getElementById('menuToggle').addEventListener('click', toggleSidebar); | |
| document.getElementById('mainHomeBtn').addEventListener('click', goHome); | |
| // Search and reset | |
| document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300)); | |
| document.getElementById('resetBtn').addEventListener('click', resetHighlighting); | |
| } | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| sidebarCollapsed = !sidebarCollapsed; | |
| if (sidebarCollapsed) { | |
| sidebar.classList.add('collapsed'); | |
| } else { | |
| sidebar.classList.remove('collapsed'); | |
| } | |
| } | |
| function goHome() { | |
| // Navigate back to main page | |
| window.location.href = 'index.html'; // Adjust path as needed | |
| } | |
| function setupVisualizationSVG() { | |
| const container = document.querySelector('.main-content'); | |
| const containerRect = container.getBoundingClientRect(); | |
| svg = d3.select('#graph') | |
| .attr('width', containerRect.width) | |
| .attr('height', containerRect.height); | |
| g = svg.append('g'); | |
| // Add zoom behavior | |
| const zoom = d3.zoom() | |
| .scaleExtent([0.1, 4]) | |
| .on('zoom', (event) => { | |
| g.attr('transform', event.transform); | |
| }); | |
| svg.call(zoom); | |
| // Click on empty space to reset highlighting | |
| svg.on('click', (event) => { | |
| if (event.target === event.currentTarget) { | |
| resetHighlighting(); | |
| } | |
| }); | |
| } | |
| async function loadGraphData(search = '') { | |
| try { | |
| showLoading(); | |
| const url = search | |
| ? `${API_BASE}/graph?search=${encodeURIComponent(search)}` | |
| : `${API_BASE}/graph`; | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error('Failed to fetch graph data'); | |
| graphData = await response.json(); | |
| renderGraph(); | |
| } catch (error) { | |
| showError('Failed to load graph data: ' + error.message); | |
| } finally { | |
| hideLoading(); | |
| } | |
| } | |
| function renderGraph() { | |
| if (!graphData.nodes || graphData.nodes.length === 0) { | |
| showError('No data to display'); | |
| return; | |
| } | |
| // Clear existing elements | |
| g.selectAll('*').remove(); | |
| resetHighlighting(); | |
| // Get container dimensions | |
| const width = +svg.attr('width'); | |
| const height = +svg.attr('height'); | |
| // Create simulation | |
| simulation = d3.forceSimulation(graphData.nodes) | |
| .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(100)) | |
| .force('charge', d3.forceManyBody().strength(-300)) | |
| .force('center', d3.forceCenter(width / 2, height / 2)) | |
| .force('collision', d3.forceCollide().radius(30)); | |
| // Create links | |
| const link = g.append('g') | |
| .selectAll('line') | |
| .data(graphData.edges) | |
| .join('line') | |
| .attr('class', 'link') | |
| .on('mouseover', showEdgeTooltip) | |
| .on('mouseout', hideTooltip); | |
| // Create nodes | |
| const node = g.append('g') | |
| .selectAll('circle') | |
| .data(graphData.nodes) | |
| .join('circle') | |
| .attr('class', 'node') | |
| .attr('r', 12) | |
| .attr('fill', d => getNodeColor(d)) | |
| .attr('stroke', '#fff') | |
| .attr('stroke-width', 2) | |
| .on('mouseover', showNodeTooltip) | |
| .on('mouseout', hideTooltip) | |
| .on('click', handleNodeClick) | |
| .call(d3.drag() | |
| .on('start', dragStarted) | |
| .on('drag', dragged) | |
| .on('end', dragEnded)); | |
| // Create labels | |
| const labels = g.append('g') | |
| .selectAll('text') | |
| .data(graphData.nodes) | |
| .join('text') | |
| .attr('class', 'node-label') | |
| .text(d => d.label.length > 12 ? d.label.substring(0, 12) + '...' : d.label); | |
| // Update positions on simulation tick | |
| simulation.on('tick', () => { | |
| link | |
| .attr('x1', d => d.source.x) | |
| .attr('y1', d => d.source.y) | |
| .attr('x2', d => d.target.x) | |
| .attr('y2', d => d.target.y); | |
| node | |
| .attr('cx', d => d.x) | |
| .attr('cy', d => d.y); | |
| labels | |
| .attr('x', d => d.x) | |
| .attr('y', d => d.y + 20); | |
| }); | |
| } | |
| function getNodeColor(node) { | |
| const colors = { | |
| 'concept': '#4CAF50', | |
| 'disease': '#f44336', | |
| 'treatment': '#2196F3', | |
| 'attribute': '#FF9800', | |
| 'method': '#9C27B0', | |
| 'default': '#607D8B' | |
| }; | |
| return colors[node.type] || colors.default; | |
| } | |
| function showNodeTooltip(event, d) { | |
| const tooltip = d3.select('#tooltip'); | |
| tooltip.transition().duration(200).style('opacity', 1); | |
| const connectionCount = graphData.edges.filter(edge => | |
| edge.source.id === d.id || edge.target.id === d.id | |
| ).length; | |
| tooltip.html(` | |
| <h4>${d.label}</h4> | |
| <p><strong>Type:</strong> ${d.type || 'Node'}</p> | |
| <p><strong>Connections:</strong> ${connectionCount}</p> | |
| <p>Click to highlight connections</p> | |
| `) | |
| .style('left', (event.pageX + 10) + 'px') | |
| .style('top', (event.pageY - 28) + 'px'); | |
| } | |
| function showEdgeTooltip(event, d) { | |
| const tooltip = d3.select('#tooltip'); | |
| tooltip.transition().duration(200).style('opacity', 1); | |
| tooltip.html(` | |
| <h4>${d.relation}</h4> | |
| <p><strong>From:</strong> ${d.source.label || d.source.id}</p> | |
| <p><strong>To:</strong> ${d.target.label || d.target.id}</p> | |
| `) | |
| .style('left', (event.pageX + 10) + 'px') | |
| .style('top', (event.pageY - 28) + 'px'); | |
| } | |
| function hideTooltip() { | |
| d3.select('#tooltip').transition().duration(500).style('opacity', 0); | |
| } | |
| function handleNodeClick(event, d) { | |
| event.stopPropagation(); | |
| if (selectedNode && selectedNode.id === d.id) { | |
| resetHighlighting(); | |
| return; | |
| } | |
| selectedNode = d; | |
| highlightConnections(d); | |
| } | |
| function highlightConnections(selectedNode) { | |
| highlightedElements.nodes.clear(); | |
| highlightedElements.edges.clear(); | |
| graphData.edges.forEach(edge => { | |
| if (edge.source.id === selectedNode.id || edge.target.id === selectedNode.id) { | |
| highlightedElements.edges.add(edge); | |
| highlightedElements.nodes.add(edge.source.id); | |
| highlightedElements.nodes.add(edge.target.id); | |
| } | |
| }); | |
| applyHighlighting(); | |
| } | |
| function applyHighlighting() { | |
| g.selectAll('.node') | |
| .classed('highlighted', d => highlightedElements.nodes.has(d.id) && (!selectedNode || d.id !== selectedNode.id)) | |
| .classed('selected', d => selectedNode && d.id === selectedNode.id) | |
| .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id)); | |
| g.selectAll('.link') | |
| .classed('highlighted', d => highlightedElements.edges.has(d)) | |
| .classed('dimmed', d => selectedNode && !highlightedElements.edges.has(d)); | |
| g.selectAll('.node-label') | |
| .classed('highlighted', d => highlightedElements.nodes.has(d.id)) | |
| .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id)); | |
| } | |
| function resetHighlighting() { | |
| selectedNode = null; | |
| highlightedElements.nodes.clear(); | |
| highlightedElements.edges.clear(); | |
| g.selectAll('.node') | |
| .classed('highlighted', false) | |
| .classed('selected', false) | |
| .classed('dimmed', false); | |
| g.selectAll('.link') | |
| .classed('highlighted', false) | |
| .classed('dimmed', false); | |
| g.selectAll('.node-label') | |
| .classed('highlighted', false) | |
| .classed('dimmed', false); | |
| } | |
| async function handleSearch() { | |
| const query = document.getElementById('searchInput').value.trim(); | |
| if (query === currentSearch) return; | |
| currentSearch = query; | |
| await loadGraphData(query); | |
| } | |
| function showError(message) { | |
| const mainContent = document.querySelector('.main-content'); | |
| const errorDiv = document.createElement('div'); | |
| errorDiv.className = 'error'; | |
| errorDiv.textContent = message; | |
| mainContent.appendChild(errorDiv); | |
| setTimeout(() => errorDiv.remove(), 5000); | |
| } | |
| function showLoading() { | |
| document.getElementById('loading').style.display = 'block'; | |
| } | |
| function hideLoading() { | |
| document.getElementById('loading').style.display = 'none'; | |
| } | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| // Drag functions | |
| function dragStarted(event, d) { | |
| if (!event.active) simulation.alphaTarget(0.3).restart(); | |
| d.fx = d.x; | |
| d.fy = d.y; | |
| } | |
| function dragged(event, d) { | |
| d.fx = event.x; | |
| d.fy = event.y; | |
| } | |
| function dragEnded(event, d) { | |
| if (!event.active) simulation.alphaTarget(0); | |
| d.fx = null; | |
| d.fy = null; | |
| } | |
| // Window resize handler | |
| window.addEventListener('resize', () => { | |
| const container = document.querySelector('.main-content'); | |
| const containerRect = container.getBoundingClientRect(); | |
| svg.attr('width', containerRect.width) | |
| .attr('height', containerRect.height); | |
| if (simulation) { | |
| simulation.force('center', d3.forceCenter(containerRect.width / 2, containerRect.height / 2)); | |
| simulation.alpha(0.3).restart(); | |
| } | |
| }); | |
| // Initialize when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', init); | |
| </script> | |
| </body> | |
| </html> |