// Mind Map Visualization Component class MindMap { static render(container, data) { if (!container || !data) return; // Clear previous content container.innerHTML = ''; // Create SVG element const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%'); svg.setAttribute('viewBox', '0 0 800 400'); container.appendChild(svg); // Setup D3 force simulation const simulation = d3.forceSimulation(data.nodes) .force('link', d3.forceLink(data.edges).id(d => d.id).distance(150)) .force('charge', d3.forceManyBody().strength(-500)) .force('center', d3.forceCenter(400, 200)) .force('collision', d3.forceCollide().radius(60)); // Create links const link = svg.append('g') .selectAll('line') .data(data.edges) .enter().append('line') .attr('stroke', '#d1d5db') .attr('stroke-width', 2); // Create nodes const node = svg.append('g') .selectAll('g') .data(data.nodes) .enter().append('g') .attr('class', 'mindmap-node') .style('cursor', 'pointer') .call(d3.drag() .on('start', this.dragstarted) .on('drag', this.dragged) .on('end', this.dragended)); // Add node circles node.append('circle') .attr('r', d => this.getNodeRadius(d)) .attr('fill', d => this.getNodeColor(d)) .attr('stroke', '#ffffff') .attr('stroke-width', 2); // Add node labels node.append('text') .text(d => this.truncateLabel(d.label)) .attr('text-anchor', 'middle') .attr('dy', '.35em') .attr('font-size', d => this.getFontSize(d)) .attr('font-weight', 'bold') .attr('fill', d => this.getTextColor(d)) .style('pointer-events', 'none'); // Add score display for product nodes node.filter(d => d.score) .append('text') .text(d => d.score) .attr('text-anchor', 'middle') .attr('dy', '2.5em') .attr('font-size', '12px') .attr('font-weight', 'normal') .attr('fill', '#6b7280') .style('pointer-events', 'none'); // Add click handlers node.on('click', (event, d) => { this.handleNodeClick(d, data); }); // Add hover effects node.on('mouseover', function(event, d) { d3.select(this).select('circle') .transition() .duration(200) .attr('r', d => d3.select(this).select('circle').attr('r') * 1.1); }); node.on('mouseout', function(event, d) { d3.select(this).select('circle') .transition() .duration(200) .attr('r', d => this.getNodeRadius(d)); }.bind(this)); // Update positions on simulation tick simulation.on('tick', () => { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); node .attr('transform', d => `translate(${d.x},${d.y})`); }); } static getNodeRadius(d) { switch (d.level) { case 0: return 50; // Root node case 1: return 35; // Category/solution nodes case 2: return 25; // Product nodes default: return 20; } } static getNodeColor(d) { switch (d.level) { case 0: return '#3b82f6'; // Primary blue case 1: return '#10b981'; // Secondary green case 2: return '#f59e0b'; // Warning orange for products default: return '#6b7280'; } } static getTextColor(d) { return d.level <= 1 ? '#ffffff' : '#374151'; } static getFontSize(d) { switch (d.level) { case 0: return '16px'; case 1: return '14px'; case 2: return '11px'; default: return '12px'; } } static truncateLabel(label) { if (label.length > 20) { return label.substring(0, 17) + '...'; } return label; } static handleNodeClick(node, data) { // Handle different types of node clicks if (node.level === 2) { // Product node this.selectProduct(node); } else if (node.level === 1) { // Category node this.focusCategory(node, data); } else if (node.level === 0) { // Root node this.showSearchDetails(); } } static selectProduct(productNode) { // Highlight selected product const event = new CustomEvent('productSelected', { detail: { productId: productNode.id, productTitle: productNode.label } }); document.dispatchEvent(event); // Update search results to focus on this product this.updateResultsFocus(productNode.id); } static focusCategory(categoryNode, data) { // Filter results to show only products in this category const categoryProducts = data.nodes.filter(n => n.level === 2 && this.isConnectedToCategory(n, categoryNode, data.edges) ); const event = new CustomEvent('categoryFocused', { detail: { category: categoryNode.label, products: categoryProducts } }); document.dispatchEvent(event); } static isConnectedToCategory(productNode, categoryNode, edges) { return edges.some(edge => (edge.source.id === categoryNode.id && edge.target.id === productNode.id) || (edge.source.id === productNode.id && edge.target.id === categoryNode.id) ); } static showSearchDetails() { // Show expanded search details const event = new CustomEvent('searchDetailsRequested'); document.dispatchEvent(event); } static updateResultsFocus(productId) { const resultsContainer = document.getElementById('search-results'); if (!resultsContainer) return; // Remove previous highlights const highlighted = resultsContainer.querySelectorAll('.bg-blue-50'); highlighted.forEach(el => el.classList.remove('bg-blue-50')); // Add highlight to selected product const productResult = Array.from(resultsContainer.children).find(child => { return child.querySelector('h4') && child.querySelector('h4').textContent.includes(productId); }); if (productResult) { productResult.classList.add('bg-blue-50', 'border-l-4', 'border-blue-500'); productResult.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } static updateVisualization(newData) { // Update the mind map with new data const container = document.getElementById('mindmap-container'); if (container) { this.render(container, newData); } } static addProductNode(product, categoryNodeId) { // Add a new product node to the existing visualization const container = document.getElementById('mindmap-container'); if (!container || !container.querySelector('svg')) return; const event = new CustomEvent('addProductToMindMap', { detail: { product, categoryNodeId } }); document.dispatchEvent(event); } static setupInteractionHandlers() { // Setup global interaction handlers document.addEventListener('productSelected', (event) => { console.log('Product selected:', event.detail); }); document.addEventListener('categoryFocused', (event) => { console.log('Category focused:', event.detail); }); document.addEventListener('searchDetailsRequested', () => { console.log('Search details requested'); }); } static exportVisualization() { // Export the current visualization as SVG or image const svg = document.querySelector('#mindmap-container svg'); if (!svg) return null; const serializer = new XMLSerializer(); const svgString = serializer.serializeToString(svg); return { svg: svgString, timestamp: new Date().toISOString(), dimensions: { width: svg.viewBox.baseVal.width, height: svg.viewBox.baseVal.height } }; } // Drag event handlers static dragstarted(event, d) { if (!event.active) this.simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } static dragged(event, d) { d.fx = event.x; d.fy = event.y; } static dragended(event, d) { if (!event.active) this.simulation.alphaTarget(0); d.fx = null; d.fy = null; } } // Initialize interaction handlers when component loads MindMap.setupInteractionHandlers(); // Export for use in main app if (typeof window !== 'undefined') { window.MindMap = MindMap; } if (typeof module !== 'undefined' && module.exports) { module.exports = MindMap; }