PageSourceSearch

https://dynamic-salmiakki-095693.netlify.app/relationship-map.js

js dynamic-salmiakki-095693.netlify.app collected 2026-10-03 10:40:26 UTC 139,401 bytes, 3,349 lines download raw bytes

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.