onesearch / components /MindMap.js
jessikat29's picture
OneSearch Technical Specification
11a16a2 verified
Raw
History Blame Contribute Delete
9.5 kB
// 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;
}