let networkGraph = null; let simulation = null; let showLabels = true; let pinnedNodes = new Set(); let hiddenCategories = new Set(); let showIsolated = true; const categoryColors = { 'core_value': { fill: '#ef4444', stroke: '#fee2e2', gradient: ['#ef4444', '#dc2626'] }, 'hard_constraint': { fill: '#f59e0b', stroke: '#fef3c7', gradient: ['#f59e0b', '#d97706'] }, 'soft_constraint': { fill: '#10b981', stroke: '#d1fae5', gradient: ['#10b981', '#059669'] }, 'factor': { fill: '#3b82f6', stroke: '#dbeafe', gradient: ['#3b82f6', '#2563eb'] } }; document.addEventListener('DOMContentLoaded', function() { const networkTabButton = document.querySelector('[data-tab="network"]'); networkTabButton.addEventListener('click', function() { setTimeout(() => { initializeNetworkGraph(); }, 100); }); document.getElementById('reset-zoom').addEventListener('click', resetZoom); document.getElementById('toggle-labels').addEventListener('click', toggleLabels); const showIsolatedBtn = document.getElementById('show-isolated'); if (showIsolatedBtn) { showIsolatedBtn.addEventListener('click', toggleIsolated); } document.querySelectorAll('.legend-item').forEach(item => { item.addEventListener('click', function() { const category = this.dataset.category; if (hiddenCategories.has(category)) { hiddenCategories.delete(category); this.classList.remove('hidden'); } else { hiddenCategories.add(category); this.classList.add('hidden'); } if (networkGraph) { filterByCategory(); } }); }); }); function initializeNetworkGraph() { const container = document.getElementById('network-graph'); if (!container || !window.appData.graph) { return; } container.innerHTML = ''; const width = container.clientWidth; const height = container.clientHeight; const svg = d3.select('#network-graph') .append('svg') .attr('width', width) .attr('height', height) .style('background', '#1a1a1a'); const defs = svg.append('defs'); const gradientKeys = Object.keys(categoryColors); gradientKeys.forEach(key => { const grad = defs.append('radialGradient') .attr('id', `grad-${key}`) .attr('cx', '50%') .attr('cy', '50%') .attr('r', '50%') .attr('fx', '50%') .attr('fy', '50%'); grad.append('stop') .attr('offset', '0%') .attr('stop-color', categoryColors[key].gradient[0]); grad.append('stop') .attr('offset', '100%') .attr('stop-color', categoryColors[key].gradient[1]); }); const arrowMarker = defs.append('marker') .attr('id', 'arrowhead') .attr('viewBox', '-0 -5 10 10') .attr('refX', 25) .attr('refY', 0) .attr('orient', 'auto') .attr('markerWidth', 6) .attr('markerHeight', 6) .append('path') .attr('d', 'M 0,-5 L 10 ,0 L 0,5') .attr('fill', '#666'); const g = svg.append('g'); const zoom = d3.zoom() .scaleExtent([0.1, 4]) .on('zoom', (event) => { g.attr('transform', event.transform); }); svg.call(zoom); const graph = window.appData.graph; if (!graph.nodes || !graph.edges) { container.innerHTML = '
No graph data available
'; return; } const nodeConnections = new Map(); graph.nodes.forEach(n => nodeConnections.set(n.id, new Set())); graph.edges.forEach(e => { nodeConnections.get(e.source.id || e.source)?.add(e.target.id || e.target); nodeConnections.get(e.target.id || e.target)?.add(e.source.id || e.source); }); const link = g.append('g') .attr('class', 'links') .selectAll('line') .data(graph.edges) .enter() .append('line') .attr('stroke-width', d => Math.max(1, Math.min(3, Math.sqrt(d.weight)))) .attr('stroke', '#666') .attr('stroke-opacity', 0.6) .attr('marker-end', 'url(#arrowhead)'); const node = g.append('g') .attr('class', 'nodes') .selectAll('g') .data(graph.nodes) .enter() .append('g') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); const nodeGroups = node.append('circle') .attr('r', d => Math.max(5, d.size)) .attr('fill', d => `url(#grad-${d.category})`) .attr('stroke', d => categoryColors[d.category]?.stroke || '#fff') .attr('stroke-width', 2) .style('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))') .style('cursor', 'grab') .on('mouseover', (event, d) => { highlightConnections(d, nodeConnections); showNodeTooltip(event, d); }) .on('mouseout', () => { resetHighlight(); hideNodeTooltip(); }) .on('click', (event, d) => { togglePin(d); showNodeDetail(event, d); }) .on('dblclick', (event, d) => { event.stopPropagation(); zoomToNode(d); }); const pinIcon = node.append('text') .attr('text-anchor', 'middle') .attr('dy', '0.35em') .attr('font-size', '12px') .attr('fill', 'white') .style('pointer-events', 'none') .style('opacity', d => pinnedNodes.has(d.id) ? 1 : 0) .text('📌'); const labels = g.append('g') .attr('class', 'labels') .selectAll('text') .data(graph.nodes) .enter() .append('text') .text(d => d.name.replace(/\n/g, ' ').substring(0, 20) + (d.name.length > 20 ? '...' : '')) .attr('font-size', '11px') .attr('fill', '#e0e0e0') .attr('text-anchor', 'middle') .attr('dy', d => -Math.max(8, d.size) - 5) .style('pointer-events', 'none') .style('text-shadow', '0 1px 3px rgba(0,0,0,0.8)') .style('opacity', 0.9); simulation = d3.forceSimulation(graph.nodes) .force('link', d3.forceLink(graph.edges).id(d => d.id).distance(80).strength(0.5)) .force('charge', d3.forceManyBody().strength(-200)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(d => Math.max(8, d.size) + 5)) .force('x', d3.forceX(width / 2).strength(0.05)) .force('y', d3.forceY(height / 2).strength(0.05)) .on('tick', ticked); function ticked() { 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); nodeGroups .attr('cx', d => d.x) .attr('cy', d => d.y); node.attr('transform', d => `translate(${d.x},${d.y})`); labels .attr('x', d => d.x) .attr('y', d => d.y); } function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; nodeGroups.style('cursor', 'grabbing'); } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); if (!pinnedNodes.has(d.id)) { d.fx = null; d.fy = null; } nodeGroups.style('cursor', 'grab'); } function highlightConnections(d, connections) { const connectedIds = connections.get(d.id) || new Set(); connectedIds.add(d.id); nodeGroups .style('opacity', n => connectedIds.has(n.id) ? 1 : 0.1) .style('filter', n => { if (connectedIds.has(n.id) && n.id !== d.id) { return 'drop-shadow(0 0 8px ' + categoryColors[n.category]?.fill + ')'; } if (n.id === d.id) { return 'drop-shadow(0 0 12px white) drop-shadow(0 4px 8px rgba(0,0,0,0.5))'; } return 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))'; }); link.style('opacity', l => { return (l.source.id === d.id || l.target.id === d.id) ? 1 : 0.1; }); labels.style('opacity', n => connectedIds.has(n.id) ? 0.9 : 0.1); } function resetHighlight() { nodeGroups .style('opacity', 1) .style('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.3))'); link.style('opacity', 0.6); labels.style('opacity', showLabels ? 0.9 : 0); } function filterByCategory() { const visibleNodes = graph.nodes.filter(n => !hiddenCategories.has(n.category)); const visibleNodeIds = new Set(visibleNodes.map(n => n.id)); nodeGroups.style('display', n => hiddenCategories.has(n.category) ? 'none' : 'block'); labels.style('display', n => hiddenCategories.has(n.category) ? 'none' : 'block'); link.style('display', l => { const sourceVisible = visibleNodeIds.has(l.source.id); const targetVisible = visibleNodeIds.has(l.target.id); return sourceVisible && targetVisible ? 'block' : 'none'; }); if (!showIsolated) { const connectedNodeIds = new Set(); graph.edges.forEach(e => { connectedNodeIds.add(e.source.id); connectedNodeIds.add(e.target.id); }); nodeGroups.style('display', n => { if (hiddenCategories.has(n.category)) return 'none'; if (!showIsolated && !connectedNodeIds.has(n.id)) return 'none'; return 'block'; }); labels.style('display', n => { if (hiddenCategories.has(n.category)) return 'none'; if (!showIsolated && !connectedNodeIds.has(n.id)) return 'none'; return 'block'; }); } simulation.alpha(0.3).restart(); } function toggleIsolated() { showIsolated = !showIsolated; const btn = document.getElementById('show-isolated'); if (btn) { btn.textContent = showIsolated ? 'Hide Isolated' : 'Show Isolated'; } filterByCategory(); } function togglePin(d) { if (pinnedNodes.has(d.id)) { pinnedNodes.delete(d.id); pinIcon.style('opacity', 0); d.fx = null; d.fy = null; } else { pinnedNodes.add(d.id); pinIcon.style('opacity', 1); d.fx = d.x; d.fy = d.y; } simulation.alpha(0.3).restart(); } function zoomToNode(d) { const padding = 100; const scale = Math.min(width, height) / (d.size * 10); networkGraph.svg.transition() .duration(750) .call(networkGraph.zoom.transform, d3.zoomIdentity .translate(width / 2, height / 2) .scale(scale) .translate(-d.x, -d.y) ); } networkGraph = { svg, g, zoom, node, labels, nodeGroups, pinIcon }; } function showNodeTooltip(event, d) { const tooltip = d3.select('body').append('div') .attr('class', 'tooltip') .style('position', 'absolute') .style('background', '#242424') .style('color', '#e0e0e0') .style('padding', '10px') .style('border-radius', '4px') .style('pointer-events', 'none') .style('z-index', '1000') .style('border', '1px solid #444') .html(` ${d.name}
Category: ${d.category}
Frequency: ${d.frequency}
Coefficient: ${d.coefficient.toFixed(3)} `); tooltip.style('left', (event.pageX + 10) + 'px') .style('top', (event.pageY - 10) + 'px'); } function hideNodeTooltip() { d3.selectAll('.tooltip').remove(); } function showNodeDetail(event, d) { const variable = window.appData.variables.find(v => v.name === d.name); if (variable) { if (typeof showVariableDetail === 'function') { showVariableDetail(variable); } } } function resetZoom() { if (networkGraph && networkGraph.zoom) { networkGraph.svg.transition() .duration(750) .call(networkGraph.zoom.transform, d3.zoomIdentity); } } function toggleLabels() { showLabels = !showLabels; if (networkGraph && networkGraph.labels) { networkGraph.labels .transition() .duration(300) .style('opacity', showLabels ? 0.9 : 0); } } function showNodeTooltip(event, d) { const connections = Array.from(networkGraph.svg.selectAll('line') .filter(l => l.source.id === d.id || l.target.id === d.id) .data()) .map(l => l.source.id === d.id ? l.target.name : l.source.name) .slice(0, 5); const tooltip = d3.select('body').append('div') .attr('class', 'tooltip') .style('position', 'absolute') .style('background', 'rgba(26, 26, 26, 0.95)') .style('color', '#e0e0e0') .style('padding', '12px') .style('border-radius', '8px') .style('pointer-events', 'none') .style('z-index', '1000') .style('border', '1px solid #444') .style('box-shadow', '0 4px 12px rgba(0,0,0,0.4)') .style('max-width', '250px') .html(`
${d.name}
Category: ${d.category}
Frequency: ${d.frequency}
Coefficient: ${d.coefficient.toFixed(3)}
${connections.length > 0 ? `
Connected to:
${connections.map(c => `
• ${c.substring(0, 25)}${c.length > 25 ? '...' : ''}
`).join('')}
` : ''} `); const tooltipWidth = 250; const tooltipHeight = 150; let left = event.pageX + 15; let top = event.pageY + 15; if (left + tooltipWidth > window.innerWidth) { left = event.pageX - tooltipWidth - 15; } if (top + tooltipHeight > window.innerHeight) { top = event.pageY - tooltipHeight - 15; } tooltip.style('left', left + 'px') .style('top', top + 'px'); } window.addEventListener('resize', function() { if (currentTab === 'network') { initializeNetworkGraph(); } });