1// WikiLive Search Map Visualization 2// Uses D3.js for interactive network graph visualization 3 4class RelationshipMap { 5 constructor(containerId) { 6 this.containerId = containerId; 7 this.width = 1200; 8 this.height = 800; 9 this.svg = null; 10 this.simulation = null; 11 this.nodes = []; 12 this.links = []; 13 this.relationshipData = null; 14 this.lastInfoboxData = null; 15 this.abortController = null; // Add abort controller for canceling requests 16 17 this.colors = { 18 person: '#FF6B6B', 19 movie: '#4ECDC4', 20 location: '#45B7D1', 21 organization: '#96CEB4', 22 concept: '#FFEAA7', 23 unknown: '#DDA0DD', 24 default: '#95A5A6' 25 }; 26 27 // Enhanced relationship type colors for dynamic connections 28 this.relationshipColors = { 29 married_to: '#E74C3C', 30 collaborated_with: '#3498DB', 31 family_relation: '#E67E22', 32 professional_hierarchy: '#9B59B6', 33 educational: '#F39C12', 34 affiliated_with: '#1ABC9C', 35 connected_to: '#34495E', 36 associated_with: '#95A5A6', 37 professional_influence: '#8E44AD', 38 cultural_impact: '#D35400', 39 contextual_background: '#7F8C8D', 40 related_to: '#BDC3C7', 41 default: '#95A5A6' 42 }; 43 44 // Filter states for user-controlled filtering 45 this.filters = { 46 nodeTypes: { 47 person: true, 48 movie: true, 49 location: true, 50 organization: true, 51 concept: true, 52 unknown: true 53 }, 54 relationshipTypes: { 55 married_to: true, 56 collaborated_with: true, 57 family_relation: true, 58 professional_hierarchy: true, 59 educational: true, 60 affiliated_with: true, 61 connected_to: true, 62 associated_with: true, 63 professional_influence: true, 64 cultural_impact: true, 65 contextual_background: true, 66 related_to: true 67 }, 68 showSecondaryRelationships: true, 69 expandView: false 70 }; 71 72 this.init(); 73 } 74 75 /** 76 * Safe appendChild helper to prevent TypeError issues 77 * @param {Element} parent - Parent element 78 * @param {Element} child - Child element to append 79 * @returns {boolean} Success status 80 */ 81 safeAppendChild(parent, child) { 82 try { 83 if (!parent || !child) { 84 console.warn('ð¨ [RelationshipMap] safeAppendChild: Invalid parent or child element'); 85 return false; 86 } 87 88 if (typeof parent.appendChild !== 'function') { 89 console.warn('ð¨ [RelationshipMap] safeAppendChild: Parent element does not have appendChild method'); 90 return false; 91 } 92 93 parent.appendChild(child); 94 return true; 95 } catch (error) { 96 console.error('ð¨ [RelationshipMap] safeAppendChild error:', error); 97 return false; 98 } 99 } 100 101 init() { 102 console.log('ðºï¸ [RelationshipMap] Initializing relationship map visualization'); 103 104 // Create SVG container 105 const container = document.getElementById(this.containerId); 106 if (!container) { 107 console.error(`Container with ID '${this.containerId}' not found`); 108 return; 109 } 110 111 // Clear existing content 112 container.innerHTML = ''; 113 114 // Get container dimensions for responsive sizing 115 const containerRect = container.getBoundingClientRect(); 116 const containerWidth = containerRect.width || 400; 117 const containerHeight = containerRect.height || 500; 118 119 // Adjust dimensions for panel mode 120 this.width = Math.max(300, containerWidth - 40); // Leave some padding 121 this.height = Math.max(200, containerHeight - 100); // Leave space for controls 122 123 // Create main container with controls (no full-screen styling) 124 const mapContainer = document.createElement('div'); 125 mapContainer.style.cssText = `
126 width: 100%; 127 height: 100%; 128 position: relative; 129 overflow: hidden; 130 display: flex; 131 flex-direction: column; 132 `; 133 134 // Create controls panel 135 this.createControlsPanel(mapContainer); 136 137 // Create SVG with responsive sizing 138 this.svg = d3.select(mapContainer) 139 .append('svg') 140 .attr('width', this.width) 141 .attr('height', this.height) 142 .style('background', 'rgba(240, 240, 240, 0.5)') 143 .style('border-radius', '8px') 144 .style('flex', '1') 145 .style('min-height', '200px'); 146 147 // Add zoom behavior 148 const zoom = d3.zoom() 149 .scaleExtent([0.1, 4]) 150 .on('zoom', (event) => { 151 this.svg.select('.main-group').attr('transform', event.transform); 152 }); 153 154 this.svg.call(zoom); 155 156 // Create main group for all elements 157 this.mainGroup = this.svg.append('g').attr('class', 'main-group'); 158 159 // Create groups for different elements 160 this.linksGroup = this.mainGroup.append('g').attr('class', 'links'); 161 this.nodesGroup = this.mainGroup.append('g').attr('class', 'nodes'); 162 this.labelsGroup = this.mainGroup.append('g').attr('class', 'labels'); 163 164 container.appendChild(mapContainer); 165
166 // Loading indicator will be shown in loadRelationshipData with entity name 167 } 168 169 createControlsPanel(container) { 170 const controlsPanel = document.createElement('div'); 171 controlsPanel.style.cssText = ` 172 background: rgba(255,255,255,0.95); 173 padding: 8px 12px; 174 border-bottom: 1px solid #e0e0e0; 175 display: flex; 176 flex-wrap: wrap; 177 align-items: center; 178 gap: 10px; 179 font-size: 12px; 180 min-height: 40px; 181 flex-shrink: 0; 182 `; 183 184 controlsPanel.innerHTML = ` 185 <div id="searchInfo" style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 200px;"> 186 <div style="font-size: 11px; color: #666;">Search:</div> 187 <div id="searchTerm" style="font-weight: 600; color: #333; font-size: 11px;">No search data</div> 188 <div style="font-size: 10px; color: #888;"> 189 <span id="totalResults">0 results</span> 190 </div> 191 </div> 192 193 <div style="display: flex; align-items: center; gap: 8px;"> 194 <label style="display: flex; align-items: center; font-size: 11px; color: #666;"> 195 Relevance: <input type="range" id="relevanceSlider" min="0" max="100" value="30" style="width: 60px; margin: 0 4px;"> 196 <span id="relevanceValue" style="font-size: 10px;">30+</span> 197 </label> 198 </div> 199 200 <div style="display: flex; align-items: center; gap: 6px;"> 201 <button id="resetZoom" style="padding: 4px 8px; background: #4ECDC4; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 10px;">Reset</button> 202 <button id="toggleFilters" style="padding: 4px 8px; background: #667eea; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 10px;">Filters</button> 203 </div> 204 205 <!-- Collapsible detailed filters --> 206 <div id="detailedFilters" style="display: none; width: 100%; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e0e0e0;"> 207 <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;"> 208 <div style="flex: 1; min-width: 120px;"> 209 <label style="display: block; margin-bottom: 3px; font-weight: bold; font-size: 10px;">Entity Types:</label> 210 <div id="entityTypeFilters" style="display: flex; flex-wrap: wrap; gap: 3px;"></div> 211 </div> 212 <div style="flex: 1; min-width: 120px;"> 213 <label style="display: block; margin-bottom: 3px; font-weight: bold; font-size: 10px;">Relationship Types:</label> 214 <div id="relationshipTypeFilters" style="max-height: 80px; overflow-y: auto; border: 1px solid #ddd; padding: 3px; border-radius: 3px; font-size: 10px;"></div> 215 </div> 216 </div> 217 218 <div style="display: flex; gap: 12px; font-size: 10px;"> 219 <label style="display: flex; align-items: center;"> 220 <input type="checkbox" id="showSecondaryRelationships" checked style="margin-right: 3px; transform: scale(0.8);"> 221 Secondary Relations 222 </label> 223 <label style="display: flex; align-items: center;"> 224 <input type="checkbox" id="expandView" style="margin-right: 3px; transform: scale(0.8);"> 225 Expand View 226 </label> 227 </div> 228 </div> 229 230 <!-- Relationship Legend --> 231 <div style="margin-bottom: 10px;"> 232 <h4 style="margin: 0 0 5px 0; color: #333;">Relationship Legend:</h4> 233 <div id="relationshipLegend" style="font-size: 11px; max-height: 120px; overflow-y: auto;"></div> 234 </div> 235 236 <div id="selectedInfo" style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 5px; display: none;"> 237 <h4 style="margin: 0 0 5px 0;">Selected:</h4>
238 <div id="selectionDetails"></div> 239 </div> 240 `; 241 242 container.appendChild(controlsPanel); 243 244 // Add event listeners after the panel is added to DOM 245 setTimeout(() => { 246 this.setupControlsEventListeners(); 247 this.buildEntityTypeFilters(); 248 this.buildRelationshipTypeFilters(); 249 this.buildRelationshipLegend(); 250 }, 100); 251 } 252 253 setupControlsEventListeners() { 254 // Relevance slider 255 const slider = document.getElementById('relevanceSlider'); 256 const valueDisplay = document.getElementById('relevanceValue'); 257 258 if (slider && valueDisplay) { 259 slider.addEventListener('input', (e) => { 260 const value = parseInt(e.target.value); 261 valueDisplay.textContent = value + '+'; 262 this.filterByRelevance(value); 263 }); 264 } 265 266 // Secondary relationships toggle 267 const secondaryToggle = document.getElementById('showSecondaryRelationships'); 268 if (secondaryToggle) { 269 secondaryToggle.addEventListener('change', (e) => { 270 this.filters.showSecondaryRelationships = e.target.checked; 271 this.applyFilters(); 272 }); 273 } 274 275 // Expand view toggle 276 const expandToggle = document.getElementById('expandView'); 277 if (expandToggle) { 278 expandToggle.addEventListener('change', (e) => { 279 this.filters.expandView = e.target.checked; 280 this.applyFilters(); 281 }); 282 } 283 284 // Reset zoom button 285 const resetBtn = document.getElementById('resetZoom'); 286 if (resetBtn) { 287 resetBtn.addEventListener('click', () => { 288 this.resetZoom(); 289 }); 290 } 291 292 // Toggle filters button 293 const toggleFiltersBtn = document.getElementById('toggleFilters'); 294 if (toggleFiltersBtn) { 295 toggleFiltersBtn.addEventListener('click', () => { 296 const detailedFilters = document.getElementById('detailedFilters'); 297 if (detailedFilters) { 298 const isVisible = detailedFilters.style.display !== 'none'; 299 detailedFilters.style.display = isVisible ? 'none' : 'block'; 300 toggleFiltersBtn.textContent = isVisible ? 'Filters' : 'Hide'; 301 } 302 }); 303 } 304 305 // Export data button (removed since not needed in compact view) 306 // const exportBtn = document.getElementById('exportData'); 307 // if (exportBtn) { 308 // exportBtn.addEventListener('click', () => {
309 // this.exportRelationshipData(); 310 // }); 311 // } 312 } 313 314 resetZoom() { 315 if (this.svg) { 316 this.svg.transition().duration(750).call( 317 d3.zoom().transform, 318 d3.zoomIdentity 319 ); 320 } 321 } 322 323 showLoadingIndicator(entityName = null) { 324 const loading = this.mainGroup.append('g') 325 .attr('class', 'loading'); 326 327 // Animated circle 328 loading.append('circle') 329 .attr('cx', this.width / 2) 330 .attr('cy', this.height / 2 - 20) 331 .attr('r', 30) 332 .attr('fill', 'none') 333 .attr('stroke', '#4CAF50') 334 .attr('stroke-width', 2) 335 .attr('stroke-dasharray', '5,5') 336 .append('animateTransform') 337 .attr('attributeName', 'transform') 338 .attr('type', 'rotate') 339 .attr('values', '0 ' + (this.width / 2) + ' ' + (this.height / 2 - 20) + ';360 ' + (this.width / 2) + ' ' + (this.height / 2 - 20)) 340 .attr('dur', '3s') 341 .attr('repeatCount', 'indefinite'); 342 343 // Entity-specific main loading message 344 const mainMessage = entityName ? 345 `ðºï¸ Creating ${entityName} Relationship MAP...` : 346 'ðºï¸ Creating Real Time Search MAP...'; 347 348 loading.append('text') 349 .attr('x', this.width / 2) 350 .attr('y', this.height / 2 - 30) 351 .attr('text-anchor', 'middle') 352 .attr('fill', '#4CAF50') 353 .attr('font-size', '28px') 354 .attr('font-weight', 'bold') 355 .text(mainMessage); 356 357 // Entity-specific secondary message 358 const secondaryMessage = entityName ? 359 `ð¤ AI Analyzing ${entityName} Connections` : 360 'ð¤ AI Analyzing Relationships & Connections'; 361 362 loading.append('text') 363 .attr('x', this.width / 2) 364 .attr('y', this.height / 2 + 20) 365 .attr('text-anchor', 'middle') 366 .attr('fill', 'rgba(255,255,255,0.9)') 367 .attr('font-size', '18px') 368 .text(secondaryMessage); 369 370 // Progress indicator 371 const progressMessage = entityName ? 372 `Discovering ${entityName} relationship patterns...` : 373 'Discovering patterns in your data...'; 374 375 loading.append('text') 376 .attr('x', this.width / 2) 377 .attr('y', this.height / 2 + 50) 378 .attr('text-anchor', 'middle') 379 .attr('fill', 'rgba(255,255,255,0.7)') 380 .attr('font-size', '14px') 381 .text(progressMessage); 382 } 383 384 hideLoadingIndicator() { 385 this.mainGroup.select('.loading').remove(); 386 } 387
388 async loadRelationshipData(infoboxData) { 389 console.log('ð¤ [RelationshipMap] Starting comprehensive relationship analysis...'); 390 console.log('ð [RelationshipMap] Input data structure:', { 391 hasSearchTerm: !!infoboxData?.searchTerm, 392 hasEntities: !!infoboxData?.entities, 393 hasInfoboxes: !!infoboxData?.infoboxes, 394 hasContextEntities: !!infoboxData?.contextEntities, 395 entitiesLength: infoboxData?.entities?.length || 0, 396 infoboxesLength: infoboxData?.infoboxes?.length || 0, 397 contextEntitiesLength: infoboxData?.contextEntities?.length || 0, 398 source: infoboxData?.source 399 }); 400 401 // Extract entity name for personalized loading messages 402 let entityName = null; 403 if (infoboxData) { 404 // Use focusEntity if available (for per-infobox analysis), otherwise use global searchTerm 405 if (this.focusEntity) { 406 entityName = this.focusEntity.title || this.focusEntity.name || 'Unknown Entity'; 407 } else if (infoboxData.searchTerm) { 408 entityName = infoboxData.searchTerm; 409 } else if (infoboxData.entities && infoboxData.entities.length > 0) { 410 entityName = infoboxData.entities[0].title || infoboxData.entities[0].name; 411 } 412 } 413 414 // Clear all previous data to start fresh - CRITICAL for new searches 415 this.clearVisualization(); 416 417 // Show entity-specific loading indicator 418 this.showLoadingIndicator(entityName); 419 420 // Clear any cached data from previous searches 421 this.lastInfoboxData = null; 422 this.relationshipData = null; 423 this.progressiveData = null; 424 425 // Create new abort controller for this analysis session 426 this.abortController = new AbortController(); 427 428 // Store infobox data for current analysis 429 this.lastInfoboxData = infoboxData; 430 431 console.log('ð [RelationshipMap] Processing new search data:', { 432 searchTerm: infoboxData.searchTerm, 433 timestamp: infoboxData.timestamp, 434 totalResults: infoboxData.totalResults 435 }); 436 437 // Update search info in controls panel 438 const searchTermElement = document.getElementById('searchTerm'); 439 const searchTimestampElement = document.getElementById('searchTimestamp'); 440 const totalResultsElement = document.getElementById('totalResults'); 441 442 if (searchTermElement && searchTimestampElement && totalResultsElement) { 443 // Use focusEntity if available, otherwise use original searchTerm 444 const displayTerm = this.focusEntity ? 445 (this.focusEntity.title || this.focusEntity.name || 'Unknown Entity') : 446 infoboxData.searchTerm; 447 448 searchTermElement.textContent = displayTerm; 449 searchTimestampElement.textContent = infoboxData.timestamp; 450 totalResultsElement.textContent = `${infoboxData.totalResults} results`; 451 452 console.log(`ð¯ [RelationshipMap] Focusing on entity: ${displayTerm}`); 453 } 454 455 try { 456 console.log('ð [RelationshipMap] About to call loadAllEntities with data:', infoboxData); 457 await this.loadAllEntities(infoboxData); 458 console.log('â [RelationshipMap] loadAllEntities completed successfully'); 459 } catch (error) { 460 console.error('â [RelationshipMap] Error loading entities:', error); 461 console.error('â [RelationshipMap] Error stack:', error.stack); 462 console.error('â [RelationshipMap] Input data that caused error:', infoboxData); 463 this.hideLoadingIndicator(); 464 this.showError('Failed to analyze relationships: ' + error.message); 465 } 466 } 467 468 async loadAllEntities(infoboxData) { 469 console.log(`ð [RelationshipMap] Loading all entities with enhanced discovery...`); 470 console.log('ð [RelationshipMap] loadAllEntities called with infoboxData:', infoboxData); 471 472 try { 473 // Use our working backend API for relationship anal
473ysis 474 console.log('ð [RelationshipMap] Using backend API for comprehensive relationship analysis'); 475 476 // Call our working backend API endpoint 477 const relationshipData = await this.callBackendAPI(infoboxData); 478 console.log('â [RelationshipMap] Backend API completed:', relationshipData); 479 480 // Process and visualize the relationship data 481 this.processBackendRelationshipData(relationshipData); 482 483 // Now process the converted data and create visualization 484 this.processData(); 485 this.createVisualization(); 486 487 } catch (error) { 488 // Handle aborted requests gracefully 489 if (error.name === 'AbortError') { 490 console.log(`ð« [RelationshipMap] Request was cancelled`); 491 return; // Don't treat this as an error 492 } 493 494 console.error(`â [RelationshipMap] Error loading all entities:`, error); 495 console.log(`ð [RelationshipMap] Attempting fallback relationship analysis...`); 496 497 try { 498 // Create fallback relationship data when backend API fails 499 const fallbackData = this.createFallbackRelationshipD
499ata(infoboxData); 500 console.log('â [RelationshipMap] Fallback relationship data created:', fallbackData); 501 502 // Process the fallback data 503 this.processBackendRelationshipData(fallbackData); 504 505 // Process and visualize the fallback data 506 this.processData(); 507 this.createVisualization(); 508 509 console.log('â [RelationshipMap] Fallback visualization completed successfully'); 510 511 } catch (fallbackError) { 512 console.error(`â [RelationshipMap] Fallback analysis also failed:`, fallbackError); 513 this.showError(`Failed to analyze relationships: ${error.message}. Fallback analysis also failed.`); 514 throw error; 515 } 516 } 517 } 518 519 updateLoadMoreButton() { 520 console.log('ð [RelationshipMap] Load More button functionality disabled - using automatic stage progression'); 521 } 522 523 updateVisualization() { 524 console.log('ð [RelationshipMap] Updating visualization with new data...'); 525 526 // Process new data 527 this.processData(); 528 529 // Smoothly update the existing visualization 530 this.updateNodes(); 531 this.updateLinks(); 532 this.updateSimulation(); 533 534 // Update insights 535 if (this.relationshipData && this.relationshipData.insights) { 536 this.showInsights(this.relationshipData.insights); 537 } 538 } 539 540 updateNodes() { 541 // Update node data 542 const nodeUpdate = this.mainGroup.selectAll('.node') 543 .data(this.nodes, d => d.id); 544 545 // Add new nodes with smooth entrance 546 const nodeEnter = nodeUpdate.enter() 547 .append('g') 548 .attr('class', 'node') 549 .style('opacity', 0) 550 .call(d3.drag() 551 .on('start', (event, d) => this.dragStarted(event, d)) 552 .on('drag', (event, d) => this.dragged(event, d)) 553 .on('end', (event, d) => this.dragEnded(event, d)) 554 ); 555 556 // Add circles to new nodes 557 nodeEnter.append('circle') 558 .attr('r', d => this.calculateNodeRadius(d)) 559 .attr('fill', d => this.colors[d.type] || this.colors.default) 560 .attr('stroke', '#fff') 561 .attr('stroke-width', 2); 562 563 // Add labels to new nodes 564 nodeEnter.append('text') 565 .attr('dy', '.35em') 566 .attr('text-anchor', 'middle') 567 .style('font-size', '12px') 568 .style('font-weight', 'bold') 569 .style('fill', '#333') 570 .style('pointer-events', 'none') 571 .text(d => d.title.length > 15 ? d.title.substring(0, 15) + '...' : d.title); 572 573 // Animate new nodes in 574 nodeEnter.transition() 575 .duration(800) 576 .style('opacity', 1); 577 578 // Update existing nodes 579 nodeUpdate.select('circle') 580 .transition() 581 .duration(500) 582 .attr('r', d => this.calculateNodeRadius(d)); 583 } 584 585 updateLinks() { 586 // Update link data 587 const linkUpdate = this.mainGroup.selectAll('.link') 588 .data(this.links, d => `${d.source.id}-${d.target.id}`); 589 590 // Add new links with smooth entrance 591 const linkEnter = linkUpdate.enter() 592 .insert('line', '.node') 593 .attr('class', 'link') 594 .style('stroke', '#999') 595 .style('stroke-opacity', 0) 596 .style('stroke-width', d => Math.max(1, d.relevance / 20)); 597 598 // Animate new links in 599 linkEnter.transition() 600 .duration(800) 601 .style('stroke-opacity', 0.6); 602 } 603 604 updateSimulation() { 605 // Restart simulation with new data - add null checks 606 if (!this.simulation) { 607 console.error('Simulation not initialized'); 608 return; 609 } 610 611 try { 612 this.simulation.nodes(this.nodes); 613 this.simulation.force('link').links(this.links); 614 this.simulation.alpha(0.3).restart(); 615 } catch (error) { 616 console.error('Error updating simulation:', error); 617 } 618 } 619 620 /** 621 * Process EntityGraphBuilder output format and convert to RelationshipMap format 622 * EntityGraphBuilder returns: { nodes, edges, metadata } 623 * RelationshipMap expects: { entities, relationships } 624 */ 625 processEntityGraphData(entityGraph) { 626 console.log('ð [RelationshipMap] Processing EntityGraphBuilder data:', entityGraph); 627 628 if (!entityGraph || !entityGraph.nodes || !entityGraph.edges) { 629 console.error('â [RelationshipMap] Invalid EntityGraphBuilder data structure:', entityGraph); 630 this.showError('Invalid graph data structure from EntityGraphBuilder'); 631 return; 632 } 633 634 // Convert EntityGraphBuilder format to RelationshipMap format 635 const convertedData = { 636 entities: entityGraph.nodes.map(node => ({ 637 name: node.id, 638 title: node.id, 639 id: node.id, 640 type: node.type || 'unknown', 641 significance: node.score || 50, 642 cluster: node.cluster || 'default', 643 wikidataId: node.wikidataId, 644 // Include image fields from backend mapping 645 image: node.image, 646 imageUrl: node.imageUrl, 647 thumbnail: node.thumbnail, 648 images: node.images, 649 // Copy all other properties to preserve backend data 650 ...node 651 })), 652 relationships: entityGraph.edges.map(edge => ({ 653 from: edge.from, 654 to: edge.to, 655 source: edge.from, // Also include source/target for compatibility 656 target: edge.to, 657 type: edge.type || 'related', 658 relevance: edge.relevance || 50, 659 description: edge.description || `${edge.from} ${edge.type} ${edge.to}`, 660 sharedProperties: edge.sharedProperties || [], 661 bidirectional: edge.bidirectional || false, 662 dataSource: edge.source || 'entitygraph' 663 })), 664 metadata: entityGraph.metadata || {} 665 }; 666 667 console.log('â [RelationshipMap] Converted EntityGraphBuilder data:', { 668 originalNodes: entityGraph.nodes.length, 669 originalEdges: entityGraph.edges.length, 670 convertedEntities: convertedData.entities.length, 671 convertedRelationships: convertedData.relationships.length 672 }); 673 674 // Store the converted data
675 this.relationshipData = convertedData; 676 677 // Hide loading indicator 678 this.hideLoadingIndicator(); 679 680 // Update the relation tree view with the new data 681 this.updateRelationTree(convertedData); 682 } 683 684 processData() { 685 if (!this.relationshipData || !this.relationshipData.entities || !this.relationshipData.relationships) { 686 console.error('Invalid relationship data structure'); 687 return; 688 } 689 690 // Process nodes (entities) - handle both old and new AI response formats 691 this.nodes = this.relationshipData.entities.map(entity => ({ 692 id: entity.name || entity.id || entity.title, // Handle new 'name' field from GPT-3.5-turbo 693 title: entity.name || entity.title || entity.id, 694 type: entity.type || 'unknown', 695 significance: entity.significance, 696 cluster: entity.cluster, 697 x: Math.random() * this.width, 698 y: Math.random() * this.height, 699 radius: this.calculateNodeRadius(entity), 700 color: this.colors[entity.type] || this.colors.default 701 })); 702 703 // Process links (relationships) - ensure source/target match node IDs 704 // Handle both EntityGraphBuilder format (from/to) and AI format (source/target) 705 this.links = this.relationshipData.relationships.map(rel => { 706 const sourceId = rel.from || rel.source; 707 const targetId = rel.to || rel.target; 708 709 return { 710 source: sourceId, 711 target: targetId, 712 type: rel.type, 713 relevance: rel.relevance, 714 description: rel.description || `${sourceId} ${rel.type} ${targetId}`, 715 sharedProperties: rel.sharedProperties || [], 716 bidirectional: rel.bidirectional || false, 717 strokeWidth: this.calculateLinkWidth(rel.relevance), 718 dataSource: rel.source === 'wikidata' ? 'wikidata' : (rel.source === 'wikipedia' ? 'wikipedia' : 'ai') // Track data source separately 719 }; 720 }); 721 722 // Auto-create missing nodes for any entities referenced in relationships 723 const nodeIds = new Set(this.nodes.map(n => n.id)); 724 const missingNodes = new Set(); 725 726 this.links.forEach(link => { 727 if (!nodeIds.has(link.source)) { 728 missingNodes.add(link.source); 729 } 730 if (!nodeIds.has(link.target)) { 731 missingNodes.add(link.target); 732 } 733 }); 734 735 // Create nodes for missing entities 736 missingNodes.forEach(nodeId => { 737 const newNode = { 738 id: nodeId, 739 title: nodeId, 740 type: this.inferEntityType(nodeId), 741 significance: 'Auto-generated from relationship', 742 cluster: 'auto', 743 x: Math.random() * this.width, 744 y: Math.random() * this.height, 745 radius: 15, // Smaller radius for auto-generated nodes 746 color: this.colors.concept || this.colors.default 747 }; 748 this.nodes.push(newNode); 749 nodeIds.add(nodeId); 750 }); 751 752 if (missingNodes.size > 0) { 753 console.log(`ð§ [RelationshipMap] Auto-created ${missingNodes.size} missing nodes:`, Array.from(missingNodes)); 754 } 755 const validLinks = []; 756 const invalidLinks = []; 757 758 this.links.forEach(link => { 759 if (nodeIds.has(link.source) && nodeIds.has(link.target)) { 760 validLinks.push(link); 761 } else { 762 invalidLinks.push(link); 763 console.warn(`â ï¸ [RelationshipMap] Invalid link found: ${link.source} -> ${link.target} (missing node)`); 764 } 765 }); 766 767 this.links = validLinks; 768 769 console.log(`ð [RelationshipMap] Processed ${this.nodes.length} nodes and ${this.links.length} valid links`); 770 if (invalidLinks.length > 0) { 771 console.log(`ð« [RelationshipMap] Filtered out ${invalidLinks.length} invalid links`); 772 } 773 console.log(`ð [RelationshipMap] Node IDs:`, this.nodes.map(n => n.id)); 774 console.log(`ð [RelationshipMap] Valid link sources/targets:`, this.links.map(l => `${l.source} -> ${l.target}`)); 775 } 776 777 /** 778 * Process EntityGraphBuilder output format and convert to visualization format 779 * EntityGraphBuilder returns: { nodes: [...], edges: [...], metadata: {...} } 780 * We need to convert to: { entities: [...], relationships: [...] } 781 */ 782 processEntityGraphData(entityGraph) { 783 console.log('ð [RelationshipMap] Processing EntityGraphBuilder data:', entityGraph); 784 785 if (!entityGraph || !entityGraph.nodes || !entityGraph.edges) { 786 console.error('â [RelationshipMap] Invalid EntityGraphBuilder data structure'); 787 return; 788 } 789 790 // Convert EntityGraphBuilder format to RelationshipMap format 791 const convertedData = { 792 entities: entityGraph.nodes.map(node => ({ 793 name: node.id, 794 title: node.id, 795 type: node.type || 'unknown', 796 significance: node.score || 50, 797 cluster: node.cluster || 'default', 798 wikidataId: node.wikidataId, 799 // Include image fields from backend mapping 800 image: node.image, 801 imageUrl: node.imageUrl, 802 thumbnail: node.thumbnail, 803 images: node.images, 804 // Copy all other properties to preserve backend data 805 ...node 806 })), 807 relationships: entityGraph.edges.map(edge => ({ 808 from: edge.from || edge.source, 809 to: edge.to || edge.target, 810 source: edge.from || edge.source, // Compatibility with existing processData 811 target: edge.to || edge.target, // Compatibility with existing processData 812 type: edge.type || 'related', 813 relevance: edge.relevance || edge.score || 50, 814 description: edge.description || `${edge.from || edge.source} ${edge.type || 'related to'} ${edge.to || edge.target}`, 815 sharedProperties: edge.sharedProperties || [], 816 bidirectional: edge.bidirectional || false, 817 dataSource: edge.dataSource || 'entitygraph' 818 })), 819 metadata: { 820 ...entityGraph.metadata, 821 source: 'EntityGraphBuilder', 822 processingMethod: 'semantic-analysis' 823 } 824 }; 825 826 console.log('â [RelationshipMap] Converted EntityGraphBuilder data:', { 827 entities: convertedData.entities.length, 828 relationships: convertedData.relationships.length, 829 metadata: convertedData.metadata 830 }); 831 832 // Store the converted data
833 this.relationshipData = convertedData; 834 835 // Process using existing logic 836 this.processData(); 837 } 838 839 calculateNodeRadius(entity) { 840 // Base radius with some variation based on significance 841 const baseRadius = 20; 842 843 // Safe significance calculation with null checks 844 let significanceBonus = 0; 845 if (entity.significance && typeof entity.significance === 'string' && entity.significance.length > 0) { 846 significanceBonus = entity.significance.length / 10; 847 } else if (entity.significance && typeof entity.significance === 'number') { 848 significanceBonus = entity.significance / 10; 849 } 850 851 // Ensure we always return a valid number 852 const radius = Math.min(baseRadius + significanceBonus, 40); 853 return isNaN(radius) ? baseRadius : radius; 854 } 855 856 inferEntityType(entityName) { 857 // Infer entity type based on name patterns 858 const name = entityName.toLowerCase(); 859 860 if (name.includes('film') || name.includes('movie') || name.includes('cinema')) { 861 return 'movie'; 862 } 863 if (name.includes('award') || name.includes('prize') || name.includes('nomination')) { 864 return 'award'; 865 } 866 if (name.includes('category') || name.includes('list of') || name.includes('films about')) { 867 return 'category'; 868 } 869 if (name.includes('company') || name.includes('studio') || name.includes('production')) { 870 return 'organization'; 871 } 872 if (name.includes('place') || name.includes('city') || name.includes('country')) { 873 return 'location'; 874 } 875 876 // Default to concept for unknown types 877 return 'concept'; 878 } 879 880 calculateLinkWidth(relevance) { 881 // Map relevance (0-100) to stroke width (1-8) 882 return Math.max(1, (relevance / 100) * 8); 883 } 884 885 createVisualization() { 886 console.log('ð¨ [RelationshipMap] Creating card-based relationship visualization'); 887 888 try { 889 // Validate data 890 if (!this.nodes || !Array.isArray(this.nodes) || this.nodes.length === 0) { 891 console.warn('â ï¸ [RelationshipMap] No nodes to visualize'); 892 this.showError('No relationship data available'); 893 return; 894 } 895 896 // Filter and limit entities to prevent clutter 897 const filteredNodes = this.filterAndLimitNodes(this.nodes); 898 console.log(`ð¯ [RelationshipMap] Filtered from ${this.nodes.length} to ${filteredNodes.length} nodes`); 899 900 // Clear existing visualization 901 this.svg.selectAll('*').remove(); 902 903 // Create card-based layout instead of force-directed graph 904 this.createCardBasedLayout(filteredNodes); 905 906 // Hide loading indicator 907 this.hideLoadingIndicator(); 908 909 // Create entity type filters 910 this.createEntityTypeFilters(); 911 912 // Show insights panel 913 if (this.relationshipData && this.relationshipData.insights) { 914 this.showInsights(this.relationshipData.insights); 915 } 916 917 console.log('â [RelationshipMap] Card-based layout created successfully'); 918 919 } catch (error) { 920 console.error('â [RelationshipMap] Error creating card-based visualization:', error); 921 this.showError('Failed to create visualization'); 922 } 923 } 924 925 filterAndLimitNodes(nodes) { 926 if (!nodes || nodes.length === 0) return []; 927 928 // Sort by relevance/importance 929 const sortedNodes = nodes.sort((a, b) => { 930 const aScore = (a.relevance || 50) + (a.radius || 20); 931 const bScore = (b.relevance || 50) + (b.radius || 20); 932 return bScore - aScore; 933 }); 934 935 // ð¯ ENHANCED: Increased limit to show more cards as requested 936 const maxNodes = 30; 937 const limitedNodes = sortedNodes.slice(0, maxNodes); 938 939 console.log(`ð [RelationshipMap] Filtered nodes:`, limitedNodes.map(n => n.title)); 940 return limitedNodes; 941 } 942 943 createCardBasedLayout(nodes) { 944 console.log('ð [RelationshipMap] Creating card-based layout'); 945 946 const container = this.svg.append('g').attr('class', 'card-container'); 947 948 // Calculate grid layout 949 const cols = Math.ceil(Math.sqrt(nodes.length)); 950 const cardWidth = 180; 951 const cardHeight = 120; 952 const padding = 20; 953 954 // Center the grid 955 const totalWidth = cols * (cardWidth + padding) - padding;
956 const totalHeight = Math.ceil(nodes.length / cols) * (cardHeight + padding) - padding; 957 const startX = (this.width - totalWidth) / 2; 958 const startY = (this.height - totalHeight) / 2; 959 960 nodes.forEach((node, index) => { 961 const row = Math.floor(index / cols); 962 const col = index % cols; 963 const x = startX + col * (cardWidth + padding); 964 const y = startY + row * (cardHeight + padding); 965 966 this.createEntityCard(container, node, x, y, cardWidth, cardHeight); 967 }); 968 } 969 970 createEntityCard(container, node, x, y, width, height) { 971 const card = container.append('g') 972 .attr('class', 'entity-card') 973 .attr('transform', `translate(${x}, ${y})`); 974 975 // Card background 976 card.append('rect') 977 .attr('width', width) 978 .attr('height', height) 979 .attr('rx', 8) 980 .attr('fill', this.getCardColor(node.type)) 981 .attr('stroke', '#ffffff') 982 .attr('stroke-width', 2) 983 .attr('opacity', 0.9) 984 .style('filter', 'drop-shadow(0 4px 8px rgba(0,0,0,0.3))'); 985 986 // Entity icon 987 const icon = this.getIconForType(node.type || 'unknown'); 988 card.append('text') 989 .attr('x', 20) 990 .attr('y', 30) 991 .attr('font-size', '24px') 992 .attr('fill', '#ffffff') 993 .text(icon); 994 995 // Entity title 996 const title = node.title.length > 20 ? node.title.substring(0, 17) + '...' : node.title; 997 card.append('text') 998 .attr('x', 20) 999 .attr('y', 55) 1000 .attr('font-size', '14px') 1001 .attr('font-weight', 'bold') 1002 .attr('fill', '#ffffff') 1003 .attr('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif') 1004 .text(title); 1005 1006 // Entity type/category 1007 if (node.type) { 1008 card.append('text') 1009 .attr('x', 20) 1010 .attr('y', 75) 1011 .attr('font-size', '11px') 1012 .attr('fill', '#e0e0e0') 1013 .attr('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif') 1014 .text(node.type.replace(/_/g, ' ').toUpperCase()); 1015 } 1016 1017 // Relevance indicator 1018 if (node.relevance) { 1019 const relevanceWidth = (node.relevance / 100) * (width - 40); 1020 card.append('rect') 1021 .attr('x', 20) 1022 .attr('y', height - 15) 1023 .attr('width', relevanceWidth) 1024 .attr('height', 3) 1025 .attr('fill', '#FFD700') 1026 .attr('opacity', 0.8); 1027 } 1028 1029 // Add hover effects 1030 card.style('cursor', 'pointer') 1031 .on('mouseover', function() { 1032 d3.select(this).select('rect') 1033 .transition().duration(200) 1034 .attr('opacity', 1) 1035 .attr('stroke-width', 3); 1036 }) 1037 .on('mouseout', function() { 1038 d3.select(this).select('rect') 1039 .transition().duration(200) 1040 .attr('opacity', 0.9) 1041 .attr('stroke-width', 2); 1042 }) 1043 .on('click', () => { 1044 // ð ENHANCED: Direct Wikipedia navigation on card click 1045 const entityData = this.getEntityData(node.id); 1046 if (entityData && entityData.wikipediaUrl) { 1047 window.open(entityData.wikipediaUrl, '_blank'); 1048 } else if (node.title) { 1049 // Fallback: construct Wikipedia URL from title 1050 const wikipediaUrl = `https://en.wikipedia.org/wiki/${encodeURIComponent(node.title.replace(/ /g, '_'))}`; 1051 window.open(wikipediaUrl, '_blank'); 1052 } else { 1053 // Fallback: show entity details if no URL available 1054 this.showEntityDetails(node); 1055 } 1056 }); 1057 } 1058 1059 getCardColor(type) { 1060 const colors = { 1061 'person': '#4A90E2', 1062 'film': '#E94B3C', 1063 'movie': '#E94B3C', // Alias for film 1064 'organization': '#50C878', 1065 'location': '#F5A623', 1066 'award': '#BD10E0', 1067 'event': '#FF6B35', 1068 'work': '#9B59B6', 1069 'book': '#9B59B6', // Alias for work 1070 'album': '#9B59B6', // Alias for work 1071 'song': '#9B59B6', // Alias for work 1072 'concept': '#34495E', 1073 'category': '#95A5A6', 1074 'unknown': '#7B68EE' 1075 }; 1076 return colors[type] || colors['unknown']; 1077 } 1078 1079 getIconForType(type) { 1080 const icons = { 1081 'person': 'ð¤', 1082 'film': 'ð¬', 1083 'movie': 'ð¬', // Alias for film 1084 'organization': 'ð¢', 1085 'location': 'ð', 1086 'award': 'ð', 1087 'event': 'ð ', 1088 'work': 'ð', 1089 'book': 'ð', 1090 'album': 'ð¿', 1091 'song': 'ðµ', 1092 'concept': 'ð¡', 1093 'category': 'ð', 1094 'unknown': 'â' 1095 }; 1096 return icons[type] || icons['unknown']; 1097 } 1098 1099 showEntityDetails(node) {
1100 console.log('ð [RelationshipMap] Showing details for:', node.title); 1101 // Could implement a modal or sidebar with detailed information 1102 alert(`Entity: ${node.title}\nType: ${node.type || 'Unknown'}\nRelevance: ${node.relevance || 'N/A'}`); 1103 } 1104 1105 createEntityTypeFilters() { 1106 const filterContainer = document.getElementById('entityTypeFilters'); 1107 1108 // Add null check to prevent appendChild error 1109 if (!filterContainer) { 1110 console.warn('Entity type filters container not found in DOM'); 1111 return; 1112 } 1113 1114 // Clear existing content safely 1115 try { 1116 filterContainer.innerHTML = ''; 1117 } catch (error) { 1118 console.error('Error clearing filter container:', error); 1119 return; 1120 } 1121 1122 const entityTypes = [...new Set(this.nodes.map(n => n.type))]; 1123 1124 entityTypes.forEach(type => { 1125 const checkbox = document.createElement('input'); 1126 checkbox.type = 'checkbox'; 1127 checkbox.id = `filter-${type}`; 1128 checkbox.checked = true; 1129 checkbox.addEventListener('change', () => this.filterByEntityType()); 1130 1131 const label = document.createElement('label'); 1132 label.htmlFor = `filter-${type}`; 1133 label.style.cssText = ` 1134 display: flex; 1135 align-items: center; 1136 margin-bottom: 5px; 1137 font-size: 12px; 1138 `; 1139 1140 const colorBox = document.createElement('span'); 1141 colorBox.style.cssText = ` 1142 width: 12px; 1143 height: 12px; 1144 background: ${this.colors[type] || this.colors.default}; 1145 margin: 0 5px; 1146 border-radius: 2px; 1147 `; 1148 1149 // Safe appendChild operations with error handling 1150 try { 1151 label.appendChild(checkbox); 1152 label.appendChild(colorBox); 1153 label.appendChild(document.createTextNode(type)); 1154 filterContainer.appendChild(label); 1155 } catch (error) { 1156 console.error('Error appending filter elements:', error); 1157 // Continue with next iteration instead of breaking 1158 } 1159 }); 1160 } 1161 1162 showInsights(insights) { 1163 if (!insights || insights.length === 0) return; 1164 1165 // Check if container still exists before proceeding 1166 const container = document.querySelector(`#${this.containerId}`); 1167 if (!container) { 1168 console.warn('â ï¸ [RelationshipMap] Container not found, skipping insights display:', this.containerId); 1169 return; 1170 } 1171 1172 // Clear any existing insights panel 1173 const existingPanel = container.querySelector('.insights'); 1174 if (existingPanel) { 1175 existingPanel.remove(); 1176 } 1177 1178 const insightsPanel = document.createElement('div'); 1179 insightsPanel.className = 'insights'; 1180 insightsPanel.style.cssText = ` 1181 position: absolute; 1182 top: 20px; 1183 right: 20px; 1184 background: rgba(255,255,255,0.95); 1185 padding: 20px; 1186 border-radius: 12px; 1187 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 1188 max-width: 300px; 1189 min-width: 280px; 1190 max-height: 400px; 1191 overflow-y: auto; 1192 z-index: 1000; 1193 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 1194 `; 1195 1196 const title = document.createElement('h3'); 1197 title.textContent = 'ð Key Insights'; 1198 title.style.cssText = ` 1199 margin: 0 0 15px 0; 1200 color: #333; 1201 font-size: 16px; 1202 font-weight: 600; 1203 `; 1204 insightsPanel.appendChild(title); 1205 1206 insights.forEach((insight, index) => { 1207 const insightDiv = document.createElement('div'); 1208 insightDiv.style.cssText = ` 1209 margin-bottom: 12px; 1210 padding: 10px; 1211 background: rgba(59, 130, 246, 0.1); 1212 border-left: 3px solid #3b82f6; 1213 border-radius: 6px; 1214 font-size: 14px; 1215 line-height: 1.4; 1216 color: #374151; 1217 `; 1218 1219 // Fix [object Object] issue by properly handling different insight data types 1220 let insightText = ''; 1221 if (typeof insight === 'string') { 1222 insightText = insight; 1223 } else if (typeof insight === 'object' && insight !== null) { 1224 // Handle object insights by extracting meaningful information 1225 if (insight.text) { 1226 insightText = insight.text; 1227 } else if (insight.description) { 1228 insightText = insight.description; 1229 } else if (insight.message) { 1230 insightText = insight.message; 1231 } else if (insight.title && insight.value) { 1232 insightText = `${insight.title}: ${insight.value}`; 1233 } else { 1234 // Fallback: create readable summary from object properties
1235 const keys = Object.keys(insight); 1236 if (keys.length > 0) { 1237 insightText = keys.map(key => `${key}: ${insight[key]}`).join(', '); 1238 } else { 1239 insightText = 'No insight data available'; 1240 } 1241 } 1242 } else { 1243 insightText = String(insight) || 'No insight data available'; 1244 } 1245 insightDiv.textContent = insightText; 1246 insightsPanel.appendChild(insightDiv); 1247 }); 1248 1249 this.container.appendChild(insightsPanel); 1250 } 1251 1252 /** 1253 * ð³ RELATION TREE VIEW: Build hierarchical tree from graph data 1254 * Transforms flat node/link data into collapsible tree structure 1255 */ 1256 buildRelationTree(rootId, nodes, links) { 1257 console.log(`ð³ [RelationshipMap] Building relation tree for root: ${rootId}`); 1258 console.log(`ð³ [RelationshipMap] Available nodes:`, nodes.map(n => ({ id: n.id, name: n.name || n.title }))); 1259 console.log(`ð³ [RelationshipMap] Available links:`, links.map(l => ({ source: l.source, target: l.target, type: l.type }))); 1260 1261 // Find the root node to get its proper name and ID 1262 let rootNode = null; 1263 let rootName = rootId; 1264 1265 // Try to find root node by ID first (handle all ID type combinations) 1266 // Look for exact match, string match, or numeric match 1267 rootNode = nodes.find(n => { 1268 // Direct match 1269 if (n.id === rootId) return true; 1270 // String comparison (both converted to strings) 1271 if (String(n.id) === String(rootId)) return true; 1272 // Numeric comparison (if both can be numbers) 1273 if (!isNaN(n.id) && !isNaN(rootId) && Number(n.id) === Number(rootId)) return true; 1274 return false; 1275 }); 1276 1277 // If not found by ID, try to find by name (for string rootId) 1278 if (!rootNode && typeof rootId === 'string') { 1279 rootNode = nodes.find(n => 1280 (n.name && n.name.toLowerCase() === rootId.toLowerCase()) || 1281 (n.title && n.title.toLowerCase() === rootId.toLowerCase()) 1282 ); 1283 } 1284 1285 if (rootNode) { 1286 rootName = rootNode.name || rootNode.title || rootNode.id; 1287 } 1288 1289 console.log(`ð³ [RelationshipMap] Root search - rootId: ${rootId} (type: ${typeof rootId})`); 1290 console.log(`ð³ [RelationshipMap] Available nodes:`, nodes.map(n => ({ id: n.id, name: n.name, title: n.title }))); 1291 console.log(`ð³ [RelationshipMap] Root node found:`, rootNode); 1292 console.log(`ð³ [RelationshipMap] Using root name: ${rootName}`); 1293 1294 // If root node not found and rootId is a string, it might be a name - search more thoroughly 1295 if (!rootNode && typeof rootId === 'string') { 1296 console.log(`ð³ [RelationshipMap] Root node not found by ID, searching by name/title more thoroughly...`); 1297 1298 // Clean the search term by removing common suffixes and extra words 1299 let cleanSearchTerm = rootId.toLowerCase().trim(); 1300 // Remove common suffixes that might be added by the system 1301 cleanSearchTerm = cleanSearchTerm.replace(/\s+(purple|blue|red|green|yellow|orange)$/i, ''); 1302 1303 rootNode = nodes.find(n => { 1304 const nodeName = (n.name || '').toLowerCase().trim(); 1305 const nodeTitle = (n.title || '').toLowerCase().trim(); 1306 1307 console.log(`ð Comparing "${cleanSearchTerm}" (cleaned from "${rootId}") with node: name="${nodeName}", title="${nodeTitle}"`); 1308 1309 // Try exact matches first 1310 if (nodeName === cleanSearchTerm || nodeTitle === cleanSearchTerm) { 1311 return true; 1312 } 1313 1314 // Try partial matches (both ways) 1315 if (nodeName.includes(cleanSearchTerm) || nodeTitle.includes(cleanSearchTerm)) { 1316 return true; 1317 } 1318 if (cleanSearchTerm.includes(nodeName) || cleanSearchTerm.includes(nodeTitle)) { 1319 return true; 1320 } 1321 1322 // Try word-by-word matching for multi-word names 1323 const searchWords = cleanSearchTerm.split(/\s+/); 1324 const nameWords = nodeName.split(/\s+/); 1325 const titleWords = nodeTitle.split(/\s+/); 1326 1327 // Check if all search words are found in name or title 1328 const nameMatch = searchWords.every(word => nameWords.some(nw => nw.includes(word) || word.includes(nw))); 1329 const titleMatch = searchWords.every(word => titleWords.some(tw => tw.includes(word) || word.includes(tw))); 1330 1331 return nameMatch || titleMatch; 1332 }); 1333 1334 if (rootNode) { 1335 rootName = rootNode.name || rootNode.title || rootNode.id; 1336 console.log(`ð³ [RelationshipMap] Found root node by enhanced name search:`, rootNode); 1337 } 1338 } 1339 1340 const tree = { 1341 name: rootName, 1342 children: [], 1343 type: 'root', 1344 icon: 'ð¯' 1345 }; 1346 1347 const nodeMap = Object.fromEntries(nodes.map(n => [n.id, n])); 1348 const processedTargets = new Set(); // Avoid duplicates 1349 1350 // CRITICAL FIX: Resolve the actual root ID once before processing links 1351 // Find the node's actual numeric ID from the nodes array 1352 let actualRootId; 1353 if (rootNode) { 1354 // The rootNode might have been found by name, but we need its actual array index/ID 1355 // Find the node's position in the nodes array, which corresponds to its ID in the links 1356 const nodeIndex = nodes.findIndex(n => n === rootNode); 1357 if (nodeIndex !== -1) { 1358 actualRootId = String(nodeIndex); 1359 console.log(`ð [buildRelationTree] Found root node at index ${nodeIndex}, using as actualRootId: ${actualRootId}`); 1360 console.log(`ð [buildRelationTree] Root node details:`, { name: rootNode.name, title: rootNode.title, id: rootNode.id, index: nodeIndex }); 1361 } else if (rootNode.id !== undefined && typeof rootNode.id === 'number') { 1362 actualRootId = String(rootNode.id); 1363 console.log(`ð [buildRelationTree] Using root node's numeric ID: ${actualRootId}`); 1364 } else { 1365 // Fallback: try to find by name in the nodes array and get its index 1366 const foundIndex = nodes.findIndex(n => 1367 (n.name && n.name.toLowerCase().includes(rootNode.name?.toLowerCase())) || 1368 (n.title && n.title.toLowerCase().includes(rootNode.title?.toLowerCase())) 1369 ); 1370 if (foundIndex !== -1) { 1371 actualRootId = String(foundIndex); 1372 console.log(`ð [buildRelationTree] Found root by name match at index ${foundIndex}, using as actualRootId: ${actualRootId}`); 1373 } else { 1374 actualRootId = String(rootId); 1375 console.log(`ð [buildRelationTree] Could not resolve root node ID, using original rootId: ${actualRootId}`); 1376 } 1377 } 1378 } else { 1379 actualRootId = String(rootId); 1380 console.log(`ð [buildRelationTree] Root node not found, using original rootId: ${actualRootId}`); 1381 } 1382 1383 // Group relationships by type for better organization 1384 const relationshipGroups = {}; 1385 1386 links.forEach(link => { 1387 // Handle different link structure formats from EntityGraphBuilder 1388 // Check if source/target are objects with id property, or primitive values 1389 let sourceId; 1390 if (typeof link.source === 'object' && link.source !== null) { 1391 sourceId = link.source.id !== undefined ? link.source.id : link.source.index; 1392 } else { 1393 sourceId = link.source !== undefined ? link.source : link.from; 1394 } 1395 1396 let targetId; 1397 if (typeof link.target === 'object' && link.target !== null) { 1398 targetId = link.target.id !== undefined ? link.target.id : link.target.index; 1399 } else { 1400 targetId = link.target !== undefined ? link.target : link.to; 1401 } 1402 1403 // Convert to string for comparison 1404 sourceId = String(sourceId); 1405 targetId = String(targetId); 1406 1407 console.log(`ð [buildRelationTree] Checking link: ${sourceId} -> ${targetId} (looking for root ID: ${actualRootId})`); 1408 1409 // Check if this link involves our root entity (by ID) 1410 1411 const isRootSource = sourceId === actualRootId; 1412 const isRootTarget = targetId === actualRootId; 1413 1414 console.log(`ð [buildRelationTree] Comparing: sourceId=${sourceId}, targetId=${targetId}, actualRootId=${actualRootId}, isRootSource=${isRootSource}
1414, isRootTarget=${isRootTarget}`); 1415 1416 if (isRootSource || isRootTarget) { 1417 // Determine the other entity (not the root) 1418 const otherEntityId = isRootSource ? targetId : sourceId; 1419 1420 if (!processedTargets.has(otherEntityId)) { 1421 const otherNode = nodeMap[otherEntityId]; 1422 1423 if (otherNode) { 1424 const relationType = link.type || 'general'; 1425 1426 if (!relationshipGroups[relationType]) { 1427 relationshipGroups[relationType] = []; 1428 } 1429 1430 // Extract proper string name from node data 1431 const nodeName = otherNode.title || otherNode.name || otherNode.id || 'Unknown Entity'; 1432 const displayName = typeof nodeName === 'string' ? nodeName : String(nodeName); 1433 1434 relationshipGroups[relationType].push({ 1435 name: displayName, 1436 relation: link.relation || link.type || 'connected to', 1437 type: otherNode.type || 'unknown', 1438 icon: this.getIconForType(otherNode.type || 'unknown'), 1439 relevance: link.relevance || 50, 1440 children: [] // Could be expanded for nested relationships 1441 }); 1442 1443 processedTargets.add(otherEntityId); 1444 console.log(`ð [buildRelationTree] Added relationship: ${rootName} -> ${displayName} (${relationType})`); 1445 } else { 1446 console.warn(`ð [buildRelationTree] Node not found for ID: ${otherEntityId}`); 1447 } 1448 } 1449 } 1450 }); 1451 1452 // Add grouped relationships to tree 1453 Object.entries(relationshipGroups).forEach(([groupType, relationships]) => { 1454 if (relationships.length > 0) { 1455 // Sort by relevance (highest first) 1456 relationships.sort((a, b) => (b.relevance || 0) - (a.relevance || 0)); 1457 1458 tree.children.push({ 1459 name: `${groupType.toUpperCase()} (${relationships.length})`, 1460 type: 'group', 1461 icon: this.getIconForRelationType(groupType), 1462 children: relationships, 1463 isGroup: true 1464 }); 1465 } 1466 }); 1467 1468 console.log(`ð³ [RelationshipMap] Built tree with ${tree.children.length} groups and ${processedTargets.size} total relationships`); 1469 return tree; 1470 } 1471 1472 /** 1473 * ð¨ Get appropriate icon for entity type 1474 */ 1475 getIconForType(type) { 1476 const icons = { 1477 person: 'ð¤', 1478 film: 'ð¬', 1479 movie: 'ð¬', 1480 organization: 'ðï¸', 1481 place: 'ð', 1482 location: 'ð', 1483 event: 'ð ', 1484 concept: 'ð¡', 1485 book: 'ð', 1486 song: 'ðµ', 1487 album: 'ð¿', 1488 award: 'ð', 1489 company: 'ð¢', 1490 school: 'ð', 1491 sport: 'â½', 1492 unknown: 'â', 1493 default: 'ð' 1494 }; 1495 return icons[type?.toLowerCase()] || icons.default; 1496 } 1497 1498 /** 1499 * ð¨ Get appropriate icon for relationship type 1500 */ 1501 getIconForRelationType(type) { 1502 const icons = { 1503 personal: 'ð¥', 1504 professional: 'ð¼', 1505 family: 'ð¨âð©âð§âð¦', 1506 romantic: 'ð', 1507 collaboration: 'ð¤', 1508 temporal: 'â°', 1509 geographic: 'ð', 1510 thematic: 'ð', 1511 general: 'ð', 1512 default: 'ð' 1513 }; 1514 return icons[type?.toLowerCase()] || icons.default; 1515 } 1516 1517 /** 1518 * ð³ RELATION TREE VIEW: Render collapsible tree sidebar 1519 * Creates interactive hierarchical view of relationships 1520 */ 1521 renderRelationTree(treeData, containerId) { 1522 console.log(`ð³ [RelationshipMap] Rendering relation tree for container: ${containerId}`); 1523 1524 const container = d3.select(`#${containerId}`); 1525 if (container.empty()) { 1526 console.warn(`ð³ [RelationshipMap] Container ${containerId} not found`); 1527 return; 1528 } 1529 1530 // Clear existing content 1531 container.selectAll('*').remove(); 1532 1533 // Create tree container with styling 1534 const treeContainer = container 1535 .append('div') 1536 .attr('class', 'relation-tree-container') 1537 .style('width', '100%') 1538 .style('height', '100%') 1539 .style('overflow-y', 'auto') 1540 .style('padding', '16px') 1541 .style('background', 'rgba(0, 0, 0, 0.05)') 1542 .style('border-radius', '8px') 1543 .style('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'); 1544 1545 // Add tree header 1546 treeContainer 1547 .append('div') 1548 .style('font-size', '16px') 1549 .style('font-weight', '600') 1550 .style('color', '#333') 1551 .style('margin-bottom', '16px') 1552 .style('padding-bottom', '8px') 1553 .style('border-bottom', '2px solid #e0e0e0') 1554 .html(`ð³ Relationships: ${treeData.name}`); 1555 1556 // Render tree nodes recursively 1557 this.renderTreeNode(treeContainer, treeData, 0); 1558 1559 console.log(`ð³ [RelationshipMap] Tree rendered with ${treeData.children?.length || 0} top-level groups`); 1560 } 1561 1562 /** 1563 * ð¿ Render individual tree node with expand/collapse functionality 1564 */ 1565 renderTreeNode(container, nodeData, depth) { 1566 const nodeDiv = container 1567 .append('div') 1568 .attr('class', 'tree-node') 1569 .style('margin-left', `${depth * 20}px`) 1570 .style('margin-bottom', '4px'); 1571 1572 const hasChildren = nodeData.children && nodeData.children.length > 0; 1573 const isExpanded = nodeData.expanded !== false; // Default to expanded 1574 1575 // Create node header with icon and text 1576 const nodeHeader = nodeDiv 1577 .append('div') 1578 .attr('class', 'tree-node-header') 1579 .style('display', 'flex') 1580 .style('align-items', 'center') 1581 .style('padding', '6px 8px') 1582 .style('border-radius', '4px') 1583 .style('cursor', hasChildren ? 'pointer' : 'default') 1584 .style('background', depth === 0 ? 'rgba(76, 175, 80, 0.1)' : 'transparent') 1585 .style('border', depth === 0 ? '1px solid rgba(76, 175, 80, 0.3)' : 'none') 1586 .on('mouseover', function() { 1587 if (hasChildren) { 1588 d3.select(this).style('background', 'rgba(76, 175, 80, 0.15)'); 1589 } 1590 }) 1591 .on('mouseout', function() { 1592 d3.select(this).style('background', depth === 0 ? 'rgba(76, 175, 80, 0.1)' : 'transparent'); 1593 }); 1594 1595 // Add expand/collapse icon for groups 1596 if (hasChildren) { 1597 nodeHeader 1598 .append('span') 1599 .style('margin-right', '6px') 1600 .style('font-size', '12px') 1601 .style('color', '#666') 1602 .text(isExpanded ? 'â¼' : 'â¶'); 1603 } 1604 1605 // Add entity/group icon 1606 nodeHeader 1607 .append('span') 1608 .style('margin-right', '8px') 1609 .style('font-size', '14px') 1610 .text(nodeData.icon || 'ð'); 1611 1612 // Add node name/label 1613 nodeHeader 1614 .append('span') 1615 .style('font-size', nodeData.isGroup ? '13px' : '12px') 1616 .style('font-weight', nodeData.isGroup ? '600' : '400') 1617 .style('color', nodeData.isGroup ? '#333' : '#555') 1618 .text(nodeData.name); 1619 1620 // Add relation type for leaf nodes 1621 if (!nodeData.isGroup && nodeData.relation) { 1622 nodeHeader 1623 .append('span') 1624 .style('margin-left', '8px') 1625 .style('font-size', '11px') 1626 .style('color', '#888') 1627 .style('font-style', 'italic') 1628 .text(`(${nodeData.relation})`); 1629 } 1630 1631 // Create children container 1632 const childrenContainer = nodeDiv 1633 .append('div') 1634 .attr('class', 'tree-children') 1635 .style('display', isExpanded ? 'block' : 'none'); 1636 1637 // Add click handler for expand/collapse 1638 if (hasChildren) { 1639 nodeHeader.on('click', () => { 1640 const currentlyExpanded = childrenContainer.style('display') !== 'none'; 1641 const newExpanded = !currentlyExpanded; 1642 1643 // Update display 1644 childrenContainer.style('display', newExpanded ? 'block' : 'none'); 1645 1646 // Update expand/collapse icon 1647 nodeHeader.select('span').text(newExpanded ? 'â¼' : 'â¶'); 1648 1649 // Store state 1650 nodeData.expanded = newExpanded; 1651 1652 console.log(`ð¿ [RelationshipMap] ${newExpanded ? 'Expanded' : 'Collapsed'} node: ${nodeData.name}`); 1653 }); 1654 } 1655 1656 // Render children recursively 1657 if (hasChildren && isExpanded) {
1658 nodeData.children.forEach(child => { 1659 this.renderTreeNode(childrenContainer, child, depth + 1); 1660 }); 1661 } 1662 } 1663 1664 /** 1665 * ð¨ Enhanced tooltip for nodes with rich content 1666 */ 1667 showNodeTooltip(event, d) { 1668 const tooltip = d3.select('body').selectAll('.relationship-tooltip') 1669 .data([0]) 1670 .join('div') 1671 .attr('class', 'relationship-tooltip') 1672 .style('position', 'absolute') 1673 .style('background', 'rgba(0, 0, 0, 0.9)') 1674 .style('color', 'white') 1675 .style('padding', '12px') 1676 .style('border-radius', '8px') 1677 .style('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif') 1678 .style('font-size', '13px') 1679 .style('line-height', '1.4') 1680 .style('box-shadow', '0 4px 12px rgba(0,0,0,0.3)') 1681 .style('border', '1px solid rgba(255,255,255,0.2)') 1682 .style('max-width', '300px') 1683 .style('z-index', '10000') 1684 .style('pointer-events', 'none'); 1685 1686 // Build rich tooltip content 1687 const content = ` 1688 <div style="border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 8px; margin-bottom: 8px;"> 1689 <strong style="font-size: 15px; color: #4CAF50;">${d.title}</strong> 1690 <div style="font-size: 11px; color: rgba(255,255,255,0.7); margin-top: 2px;"> 1691 Type: ${d.type || 'Unknown'} ⢠Radius: ${d.radius || 20}px 1692 </div> 1693 </div> 1694 <div style="margin-bottom: 8px;"> 1695 ${d.summary ? `<div style="color: rgba(255,255,255,0.9); margin-bottom: 6px;">${d.summary}</div>` : ''} 1696 ${d.facts && d.facts.length > 0 ? ` 1697 <div style="font-size: 12px; color: rgba(255,255,255,0.8);"> 1698 <strong>Key Facts:</strong> 1699 <ul style="margin: 4px 0 0 16px; padding: 0;"> 1700 ${d.facts.slice(0, 3).map(fact => `<li style="margin-bottom: 2px;">${fact}</li>`).join('')} 1701 </ul> 1702 </div> 1703 ` : ''} 1704 </div> 1705 <div style="font-size: 11px; color: rgba(255,255,255,0.6); border-top: 1px solid rgba(255,255,255,0.1); padding-top: 6px;"> 1706 ð¡ Click to explore connections 1707 </div> 1708 `; 1709 1710 tooltip.html(content) 1711 .style('left', (event.pageX + 10) + 'px') 1712 .style('top', (event.pageY - 10) + 'px') 1713 .style('opacity', 0) 1714 .transition() 1715 .duration(200) 1716 .style('opacity', 1); 1717 } 1718 1719 showLinkTooltip(event, d) { 1720 // Create enhanced link tooltip with relationship details 1721 const tooltip = d3.select('body').selectAll('.relationship-tooltip') 1722 .data([d]) 1723 .join('div') 1724 .attr('class', 'relationship-tooltip') 1725 .style('position', 'absolute') 1726 .style('background', 'rgba(0, 0, 0, 0.9)') 1727 .style('color', 'white') 1728 .style('padding', '12px') 1729 .style('border-radius', '8px') 1730 .style('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif') 1731 .style('font-size', '13px') 1732 .style('line-height', '1.4') 1733 .style('box-shadow', '0 4px 12px rgba(0,0,0,0.3)') 1734 .style('border', '1px solid rgba(255,255,255,0.2)') 1735 .style('max-width', '280px') 1736 .style('z-index', '10000') 1737 .style('pointer-events', 'none'); 1738 1739 // Build relationship tooltip content 1740 const sourceTitle = d.source?.title || d.source?.id || 'Unknown'; 1741 const targetTitle = d.target?.title || d.target?.id || 'Unknown'; 1742 const relevanceColor = d.relevance >= 80 ? '#4CAF50' : d.relevance >= 60 ? '#FF9800' : '#FF5722'; 1743 1744 const content = ` 1745 <div style="border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 8px; margin-bottom: 8px;"> 1746 <strong style="font-size: 14px; color: #64B5F6;">${d.type || 'Relationship'}</strong> 1747 <div style="font-size: 11px; color: rgba(255,255,255,0.7); margin-top: 2px;"> 1748 Relevance: <span style="color: ${relevanceColor}; font-weight: bold;">${d.relevance || 50}%</span> 1749 </div> 1750 </div> 1751 <div style="margin-bottom: 8px;"> 1752 <div style="color: rgba(255,255,255,0.9); margin-bottom: 4px;"> 1753 <strong>${sourceTitle}</strong> â <strong>${targetTitle}</strong> 1754 </div> 1755 ${d.description ? `<div style="color: rgba(255,255,255,0.8); font-size: 12px;">${d.description}</div>` : ''} 1756 ${d.dataSource ? `<div style="font-size: 11px; color: rgba(255,255,255,0.6); margin-top: 4px;">Source: ${d.dataSource}</div>` : ''} 1757 </div> 1758 <div style="font-size: 11px; color: rgba(255,255,255,0.6); border-top: 1px solid rgba(255,255,255,0.1); padding-top: 6px;"> 1759 ð Click to highlight this connection 1760 </div> 1761 `; 1762 1763 tooltip.html(content) 1764 .style('left', (event.pageX + 10) + 'px') 1765 .style('top', (event.pageY - 10) + 'px') 1766 .style('opacity', 0) 1767 .transition() 1768 .duration(200) 1769 .style('opacity', 1); 1770 } 1771 1772 hideTooltip() { 1773 // Hide tooltip with smooth transition 1774 d3.select('body').selectAll('.relationship-tooltip') 1775 .transition() 1776 .duration(150) 1777 .style('opacity', 0) 1778 .remove(); 1779 } 1780 1781 selectNode(d) { 1782 // Remove any existing relationship info panel 1783 this.hideRelationshipInfo(); 1784 1785 // Highlight connected relationships 1786 this.highlightConnections(d); 1787 1788 // Show relationship information near the node 1789 this.showRelationshipInfo(d); 1790 } 1791 1792 selectLink(d) { 1793 // Implementation for link selection 1794 } 1795 1796 highlightConnections(selectedNode) { 1797 // Highlight all links connected to this node 1798 this.linksGroup.selectAll('line') 1799 .style('stroke', d => 1800 (d.source.id === selectedNode.id || d.target.id === selectedNode.id) 1801 ? '#FFD700' : '#999' 1802 ) 1803 .style('stroke-width', d => 1804 (d.source.id === selectedNode.id || d.target.id === selectedNode.id) 1805 ? d.strokeWidth * 2 : d.strokeWidth 1806 ); 1807 } 1808 1809 showRelationshipInfo(selectedNode) { 1810 // Find all relationships connected to this node 1811 const connectedRelationships = this.links.filter(link => 1812 link.source.id === selectedNode.id || link.target.id === selectedNode.id 1813 ); 1814 1815 if (connectedRelationships.length === 0) { 1816 return; // No relationships to show 1817 } 1818
1819 // Create relationship info panel with enhanced styling 1820 const infoPanel = document.createElement('div'); 1821 infoPanel.id = 'relationship-info-panel'; 1822 infoPanel.style.cssText = ` 1823 position: fixed; 1824 background: rgba(20, 20, 30, 0.95); 1825 color: white; 1826 padding: 0; 1827 border-radius: 12px; 1828 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1); 1829 max-width: 400px; 1830 min-width: 350px; 1831 max-height: 500px; 1832 overflow-y: auto; 1833 z-index: 10001; 1834 font-size: 13px; 1835 line-height: 1.5; 1836 pointer-events: auto; 1837 border: 1px solid rgba(255, 215, 0, 0.2); 1838 backdrop-filter: blur(10px); 1839 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 1840 `; 1841 1842 // Get the SVG element and its position on the page 1843 const svgElement = document.querySelector(`#${this.containerId} svg`); 1844 const container = document.querySelector(`#${this.containerId}`); 1845 1846 if (!svgElement || !container) { 1847 console.error('Container or SVG not found'); 1848 return; 1849 } 1850 1851 const svgRect = svgElement.getBoundingClientRect(); 1852 1853 // Convert SVG coordinates to page coordinates 1854 const pageX = svgRect.left + selectedNode.x; 1855 const pageY = svgRect.top + selectedNode.y; 1856 1857 // Position the panel very close to the node with smart positioning 1858 let panelX = pageX + 25; 1859 let panelY = pageY - 10; 1860 1861 // Check if panel would go off-screen and adjust position 1862 const panelWidth = 400; 1863 const panelHeight = Math.min(500, connectedRelationships.length * 100 + 200); 1864 1865 if (panelX + panelWidth > window.innerWidth - 20) { 1866 panelX = pageX - panelWidth - 25; 1867 } 1868 1869 if (panelY + panelHeight > window.innerHeight - 20) { 1870 panelY = window.innerHeight - panelHeight - 20; 1871 } 1872 if (panelY < 20) { 1873 panelY = 20; 1874 } 1875 1876 infoPanel.style.left = `${panelX}px`; 1877 infoPanel.style.top = `${panelY}px`; 1878 1879 // Get entity data from the original infobox data 1880 const entityData = this.getEntityData(selectedNode); 1881 1882 // Build enhanced relationship information content 1883 let relationshipHTML = ` 1884 <!-- Entity Header with Image and Details --> 1885 <div style=" 1886 background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.1)); 1887 padding: 16px 20px; 1888 border-radius: 12px 12px 0 0; 1889 border-bottom: 1px solid rgba(255, 215, 0, 0.3); 1890 "> 1891 <div style="display: flex; align-items: flex-start; gap: 12px;"> 1892 ${entityData.image ? ` 1893 <img src="${entityData.image}" 1894 style=" 1895 width: 60px; 1896 height: 60px; 1897 border-radius: 8px; 1898 object-fit: cover; 1899 border: 2px solid rgba(255, 215, 0, 0.3); 1900 flex-shrink: 0; 1901 " 1902 onerror="this.style.display='none'" 1903 /> 1904 ` : ` 1905 <div style=" 1906 width: 60px; 1907 height: 60px; 1908 border-radius: 8px; 1909 background: rgba(255, 215, 0, 0.2); 1910 display: flex; 1911 align-items: center; 1912 justify-content: center; 1913 font-size: 24px; 1914 flex-shrink: 0; 1915 "> 1916 ${this.getEntityTypeIcon(entityData.type)} 1917 </div> 1918 `} 1919 <div style="flex: 1; min-width: 0;"> 1920 <div style=" 1921 font-weight: 600; 1922 color: #FFD700; 1923 font-size: 16px; 1924 margin-bottom: 4px; 1925 word-break: break-word; 1926 "> 1927 ${selectedNode.title} 1928 </div> 1929 <div style=" 1930 display: flex; 1931 align-items: center; 1932 gap: 8px; 1933 margin-bottom: 6px; 1934 ">
1935 <span style=" 1936 background: ${this.getEntityTypeColor(entityData.type)}; 1937 color: white; 1938 padding: 2px 8px; 1939 border-radius: 12px; 1940 font-size: 11px; 1941 font-weight: 500; 1942 text-transform: uppercase; 1943 "> 1944 ${this.getEntityTypeIcon(entityData.type)} ${entityData.type || 'Unknown'} 1945 </span> 1946 ${entityData.relevanceScore ? ` 1947 <span style=" 1948 background: rgba(76, 175, 80, 0.8); 1949 color: white; 1950 padding: 2px 8px; 1951 border-radius: 12px; 1952 font-size: 11px; 1953 font-weight: 500; 1954 "> 1955 â ${entityData.relevanceScore}% relevant 1956 </span> 1957 ` : ''} 1958 </div> 1959 ${entityData.summary ? ` 1960 <div style=" 1961 color: rgba(255, 255, 255, 0.8); 1962 font-size: 12px; 1963 line-height: 1.4; 1964 margin-bottom: 8px; 1965 "> 1966 ${entityData.summary.substring(0, 120)}${entityData.summary.length > 120 ? '...' : ''} 1967 </div> 1968 ` : ''} 1969 </div> 1970 </div> 1971 1972 <!-- Action Buttons --> 1973 <div style=" 1974 display: flex; 1975 gap: 8px; 1976 margin-top: 12px; 1977 flex-wrap: wrap; 1978 "> 1979 ${entityData.wikipediaUrl ? ` 1980 <button onclick="window.open('${entityData.wikipediaUrl}', '_blank')" style=" 1981 background: rgba(255, 255, 255, 0.1); 1982 border: 1px solid rgba(255, 255, 255, 0.2); 1983 color: white; 1984 padding: 6px 12px; 1985 border-radius: 6px; 1986 font-size: 11px; 1987 cursor: pointer; 1988 transition: all 0.2s; 1989 " onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'"> 1990 ð Wikipedia 1991 </button> 1992 ` : ''} 1993 <button onclick="navigator.clipboard.writeText('${selectedNode.title}: ${entityData.summary?.replace(/'/g, "\\'") || 'No description available'}')" style=" 1994 background: rgba(255, 255, 255, 0.1); 1995 border: 1px solid rgba(255, 255, 255, 0.2); 1996 color: white; 1997 padding: 6px 12px; 1998 border-radius: 6px; 1999 font-size: 11px; 2000 cursor: pointer; 2001 transition: all 0.2s; 2002 " onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'"> 2003 ð Copy Info 2004 </button> 2005 <button onclick="window.relationshipMapInstance?.findSimilarEntities('${selectedNode.id}')" style=" 2006 background: rgba(255, 255, 255, 0.1); 2007 border: 1px solid rgba(255, 255, 255, 0.2); 2008 color: white; 2009 padding: 6px 12px; 2010 border-radius: 6px; 2011 font-size: 11px; 2012 cursor: pointer; 2013 transition: all 0.2s; 2014 " onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'"> 2015 ð Find Similar 2016 </button> 2017 </div> 2018 </div> 2019 2020 <!-- Connections Section --> 2021 <div style="padding: 16px 20px;"> 2022 <div style=" 2023 font-size: 14px; 2024 color: #FFD700; 2025 margin-bottom: 12px; 2026 font-weight: 600; 2027 display: flex; 2028 align-items: center; 2029 gap: 8px; 2030 "> 2031 ð Connections (${connectedRelationships.length}) 2032 </div> 2033 <div style=" 2034 max-height: 280px; 2035 overflow-y: auto; 2036 padding-right: 8px; 2037 "> 2038 `; 2039 2040 // Group relationships by type for better organization 2041 const relationshipsByType = {}; 2042 connectedRelationships.forEach(link => { 2043 const type = link.type || 'Related'; 2044 if (!relationshipsByType[type]) { 2045 relationshipsByType[type] = []; 2046 } 2047 relationshipsByType[type].push(link); 2048 }); 2049 2050 // Display relationships grouped by type 2051 Object.entries(relationshipsByType).forEach(([type, relationships]) => { 2052 relationshipHTML += ` 2053 <div style="margin-bottom: 16px;"> 2054 <div style=" 2055 color: rgba(255, 215, 0, 0.8); 2056 font-size: 12px; 2057 font-weight: 600; 2058 margin-bottom: 8px; 2059 text-transform: uppercase; 2060 letter-spacing: 0.5px; 2061 "> 2062 ð ${type} (${relationships.length}) 2063 </div> 2064 `; 2065 2066 relationships.forEach((link, index) => { 2067 const otherNode = link.source.id === selectedNode.id ? link.target : link.source; 2068 const direction = link.source.id === selectedNode.id ? 'â' : 'â'; 2069 const otherEntityData = this.getEntityData(otherNode); 2070 const isLast = index === relationships.length - 1; 2071 2072 relationshipHTML += ` 2073 <div style=" 2074 margin-bottom: ${isLast ? '0' : '8px'}; 2075 padding: 10px; 2076 background: rgba(255, 255, 255, 0.05); 2077 border-radius: 8px; 2078 border-left: 3px solid ${this.getEntityTypeColor(otherEntityData.type)}; 2079 "> 2080 <div style=" 2081 display: flex; 2082 align-items: center; 2083 gap: 8px; 2084 margin-bottom: 6px; 2085 "> 2086 ${otherEntityData.image ? ` 2087 <img src="${otherEntityData.image}" 2088 style=" 2089 width: 24px; 2090 height: 24px; 2091 border-radius: 4px; 2092 object-fit: cover; 2093 " 2094 onerror="this.style.display='none'" 2095 /> 2096 ` : ` 2097 <div style=" 2098 width: 24px; 2099 height: 24px; 2100 border-radius: 4px; 2101 background: ${this.getEntityTypeColor(otherEntityData.type)}; 2102 display: flex; 2103 align-items: center; 2104 justify-content: center; 2105 font-size: 12px; 2106 "> 2107 ${this.getEntityTypeIcon(otherEntityData.type)} 2108 </div> 2109 `} 2110 <div style="flex: 1;"> 2111 <div style=" 2112 color: #FFD700; 2113 font-size: 13px; 2114 font-weight: 500; 2115 display: flex; 2116 align-items: center; 2117 gap: 6px; 2118 ">
2119 <span style="color: rgba(255, 255, 255, 0.6); font-size: 12px;">${direction}</span> 2120 ${otherNode.title || otherNode.id} 2121 </div> 2122 </div> 2123 </div> 2124 2125 ${link.description ? ` 2126 <div style=" 2127 color: rgba(255, 255, 255, 0.9); 2128 font-size: 12px; 2129 margin-bottom: 4px; 2130 line-height: 1.4; 2131 "> 2132 ð ${link.description} 2133 </div> 2134 ` : ''} 2135 2136 <div style=" 2137 display: flex; 2138 align-items: center; 2139 gap: 12px; 2140 font-size: 11px; 2141 color: rgba(255, 255, 255, 0.6); 2142 "> 2143 ${link.year ? ` 2144 <span>ð ${link.year}</span> 2145 ` : ''} 2146 ${link.relevance ? ` 2147 <span>â ${link.relevance}%</span> 2148 ` : ''} 2149 ${otherEntityData.type ? ` 2150 <span>${this.getEntityTypeIcon(otherEntityData.type)} ${otherEntityData.type}</span> 2151 ` : ''} 2152 </div> 2153 </div> 2154 `; 2155 }); 2156 2157 relationshipHTML += `</div>`; 2158 }); 2159 2160 relationshipHTML += ` 2161 </div> 2162 </div> 2163 `; 2164 2165 infoPanel.innerHTML = relationshipHTML; 2166 2167 // Add the panel to the document 2168 document.body.appendChild(infoPanel); 2169 2170 // Set up click outside to close 2171 this.hideRelationshipInfoHandler = (event) => { 2172 if (!infoPanel.contains(event.target)) { 2173 this.hideRelationshipInfo(); 2174 } 2175 }; 2176 2177 setTimeout(() => { 2178 document.addEventListener('click', this.hideRelationshipInfoHandler, { once: true }); 2179 }, 100); 2180 } 2181 2182 hideRelationshipInfo() { 2183 const existingPanel = document.getElementById('relationship-info-panel'); 2184 if (existingPanel) { 2185 existingPanel.remove(); 2186 } 2187 2188 // Remove event listener if it exists 2189 document.removeEventListener('click', this.hideRelationshipInfoHandler); 2190 } 2191 2192 // Bound method for event listener 2193 hideRelationshipInfoHandler = () => { 2194 this.hideRelationshipInfo(); 2195 } 2196 2197 filterByRelevance(minRelevance) { 2198 this.linksGroup.selectAll('line') 2199 .style('opacity', d => d.relevance >= minRelevance ? 1 : 0.1); 2200 } 2201 2202 filterByEntityType() { 2203 const activeTypes = []; 2204 document.querySelectorAll('[id^="filter-"]').forEach(checkbox => { 2205 if (checkbox.checked) { 2206 activeTypes.push(checkbox.id.replace('filter-', '')); 2207 } 2208 }); 2209 2210 this.nodesGroup.selectAll('circle') 2211 .style('opacity', d => activeTypes.includes(d.type) ? 1 : 0.2); 2212 2213 this.labelsGroup.selectAll('text') 2214 .style('opacity', d => activeTypes.includes(d.type) ? 1 : 0.2); 2215 } 2216
2217 exportRelationshipData() { 2218 const dataStr = JSON.stringify(this.relationshipData, null, 2); 2219 const dataBlob = new Blob([dataStr], {type: 'application/json'}); 2220 const url = URL.createObjectURL(dataBlob); 2221 2222 const link = document.createElement('a'); 2223 link.href = url; 2224 link.download = `relationship-analysis-${Date.now()}.json`; 2225 link.click(); 2226 2227 URL.revokeObjectURL(url); 2228 } 2229 2230 showError(message) { 2231 const errorDiv = this.mainGroup.append('g').attr('class', 'error'); 2232 2233 errorDiv.append('text') 2234 .attr('x', this.width / 2) 2235 .attr('y', this.height / 2) 2236 .attr('text-anchor', 'middle') 2237 .attr('fill', '#FF6B6B') 2238 .attr('font-size', '20px') 2239 .attr('font-weight', 'bold') 2240 .text('â Error loading relationship data'); 2241 2242 errorDiv.append('text') 2243 .attr('x', this.width / 2) 2244 .attr('y', this.height / 2 + 30) 2245 .attr('text-anchor', 'middle') 2246 .attr('fill', 'white') 2247 .attr('font-size', '14px') 2248 .text(message); 2249 } 2250 2251 // Drag event handlers 2252 dragStarted(event, d) { 2253 // Add null checks to prevent errors 2254 if (!d || !event) { 2255 console.warn('Invalid drag event or data'); 2256 return; 2257 } 2258 2259 if (!event.active && this.simulation) { 2260 this.simulation.alphaTarget(0.3).restart(); 2261 } 2262 d.fx = d.x; 2263 d.fy = d.y; 2264 2265 // Track drag start position to detect actual dragging 2266 d.dragStartX = event.x; 2267 d.dragStartY = event.y; 2268 d.isDragging = false; 2269 } 2270 2271 dragged(event, d) { 2272 // Add null checks to prevent errors 2273 if (!d || !event) { 2274 console.warn('Invalid drag event or data'); 2275 return; 2276 } 2277 2278 d.fx = event.x; 2279 d.fy = event.y; 2280 2281 // Calculate distance dragged to determine if this is actual dragging 2282 const dragDistance = Math.sqrt( 2283 Math.pow(event.x - d.dragStartX, 2) + Math.pow(event.y - d.dragStartY, 2) 2284 ); 2285 2286 if (dragDistance > 5) { 2287 d.isDragging = true; 2288 } 2289 } 2290 2291 dragEnded(event, d) { 2292 // Add null checks to prevent errors 2293 if (!d || !event) { 2294 console.warn('Invalid drag event or data'); 2295 return; 2296 } 2297 2298 if (!event.active && this.simulation) { 2299 this.simulation.alphaTarget(0); 2300 } 2301 d.fx = null; 2302 d.fy = null; 2303 2304 // Reset dragging state after a short delay 2305 setTimeout(() => { 2306 d.isDragging = false; 2307 }, 100); 2308 } 2309 2310 /** 2311 * ð³ INTEGRATION: Add Relation Tree View to the relationship map 2312 * Creates a collapsible sidebar with hierarchical relationship view 2313 */ 2314 addRelationTreeView() { 2315 console.log('ð³ [RelationshipMap] Adding Relation Tree View sidebar'); 2316 2317 // Remove any existing sidebar first 2318 d3.select('#relation-tree-sidebar').remove(); 2319 d3.select('#tree-view-toggle').remove(); 2320 2321 // Create tree sidebar container directly on body for better reliability 2322 const treeSidebar = d3.select('body') 2323 .append('div') 2324 .attr('id', 'relation-tree-sidebar') 2325 .style('position', 'fixed') // Use fixed positioning for better visibility 2326 .style('top', '80px') 2327 .style('right', '20px') 2328 .style('width', '320px') 2329 .style('height', '60vh') // Use viewport height for better sizing 2330 .style('background', 'rgba(255, 255, 255, 0.95)') 2331 .style('border', '1px solid rgba(0, 0, 0, 0.1)') 2332 .style('border-radius', '12px') 2333 .style('box-shadow', '0 4px 20px rgba(0, 0, 0, 0.15)') 2334 .style('backdrop-filter', 'blur(10px)') 2335 .style('z-index', '1000') 2336 .style('display', 'block'); // Show by default 2337 2338 // Add toggle button for tree view 2339 const toggleButton = d3.select('body') // Append to body for fixed positioning 2340 .append('button') 2341 .attr('id', 'tree-view-toggle') 2342 .style('position', 'fixed') 2343 .style('top', '40px') 2344 .style('right', '360px') // Position to left of sidebar to avoid overlap 2345 .style('padding', '10px 16px') 2346 .style('background', 'rgba(76, 175, 80, 0.9)') 2347 .style('color', 'white') 2348 .style('border', 'none') 2349 .style('border-radius', '8px') 2350 .style('cursor', 'pointer') 2351 .style('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif') 2352 .style('font-size', '14px') 2353 .style('font-weight', '600') 2354 .style('box-shadow', '0 2px 8px rgba(0, 0, 0, 0.2)') 2355 .style('z-index', '1001') 2356 .text('â Close Tree') // Initial text since tree is shown by default 2357 .on('click', () => { 2358 const isVisible = treeSidebar.style('display') !== 'none'; 2359 treeSidebar.style('display', isVisible ? 'none' : 'block'); 2360 toggleButton.text(isVisible ? 'ð³ Tree View' : 'â Close Tree'); 2361 2362 if (!isVisible && this.nodes && this.links) { 2363 this.updateRelationTree(); 2364 } 2365 }); 2366 2367 // Add initial placeholder content to make sidebar visible 2368 treeSidebar 2369 .append('div') 2370 .style('padding', '16px') 2371 .style('color', '#666') 2372 .style('font-family', 'Inter, -apple-system, BlinkMacSystemFont, sans-serif') 2373 .style('font-size', '14px') 2374 .html('ð³ <strong>Relation Tree View</strong><br><br>Loading relationship tree...'); 2375 2376 console.log('ð³ [RelationshipMap] Relation Tree View sidebar added with placeholder content'); 2377 2378 // Debug: Check if sidebar is actually visible 2379 const sidebarElement = document.getElementById('relation-tree-sidebar'); 2380 if (sidebarElement) { 2381 console.log('â [RelationshipMap] Tree sidebar DOM element exists');
2382 console.log('ð [RelationshipMap] Sidebar dimensions:', { 2383 width: sidebarElement.offsetWidth, 2384 height: sidebarElement.offsetHeight, 2385 display: window.getComputedStyle(sidebarElement).display, 2386 visibility: window.getComputedStyle(sidebarElement).visibility 2387 }); 2388 } else { 2389 console.error('â [RelationshipMap] Tree sidebar DOM element NOT found!'); 2390 } 2391 } 2392 2393 /** 2394 * ð Update the relation tree with current graph data 2395 */ 2396 updateRelationTree(relationshipData = null) { 2397 console.log('ð [RelationshipMap] updateRelationTree called with data:', relationshipData); 2398 2399 // Use provided data if available, otherwise use instance data 2400 let nodes = this.nodes; 2401 let links = this.links; 2402 2403 if (relationshipData && relationshipData.entities && relationshipData.relationships) { 2404 console.log('ð [RelationshipMap] Using provided relationship data for tree view'); 2405 // Convert relationship data to nodes/links format for tree building 2406 nodes = relationshipData.entities.map(entity => ({ 2407 id: entity.name || entity.title || entity.id, 2408 title: entity.name || entity.title || entity.id, 2409 type: entity.type || 'unknown' 2410 })); 2411 2412 links = relationshipData.relationships.map(rel => ({ 2413 source: rel.from || rel.source, 2414 target: rel.to || rel.target, 2415 type: rel.type || 'related', 2416 relevance: rel.relevance || 50 2417 })); 2418 } 2419 2420 console.log('ð [RelationshipMap] Data check:', { 2421 nodes: nodes?.length || 0, 2422 links: links?.length || 0, 2423 focusEntity: this.focusEntity 2424 }); 2425 2426 if (!nodes || !links || nodes.length === 0) { 2427 console.warn('ð³ [RelationshipMap] No data available for tree view'); 2428 // Still show the container with a message 2429 const container = d3.select(`#${this.containerId}`); 2430 if (!container.empty()) { 2431 container.html(` 2432 <div style="display: flex; justify-content: center; align-items: center; height: 100%; color: white; font-size: 18px;"> 2433 <div style="text-align: center;"> 2434 <div style="font-size: 48px; margin-bottom: 20px;">ð</div> 2435 <div>No relationship data available</div> 2436 <div style="font-size: 14px; margin-top: 10px; opacity: 0.7;">Try searching for a different entity</div> 2437 </div> 2438 </div> 2439 `); 2440 } 2441 return; 2442 } 2443 2444 // Check if tree sidebar exists with enhanced debugging 2445 console.log('ð³ [RelationshipMap] Looking for tree sidebar element...'); 2446 const treeSidebar = d3.select('#relation-tree-sidebar'); 2447 2448 if (treeSidebar.empty()) { 2449 console.error('ð³ [RelationshipMap] Tree sidebar element not found! Checking DOM...'); 2450 const sidebarElement = document.getElementById('relation-tree-sidebar'); 2451 console.log('ð³ [RelationshipMap] Direct DOM check:', sidebarElement); 2452 2453 // Try to create the sidebar if it doesn't exist 2454 if (!sidebarElement) { 2455 console.log('ð³ [RelationshipMap] Sidebar missing, creating it now...'); 2456 try { 2457 this.addRelationTreeView(); 2458 2459 // Wait a moment for DOM to update 2460 setTimeout(() => { 2461 const newTreeSidebar = d3.select('#relation-tree-sidebar'); 2462 if (newTreeSidebar.empty()) { 2463 console.error('ð³ [RelationshipMap] Still cannot find tree sidebar after creation!'); 2464 return; 2465 }
2466 this.updateRelationTree(relationshipData); // Recursive call after creation 2467 }, 100); 2468 return; 2469 } catch (error) { 2470 console.error('ð³ [RelationshipMap] Error creating sidebar:', error); 2471 return; 2472 } 2473 } 2474 return; 2475 } 2476 2477 console.log(`ð³ [RelationshipMap] Tree sidebar found, proceeding with update...`); 2478 2479 // Find the focus entity (usually the first node or the one with highest centrality) 2480 // Extract string name from focusEntity object if needed 2481 let focusEntityName; 2482 if (this.focusEntity) { 2483 if (typeof this.focusEntity === 'string') { 2484 focusEntityName = this.focusEntity; 2485 } else { 2486 // Extract name from object 2487 focusEntityName = this.focusEntity.title || this.focusEntity.name || this.focusEntity.id || String(this.focusEntity); 2488 } 2489 } else { 2490 focusEntityName = nodes[0]?.id || 'Unknown'; 2491 } 2492 2493 // Ensure we have a string 2494 const focusEntity = typeof focusEntityName === 'string' ? focusEntityName : String(focusEntityName); 2495 2496 // Build tree data structure 2497 console.log(`ð³ [RelationshipMap] Building tree data for focus entity: ${focusEntity}`); 2498 const treeData = this.buildRelationTree(focusEntity, nodes, links); 2499 2500 if (!treeData) { 2501 console.error('ð³ [RelationshipMap] Failed to build tree data'); 2502 return; 2503 } 2504 2505 // Render the tree in the sidebar with enhanced error handling 2506 console.log(`ð³ [RelationshipMap] Rendering tree with ${treeData.children?.length || 0} children`); 2507 try { 2508 this.renderRelationTree(treeData, 'relation-tree-sidebar'); 2509 console.log(`â [RelationshipMap] Tree rendered successfully`); 2510 } catch (error) { 2511 console.error(`â [RelationshipMap] Error rendering tree:`, error); 2512 // Show fallback message in sidebar 2513 this.showFallbackTreeMessage(relationshipData); 2514 } 2515 2516 console.log(`ð³ [RelationshipMap] Tree updated successfully for focus entity: ${focusEntity}`); 2517 } 2518 2519 /** 2520 * Create SVG patterns for node images 2521 */ 2522 createNodeImagePatterns() { 2523 console.log('ð¼ï¸ [RelationshipMap] Creating image patterns for nodes'); 2524 2525 // Create or get defs element for patterns 2526 let defs = this.svg.select('defs'); 2527 if (defs.empty()) { 2528 defs = this.svg.append('defs'); 2529 } 2530 2531 // Create patterns for each node that has an image 2532 this.nodes.forEach(node => { 2533 const imageUrl = this.getNodeImageUrl(node); 2534 if (imageUrl) { 2535 const patternId = `image-pattern-${this.sanitizeId(node.id)}`; 2536 2537 // Remove existing pattern if it exists 2538 defs.select(`#${patternId}`).remove(); 2539 2540 // Create new pattern 2541 const pattern = defs.append('pattern') 2542 .attr('id', patternId) 2543 .attr('patternUnits', 'objectBoundingBox') 2544 .attr('width', '100%') 2545 .attr('height', '100%'); 2546 2547 // Add image to pattern 2548 pattern.append('image') 2549 .attr('href', imageUrl) 2550 .attr('width', '100%') 2551 .attr('height', '100%') 2552 .attr('preserveAspectRatio', 'xMidYMid slice') 2553 .style('opacity', 0.9) 2554 .on('error', function() { 2555 console.warn(`ð¼ï¸ [RelationshipMap] Failed to load image for ${node.title}: ${imageUrl}`); 2556 // Hide the pattern on error 2557 d3.select(this.parentNode).style('display', 'none'); 2558 }); 2559 2560 console.log(`ð¼ï¸ [RelationshipMap] Created image pattern for ${node.title}: ${imageUrl}`); 2561 } 2562 }); 2563 } 2564 2565 /** 2566 * Get image URL for a node from various sources 2567 * @param {Object} node - The node object 2568 * @returns {string|null} Image URL or null if not available 2569 */ 2570 getNodeImageUrl(node) { 2571 // Debug: Log node data to see what image fields are available 2572 console.log(`[getNodeImageUrl] Checking node "${node.name || node.title || node.id}":`, { 2573 image: node.image, 2574 imageUrl: node.imageUrl, 2575 thumbnail: node.thumbnail, 2576 images: node.images, 2577 infobox_image: node.infobox?.image, 2578 hasImageFields: !!(node.image || node.imageUrl || node.thumbnail || (node.images && node.images.length > 0)) 2579 }); 2580 2581 // Try multiple sources for image URL 2582 let imageUrl = null; 2583 2584 // 1. Direct image property 2585 if (node.image) { 2586 imageUrl = node.image; 2587 console.log(`[getNodeImageUrl] Using node.image: ${imageUrl}`); 2588 } 2589 // 2. ImageUrl property (from backend mapping) 2590 else if (node.imageUrl) { 2591 imageUrl = node.imageUrl; 2592 console.log(`[getNodeImageUrl] Using node.imageUrl: ${imageUrl}`); 2593 } 2594 // 3. Images array (take first one) 2595 else if (node.images && Array.isArray(node.images) && node.images.length > 0) { 2596 imageUrl = node.images[0]; 2597 console.log(`[getNodeImageUrl] Using node.images[0]: ${imageUrl}`); 2598 } 2599 // 4. Infobox image property 2600 else if (node.infobox && node.infobox.image) { 2601 imageUrl = node.infobox.image; 2602 console.log(`[getNodeImageUrl] Using node.infobox.image: ${imageUrl}`); 2603 } 2604 // 5. Wikipedia thumbnail 2605 else if (node.thumbnail) { 2606 imageUrl = node.thumbnail; 2607 console.log(`[getNodeImageUrl] Using node.thumbnail: ${imageUrl}`); 2608 } 2609 // 6. Extract from Wikipedia URL (construct thumbnail URL) 2610 else if (node.url && node.url.includes('wikipedia.org')) { 2611 // This is a fallback - we could potentially construct a thumbnail URL 2612 // but it's complex and not always reliable 2613 console.log(`[getNodeImageUrl] No image found for Wikipedia URL: ${node.url}`); 2614 } 2615 else { 2616 console.log(`[getNodeImageUrl] No image sources found for node "${node.name || node.title || node.id}"`); 2617 } 2618 2619 // Validate and clean up the URL 2620 if (imageUrl) { 2621 // Handle relative URLs 2622 if (imageUrl.startsWith('//')) { 2623 imageUrl = 'https:' + imageUrl; 2624 } else if (imageUrl.startsWith('/')) { 2625 imageUrl = 'https://en.wikipedia.org' + imageUrl; 2626 } 2627 2628 // Ensure it's a valid image URL 2629 if (imageUrl.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i) || imageUrl.includes('upload.wikimedia.org')) { 2630 return imageUrl; 2631 } 2632 } 2633 2634 return null; 2635 } 2636 2637 /** 2638 * Sanitize ID for use in SVG patterns 2639 * @param {string} id - The ID to sanitize 2640 * @returns {string} Sanitized ID 2641 */ 2642 sanitizeId(id) { 2643 if (!id) return 'unknown'; 2644 return id.toString() 2645 .replace(/[^a-zA-Z0-9-_]/g, '-') 2646 .replace(/--+/g, '-') 2647 .replace(/^-|-$/g, '') 2648 .toLowerCase(); 2649 } 2650 2651 /** 2652 * Show fallback message in tree sidebar when rendering fails 2653 * @param {Object}
2653 relationshipData - The relationship data to display 2654 */ 2655 showFallbackTreeMessage(relationshipData) { 2656 console.log('ð [RelationshipMap] Showing fallback tree message'); 2657 2658 const treeSidebar = document.getElementById('relation-tree-sidebar'); 2659 if (!treeSidebar) { 2660 console.warn('â ï¸ [RelationshipMap] Tree sidebar not found for fallback message'); 2661 return; 2662 } 2663 2664 try { 2665 // Clear existing content 2666 treeSidebar.innerHTML = ''; 2667 2668 // Create fallback content 2669 const fallbackContent = document.createElement('div'); 2670 fallbackContent.style.cssText = ` 2671 padding: 20px; 2672 color: white; 2673 font-family: Arial, sans-serif; 2674 text-align: center; 2675 `; 2676 2677 // Add title 2678 const title = document.createElement('h3'); 2679 title.textContent = 'Relationship Analysis'; 2680 title.style.cssText = ` 2681 margin: 0 0 15px 0; 2682 color: #4ECDC4; 2683 font-size: 16px; 2684 `; 2685 fallbackContent.appendChild(title); 2686 2687 // Add data source indicator 2688 const dataSource = relationshipData?.dataSource || 'unknown'; 2689 const sourceIndicator = document.createElement('div'); 2690 sourceIndicator.textContent = `Data Source: ${dataSource}`; 2691 sourceIndicator.style.cssText = ` 2692 margin-bottom: 15px; 2693 font-size: 12px; 2694 color: #95A5A6; 2695 font-style: italic; 2696 `; 2697 fallbackContent.appendChild(sourceIndicator); 2698 2699 // Add entity count 2700 if (relationshipData?.entities) { 2701 const entityCount = document.createElement('div'); 2702 entityCount.textContent = `Entities: ${relationshipData.entities.length}`; 2703 entityCount.style.cssText = ` 2704 margin-bottom: 10px; 2705 font-size: 14px; 2706 color: #E74C3C; 2707 `; 2708 fallbackContent.appendChild(entityCount); 2709 } 2710 2711 // Add relationship count 2712 if (relationshipData?.relationships) { 2713 const relationshipCount = document.createElement('div'); 2714 relationshipCount.textContent = `Relationships: ${relationshipData.relationships.length}`; 2715 relationshipCount.style.cssText = ` 2716 margin-bottom: 15px; 2717 font-size: 14px; 2718 color: #3498DB; 2719 `; 2720 fallbackContent.appendChild(relationshipCount); 2721 } 2722 2723 // Add insights if available 2724 if (relationshipData?.insights && Array.isArray(relationshipData.insights)) { 2725 const insightsTitle = document.createElement('div'); 2726 insightsTitle.textContent = 'Insights:'; 2727 insightsTitle.style.cssText = ` 2728 margin-bottom: 10px; 2729 font-weight: bold; 2730 color: #F39C12; 2731 font-size: 14px; 2732 `; 2733 fallbackContent.appendChild(insightsTitle); 2734 2735 relationshipData.insights.forEach(insight => { 2736 const insightDiv = document.createElement('div'); 2737 insightDiv.textContent = `⢠${insight}`; 2738 insightDiv.style.cssText = ` 2739 margin-bottom: 5px; 2740 font-size: 12px; 2741 color: #BDC3C7; 2742 text-align: left; 2743 `; 2744 fallbackContent.appendChild(insightDiv); 2745 }); 2746 } 2747 2748 // Add the fallback content to sidebar 2749 this.safeAppendChild(treeSidebar, fallbackContent); 2750 2751 console.log('â [RelationshipMap] Fallback tree message displayed successfully'); 2752 2753 } catch (error) { 2754 console.error('â [RelationshipMap] Error showing fallback tree message:', error); 2755 // Last resort: simple text message 2756 treeSidebar.innerHTML = ` 2757 <div style="padding: 20px; color: white; text-align: center;"> 2758 <h3 style="color: #4ECDC4;">Relationship Data Available</h3> 2759 <p style="color: #BDC3C7;">Tree view temporarily unavailable</p> 2760 </div> 2761 `; 2762 } 2763 } 2764 2765 /**
2766 * ð¯ Set focus entity for relationship analysis 2767 */ 2768 setFocusEntity(entityId) { 2769 this.focusEntity = entityId; 2770 console.log(`ð¯ [RelationshipMap] Focus entity set to: ${entityId}`); 2771 2772 // Update tree view if it's visible 2773 const treeSidebar = d3.select('#relation-tree-sidebar'); 2774 if (!treeSidebar.empty() && treeSidebar.style('display') !== 'none') { 2775 this.updateRelationTree(); 2776 } 2777 } 2778 2779 clearVisualization() { 2780 console.log('ð§¹ [RelationshipMap] Clearing previous visualization data...'); 2781 2782 // Cancel any ongoing requests 2783 if (this.abortController) { 2784 this.abortController.abort(); 2785 this.abortController = null; 2786 console.log('ð« [RelationshipMap] Cancelled ongoing requests'); 2787 } 2788 2789 // Stop and clear existing simulation 2790 if (this.simulation) { 2791 this.simulation.stop(); 2792 this.simulation = null; 2793 } 2794 2795 // Clear data arrays and cached data 2796 this.nodes = []; 2797 this.links = []; 2798 this.relationshipData = null; 2799 this.progressiveData = null; 2800 this.lastInfoboxData = null; 2801 2802 // Clear all visual elements 2803 if (this.nodesGroup) { 2804 this.nodesGroup.selectAll('*').remove(); 2805 } 2806 if (this.linksGroup) { 2807 this.linksGroup.selectAll('*').remove(); 2808 } 2809 if (this.labelsGroup) { 2810 this.labelsGroup.selectAll('*').remove(); 2811 } 2812 2813 // Clear any existing relationship info panel 2814 this.hideRelationshipInfo(); 2815 2816 // Clear any existing insights panel 2817 const insightsPanel = document.querySelector(`#${this.containerId} .insights`); 2818 if (insightsPanel) { 2819 insightsPanel.remove(); 2820 } 2821 2822 // Clear any existing stage progress panel 2823 const stageProgressPanel = document.querySelector(`#${this.containerId} .stage-progress`); 2824 if (stageProgressPanel) { 2825 stageProgressPanel.remove(); 2826 } 2827 2828 // Clear entity type filters 2829 const filtersPanel = document.querySelector(`#${this.containerId} .entity-filters`); 2830 if (filtersPanel) { 2831 filtersPanel.remove(); 2832 } 2833 2834 // Clear any existing error messages 2835 if (this.mainGroup) { 2836 this.mainGroup.select('.error').remove(); 2837 this.mainGroup.select('.loading').remove(); 2838 } 2839 2840 console.log('â [RelationshipMap] Visualization cleared successfully'); 2841 } 2842 2843 /** 2844 * Create fallback relationship data when backend API fails 2845 * @param {Object} infoboxData - The infobox data to analyze 2846 * @returns {Object} Basic relationship data structure 2847 */
2848 createFallbackRelationshipData(infoboxData) { 2849 console.log('ð [RelationshipMap] Creating fallback relationship data from infoboxData:', infoboxData); 2850 2851 if (!infoboxData || !Array.isArray(infoboxData) || infoboxData.length === 0) { 2852 console.warn('â ï¸ [RelationshipMap] Invalid infoboxData for fallback analysis'); 2853 return { 2854 entities: [], 2855 relationships: [], 2856 insights: ['No data available for relationship analysis'], 2857 dataSource: 'fallback' 2858 }; 2859 } 2860 2861 // Extract entities from infobox data 2862 const entities = []; 2863 const relationships = []; 2864 const processedNames = new Set(); 2865 2866 infoboxData.forEach((infobox, index) => { 2867 if (!infobox || !infobox.title) return; 2868 2869 const entityName = infobox.title; 2870 if (processedNames.has(entityName)) return; 2871 processedNames.add(entityName); 2872 2873 // Create entity from infobox 2874 const entity = { 2875 id: entityName, 2876 name: entityName, 2877 type: this.classifyEntityType(infobox), 2878 significance: index === 0 ? 'High' : 'Medium', 2879 cluster: 'main', 2880 description: infobox.summary || `Information about ${entityName}` 2881 }; 2882 entities.push(entity); 2883 2884 // Extract related entities from infobox links and create relationships 2885 if (infobox.links && Array.isArray(infobox.links)) {
2886 infobox.links.slice(0, 5).forEach(link => { // Limit to 5 links to avoid clutter 2887 if (link && link.title && link.title !== entityName) { 2888 const relatedEntityName = link.title; 2889 2890 // Add related entity if not already processed 2891 if (!processedNames.has(relatedEntityName)) { 2892 processedNames.add(relatedEntityName); 2893 entities.push({ 2894 id: relatedEntityName, 2895 name: relatedEntityName, 2896 type: this.classifyEntityType(link), 2897 significance: 'Low', 2898 cluster: 'related', 2899 description: `Related to ${entityName}` 2900 }); 2901 } 2902 2903 // Create relationship 2904 relationships.push({ 2905 source: entityName, 2906 target: relatedEntityName, 2907 type: 'associated_with', 2908 relevance: 60, 2909 description: `${entityName} is associated with ${relatedEntityName}`, 2910 dataSource: 'fallback' 2911 }); 2912 } 2913 }); 2914 } 2915 }); 2916 2917 // ð¯ FIXED: Create selective cross-relationships only between highly relevant entities 2918 const mainEntities = entities.filter(e => e.significance === 'High'); 2919 const primaryEntity = mainEntities[0]; // Usually the search subject 2920 2921 // Only create relationships FROM the primary entity TO other significant entities 2922 if (primaryEntity && mainEntities.length > 1) { 2923 // Limit to top 5 most relevant connections to prevent clutter 2924 const topRelatedEntities = mainEntities.slice(1, 6); 2925 2926 topRelatedEntities.forEach(relatedEntity => { 2927 // Only create meaningful relationships based on entity types 2928 const relationType = this.determineRelationshipType(primaryEntity, relatedEntity); 2929 2930 if (relationType !== 'skip') { // Skip generic/weak connections 2931 relationships.push({ 2932 source: primaryEntity.name, 2933 target: relatedEntity.name, 2934 type: relationType, 2935 relevance: 70, // Higher relevance for selective relationships 2936 description: `${primaryEntity.name} ${this.getRelationDescription(relationType)} ${relatedEntity.name}`, 2937 dataSource: 'fallback' 2938 }); 2939 } 2940 }); 2941 } 2942 2943 const fallbackData = { 2944 entities: entities, 2945 relationships: relationships, 2946 insights: [ 2947 `Found ${entities.length} entities from search results`, 2948 `Created ${relationships.length} basic relationships`, 2949 'This is fallback data - OpenAI analysis was not available' 2950 ], 2951 dataSource: 'fallback', 2952 stage: 'fallback' 2953 }; 2954 2955 console.log('â [RelationshipMap] Fallback relationship data created:', { 2956 entities: entities.length, 2957 relationships: relationships.length 2958 }); 2959 2960 return fallbackData; 2961 } 2962 2963 /** 2964 * Classify entity type based on infobox data 2965 * @param {Object} infobox - Infobox data 2966 * @returns {string} Entity type 2967 */ 2968 classifyEntityType(infobox) { 2969 if (!infobox || !infobox.title) return 'unknown'; 2970 2971 const title = infobox.title.toLowerCase(); 2972 const summary = (infobox.summary || '').toLowerCase(); 2973 2974 // Person indicators 2975 if (summary.includes('born') || summary.includes('actor') || summary.includes('singer') || 2976 summary.includes('director') || summary.includes('writer') || summary.includes('politician')) { 2977 return 'person'; 2978 } 2979 2980 // Movie/TV indicators 2981 if (summary.includes('film') || summary.includes('movie') || summary.includes('television') || 2982 summary.includes('series') || title.includes('film') || title.includes('movie')) { 2983 return 'movie'; 2984 } 2985 2986 // Location indicators 2987 if (summary.includes('city') || summary.includes('country') || summary.includes('state') || 2988 summary.includes('located') || summary.includes('capital')) { 2989 return 'location'; 2990 } 2991 2992 // Organization indicators 2993 if (summary.includes('company') || summary.includes('organization') || summary.includes('corporation') || 2994 summary.includes('founded') || summary.includes('business')) { 2995 return 'organization'; 2996 } 2997 2998 return 'concept'; 2999 } 3000 3001 /** 3002 * Determine appropriate relationship type between two entities 3003 * @param {Object} entityA - First entity 3004 * @param {Object}
3004 entityB - Second entity 3005 * @returns {string} Relationship type or 'skip' to avoid connection 3006 */ 3007 determineRelationshipType(entityA, entityB) { 3008 const typeA = entityA.type; 3009 const typeB = entityB.type; 3010 3011 // Skip relationships between same-type generic entities to reduce clutter 3012 if (typeA === 'concept' && typeB === 'concept') return 'skip'; 3013 if (typeA === 'unknown' && typeB === 'unknown') return 'skip'; 3014 3015 // Person-to-person relationships 3016 if (typeA === 'person' && typeB === 'person') return 'associated_with'; 3017 3018 // Person-to-work relationships 3019 if ((typeA === 'person' && typeB === 'movie') || (typeA === 'movie' && typeB === 'person')) { 3020 return 'featured_in'; 3021 } 3022 3023 // Location-based relationships 3024 if ((typeA === 'person' && typeB === 'location') || (typeA === 'location' && typeB === 'person')) { 3025 return 'located_in'; 3026 } 3027 3028 // Organization relationships 3029 if ((typeA === 'person' && typeB === 'organization') || (typeA === 'organization' && typeB === 'person')) { 3030 return 'affiliated_with'; 3031 } 3032 3033 // Default meaningful relationship 3034 return 'related_to'; 3035 } 3036 3037 /** 3038 * Get human-readable description for relationship type 3039 * @param {string} relationType - The relationship type 3040 * @returns {string} Human-readable description 3041 */ 3042 getRelationDescription(relationType) { 3043 const descriptions = { 3044 'associated_with': 'is associated with', 3045 'featured_in': 'is featured in', 3046 'located_in': 'is located in', 3047 'affiliated_with': 'is affiliated with', 3048 'related_to': 'is related to', 3049 'connected_to': 'is connected to' 3050 }; 3051 3052 return descriptions[relationType] || 'is connected to'; 3053 } 3054 3055 /** 3056 * Call our working backend API for relationship analysis 3057 * @param {Object} infoboxData - The infobox data to analyze 3058 * @returns {Object} Relationship data from backend 3059 */ 3060 async callBackendAPI(infoboxData) { 3061 console.log('ð [RelationshipMap] Calling backend API with data:', infoboxData); 3062 3063 try { 3064 const response = await fetch('/.netlify/functions/analyze-relationships', { 3065 method: 'POST', 3066 headers: { 3067 'Content-Type': 'application/json', 3068 }, 3069 body: JSON.stringify({ 3070 infoboxData: infoboxData 3071 }) 3072 }); 3073 3074 if (!response.ok) { 3075 throw new Error(`Backend API error: ${response.status} ${response.statusText}`); 3076 } 3077 3078 const data = await response.json(); 3079 console.log('â [RelationshipMap] Backend API response:', data); 3080 return data; 3081 3082 } catch (error) { 3083 console.error('â [RelationshipMap] Backend API call failed:', error); 3084 throw error; 3085 } 3086 } 3087 3088 /** 3089 * Process backend relationship data and convert to visualization format 3090 * @param {Object} relationshipData - Data from backend API 3091 */
3092 processBackendRelationshipData(relationshipData) { 3093 console.log('ð [RelationshipMap] Processing backend relationship data:', relationshipData); 3094 3095 if (!relationshipData || !relationshipData.entities || !relationshipData.relationships) { 3096 console.warn('â ï¸ [RelationshipMap] Invalid backend data structure'); 3097 this.showError('Invalid relationship data received from backend'); 3098 return; 3099 } 3100 3101 // Store the relationship data 3102 this.relationshipData = relationshipData; 3103 3104 // Convert backend format to our internal format 3105 this.nodes = relationshipData.entities.map(entity => ({ 3106 id: entity.id || entity.name, 3107 title: entity.name, 3108 name: entity.name, 3109 type: entity.type || 'unknown', 3110 significance: entity.significance || 'Medium', 3111 cluster: entity.cluster || 'default', 3112 x: Math.random() * this.width, 3113 y: Math.random() * this.height, 3114 radius: this.calculateNodeRadius(entity), 3115 color: this.colors[entity.type] || this.colors.default, 3116 // Include image fields from backend mapping 3117 image: entity.image, 3118 imageUrl: entity.imageUrl, 3119 thumbnail: entity.thumbnail, 3120 images: entity.images, 3121 // Copy all other properties to preserve backend data 3122 ...entity 3123 })); 3124 3125 this.links = relationshipData.relationships.map(rel => ({ 3126 source: rel.source, 3127 target: rel.target, 3128 type: rel.type || 'related', 3129 relevance: rel.relevance || 50, 3130 description: rel.description || '', 3131 dataSource: rel.dataSource || 'backend' 3132 })); 3133 3134 console.log(`â [RelationshipMap] Processed ${this.nodes.length} nodes and ${this.links.length} relationships`); 3135 console.log('ð [RelationshipMap] Nodes:', this.nodes.map(n => n.id)); 3136 console.log('ð [RelationshipMap] Links:', this.links.map(l => `${l.source} -> ${l.target}`)); 3137 3138 // ð§ FIX: Update the Relation Tree View sidebar with the processed data 3139 console.log('ð³ [RelationshipMap] Updating Relation Tree View with backend data'); 3140 this.updateRelationTree(relationshipData); 3141 } 3142 3143 /** 3144 * Build entity type filter checkboxes 3145 */ 3146 buildEntityTypeFilters() { 3147 const container = document.getElementById('entityTypeFilters'); 3148 if (!container) return; 3149 3150 container.innerHTML = ''; 3151 3152 Object.keys(this.filters.nodeTypes).forEach(type => { 3153 const label = document.createElement('label'); 3154 label.style.cssText = ` 3155 display: flex; 3156 align-items: center; 3157 margin: 2px; 3158 font-size: 11px; 3159 cursor: pointer; 3160 padding: 2px 6px; 3161 border-radius: 3px; 3162 background: ${this.colors[type] || this.colors.default}; 3163 color: white; 3164 `; 3165 3166 const checkbox = document.createElement('input'); 3167 checkbox.type = 'checkbox'; 3168 checkbox.checked = this.filters.nodeTypes[type]; 3169 checkbox.style.marginRight = '4px'; 3170 3171 checkbox.addEventListener('change', (e) => { 3172 this.filters.nodeTypes[type] = e.target.checked; 3173 this.applyFilters(); 3174 }); 3175 3176 label.appendChild(checkbox); 3177 label.appendChild(document.createTextNode(type)); 3178 container.appendChild(label); 3179 }); 3180 } 3181 3182 /** 3183 * Build relationship type filter checkboxes 3184 */ 3185 buildRelationshipTypeFilters() { 3186 const container = document.getElementById('relationshipTypeFilters'); 3187 if (!container) return; 3188 3189 container.innerHTML = ''; 3190 3191 Object.keys(this.filters.relationshipTypes).forEach(type => { 3192 const label = document.createElement('label'); 3193 label.style.cssText = `
3194 display: flex; 3195 align-items: center; 3196 margin: 2px 0; 3197 font-size: 11px; 3198 cursor: pointer; 3199 `; 3200 3201 const checkbox = document.createElement('input'); 3202 checkbox.type = 'checkbox'; 3203 checkbox.checked = this.filters.relationshipTypes[type]; 3204 checkbox.style.marginRight = '4px'; 3205 3206 checkbox.addEventListener('change', (e) => { 3207 this.filters.relationshipTypes[type] = e.target.checked; 3208 this.applyFilters(); 3209 }); 3210 3211 const colorBox = document.createElement('span'); 3212 colorBox.style.cssText = ` 3213 width: 12px; 3214 height: 12px; 3215 background: ${this.relationshipColors[type] || this.relationshipColors.default}; 3216 margin-right: 4px; 3217 border-radius: 2px; 3218 `; 3219 3220 label.appendChild(checkbox); 3221 label.appendChild(colorBox); 3222 label.appendChild(document.createTextNode(type.replace(/_/g, ' '))); 3223 container.appendChild(label); 3224 }); 3225 } 3226 3227 /** 3228 * Build relationship legend 3229 */ 3230 buildRelationshipLegend() { 3231 const container = document.getElementById('relationshipLegend'); 3232 if (!container) return; 3233 3234 container.innerHTML = ''; 3235 3236 Object.entries(this.relationshipColors).forEach(([type, color]) => { 3237 if (type === 'default') return; 3238 3239 const item = document.createElement('div'); 3240 item.style.cssText = ` 3241 display: flex; 3242 align-items: center; 3243 margin: 2px 0; 3244 font-size: 10px; 3245 `; 3246 3247 const colorBox = document.createElement('span'); 3248 colorBox.style.cssText = ` 3249 width: 10px; 3250 height: 10px; 3251 background: ${color}; 3252 margin-right: 4px; 3253 border-radius: 2px; 3254 `; 3255 3256 item.appendChild(colorBox); 3257 item.appendChild(document.createTextNode(type.replace(/_/g, ' '))); 3258 container.appendChild(item); 3259 }); 3260 } 3261 3262 /** 3263 * Apply current filters to the visualization 3264 */ 3265 applyFilters() { 3266 if (!this.svg || !this.nodes || !this.links) return; 3267 3268 // Filter nodes by type 3269 const visibleNodes = this.nodes.filter(node => 3270 this.filters.nodeTypes[node.type] !== false 3271 ); 3272 3273 // Filter links by type and visible nodes 3274 const visibleNodeIds = new Set(visibleNodes.map(n => n.id)); 3275 const visibleLinks = this.links.filter(link => { 3276 const sourceId = typeof link.source === 'object' ? link.source.id : link.source; 3277 const targetId = typeof link.target === 'object' ? link.target.id : link.target; 3278 3279 return visibleNodeIds.has(sourceId) && 3280 visibleNodeIds.has(targetId) && 3281 this.filters.relationshipTypes[link.type] !== false; 3282 }); 3283 3284 // Update visualization with filtered data 3285 this.updateVisualization(visibleNodes, visibleLinks); 3286 } 3287 3288 /** 3289 * Update visualization with filtered data 3290 */ 3291 updateVisualization(nodes, links) { 3292 // Update node visibility 3293 this.nodesGroup.selectAll('.node') 3294 .style('display', d => nodes.find(n => n.id === d.id) ? 'block' : 'none'); 3295 3296 // Update link visibility and colors 3297 this.linksGroup.selectAll('.link') 3298 .style('display', d => { 3299 const sourceId = typeof d.source === 'object' ? d.source.id : d.source; 3300 const targetId = typeof d.target === 'object' ? d.target.id : d.target; 3301 return links.find(l => { 3302 const lSourceId = typeof l.source === 'object' ? l.source.id : l.source; 3303 const lTargetId = typeof l.target === 'object' ? l.target.id : l.target; 3304 return lSourceId === sourceId && lTargetId === targetId; 3305 }) ? 'block' : 'none'; 3306 }) 3307 .style('stroke', d => this.relationshipColors[d.type] || this.relationshipColors.default); 3308 3309 // Update label visibility 3310 this.labelsGroup.selectAll('.label') 3311 .style('display', d => nodes.find(n =>
3311 n.id === d.id) ? 'block' : 'none'); 3312 } 3313 3314 /** 3315 * Export relationship data as JSON 3316 */ 3317 exportRelationshipData() { 3318 if (!this.relationshipData) { 3319 alert('No relationship data to export'); 3320 return; 3321 } 3322 3323 const exportData = { 3324 searchTerm: this.relationshipData.searchTerm || 'Unknown', 3325 timestamp: this.relationshipData.timestamp || new Date().toISOString(), 3326 nodes: this.nodes, 3327 links: this.links, 3328 insights: this.relationshipData.insights || [], 3329 clusters: this.relationshipData.clusters || [] 3330 }; 3331 3332 const dataStr = JSON.stringify(exportData, null, 2); 3333 const dataBlob = new Blob([dataStr], { type: 'application/json' }); 3334 3335 const link = document.createElement('a'); 3336 link.href = URL.createObjectURL(dataBlob); 3337 link.download = `relationship-data-${exportData.searchTerm.replace(/\s+/g, '-')}-${Date.now()}.json`; 3338 link.click(); 3339 3340 console.log('ð [RelationshipMap] Exported relationship data:', exportData); 3341 } 3342} 3343 3344// Export the RelationshipMap class for use in other modules 3345if (typeof module !== 'undefined' && module.exports) { 3346 module.exports = RelationshipMap; 3347} else if (typeof window !== 'undefined') { 3348 window.RelationshipMap = RelationshipMap; 3349}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.