/** * Test WebSocket connectivity and chart update * Call from browser console: testWebSocket() */ window.testWebSocket = async function() { debugLog('=== WEBSOCKET CONNECTIVITY TEST ==='); try { const response = await fetch('/api/test-websocket'); const result = await response.json(); debugLog('WebSocket test API response', result); if (result.status === 'success') { debugLog('✅ WebSocket test message sent from server'); debugLog('Wait for incoming WebSocket message...'); } else { debugLog('❌ WebSocket test API failed', result); } } catch (error) { debugLog('❌ WebSocket test API error', error); } }; /** * Enhanced distribution update test function * Call from browser console: testDistributionUpdate() */ window.testDistributionUpdate = async function() { debugLog('=== ENHANCED DISTRIBUTION UPDATE TEST ==='); // Test 1: Check current state debugLog('Current state check', { chartExists: !!charts.distribution, canvasExists: !!document.getElementById('distributionChart'), simulationId: currentSimulation.id, distributionData: currentSimulation.data.distribution }); // Test 2: Try to fetch distribution from backend directly try { const debugResponse = await fetch('/debug'); const debugData = await debugResponse.json(); debugLog('Backend debug data', debugData.distribution_test); if (debugData.distribution_test) { // Test updating chart with backend data const testData = debugData.distribution_test; if (charts.distribution && testData.labels && testData.counts) { debugLog('Testing with backend debug data', testData); charts.distribution.data.labels = testData.labels; charts.distribution.data.datasets[0].data = testData.counts; charts.distribution.update(); debugLog('✅ Backend debug data update: SUCCESS'); } } } catch (error) { debugLog('❌ Backend debug test failed', error); } // Test 3: Check canvas rendering const canvas = document.getElementById('distributionChart'); if (canvas) { debugLog('Canvas properties', { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight, style: canvas.style.cssText }); } debugLog('=== ENHANCED TEST COMPLETE ==='); }; /** * Test simulation flow for distribution data * Call from browser console: testSimulationFlow() */ window.testSimulationFlow = async function() { debugLog('=== SIMULATION FLOW TEST ==='); // Test 1: Create test simulation try { const testParams = { r_rate: 0.05, g_rate: 0.03, num_agents: 20, iterations: 50, initial_capital: 1000, initial_consumption: 1000 }; debugLog('Creating test simulation', testParams); const createResponse = await fetch('/api/simulation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(testParams) }); const createData = await createResponse.json(); debugLog('Test simulation created', createData); if (createData.simulation_id) { // Test 2: Start simulation and monitor debugLog('Starting test simulation'); const startResponse = await fetch(`/api/simulation/${createData.simulation_id}/start`, { method: 'POST' }); if (startResponse.ok) { debugLog('Test simulation started, monitoring for 5 seconds...'); // Monitor for updates let monitorCount = 0; const monitorInterval = setInterval(async () => { try { const dataResponse = await fetch(`/api/simulation/${createData.simulation_id}/data?include_distribution=true`); const simData = await dataResponse.json(); debugLog(`Monitor ${monitorCount}`, { iteration: simData.latest_snapshot?.iteration, hasDistribution: !!simData.distribution, distribution: simData.distribution }); monitorCount++; if (monitorCount >= 5) { clearInterval(monitorInterval); debugLog('Test monitoring complete'); } } catch (error) { debugLog('Monitor error', error); clearInterval(monitorInterval); } }, 1000); } } } catch (error) { debugLog('❌ Simulation flow test failed', error); } debugLog('=== SIMULATION FLOW TEST STARTED ==='); }; /** * Production debugging test function * Call from browser console: testDistributionChart() */ window.testDistributionChart = function() { debugLog('=== DISTRIBUTION CHART DIAGNOSTIC TEST ==='); // Test 1: Check if elements exist const canvas = document.getElementById('distributionChart'); debugLog('Canvas element found', !!canvas); // Test 2: Check Chart.js availability debugLog('Chart.js available', typeof Chart !== 'undefined'); // Test 3: Check chart instance debugLog('Chart instance exists', !!charts.distribution); // Test 4: Check current data debugLog('Current simulation data', { hasId: !!currentSimulation.id, isRunning: currentSimulation.isRunning, distributionData: currentSimulation.data.distribution }); // Test 5: Check WebSocket connection debugLog('WebSocket status', { socketExists: !!window.MarkovEconomics.socket, isConnected: window.MarkovEconomics ? window.MarkovEconomics.isConnected : 'unknown' }); // Test 6: Manually test API endpoint if (currentSimulation.id) { fetch(`/api/simulation/${currentSimulation.id}/distribution?bins=5`) .then(response => response.json()) .then(data => { debugLog('Manual API test result', data); }) .catch(error => { debugLog('Manual API test failed', error); }); } // Test 7: Try to create test data and update chart if (charts.distribution) { const testLabels = ['$0-100', '$100-200', '$200-300']; const testCounts = [5, 8, 2]; try { charts.distribution.data.labels = testLabels; charts.distribution.data.datasets[0].data = testCounts; charts.distribution.update(); debugLog('Manual chart update test: SUCCESS'); } catch (error) { debugLog('Manual chart update test: FAILED', error); } } debugLog('=== TEST COMPLETE ==='); }; // Debug flag - can be enabled even in production for troubleshooting const DEBUG_DISTRIBUTION = true; /** * Debug logger that works in production */ function debugLog(message, data = null) { if (DEBUG_DISTRIBUTION) { if (data) { console.log(`[DISTRIBUTION DEBUG] ${message}:`, data); } else { console.log(`[DISTRIBUTION DEBUG] ${message}`); } } } /** * Simulation Control and Visualization * * Handles the main simulation interface, real-time charting, * and parameter control for the Markov Economics application. */ // Simulation state let currentSimulation = { id: null, isRunning: false, parameters: {}, data: { iterations: [], totalWealth: [], giniCoefficients: [], capitalShare: [], top10Share: [], distribution: { labels: [], counts: [] } } }; // Chart instances let charts = { wealthEvolution: null, inequality: null, distribution: null }; /** * Initialize the simulation interface */ function initializeSimulation() { console.log('Initializing simulation interface...'); // Initialize parameter controls initializeParameterControls(); // Initialize charts initializeCharts(); // Initialize event listeners initializeEventListeners(); // Initialize real-time updates initializeRealtimeUpdates(); console.log('✅ Simulation interface ready'); } /** * Initialize parameter controls with sliders */ function initializeParameterControls() { const controls = [ { id: 'capitalRate', valueId: 'capitalRateValue', suffix: '%', scale: 100 }, { id: 'growthRate', valueId: 'growthRateValue', suffix: '%', scale: 100 }, { id: 'numAgents', valueId: 'numAgentsValue', suffix: '', scale: 1 }, { id: 'iterations', valueId: 'iterationsValue', suffix: '', scale: 1 } ]; controls.forEach(control => { const slider = document.getElementById(control.id); const valueDisplay = document.getElementById(control.valueId); if (slider && valueDisplay) { slider.addEventListener('input', function() { const value = parseFloat(this.value); const displayValue = (value / control.scale).toFixed(control.scale === 100 ? 1 : 0); valueDisplay.textContent = displayValue + control.suffix; // Update inequality warning updateInequalityWarning(); }); } }); } /** * Update inequality warning based on r vs g */ function updateInequalityWarning() { const capitalRate = parseFloat(document.getElementById('capitalRate').value) / 100; const growthRate = parseFloat(document.getElementById('growthRate').value) / 100; const warning = document.getElementById('inequalityAlert'); if (warning) { if (capitalRate > growthRate) { warning.style.display = 'block'; warning.className = 'alert alert-danger'; warning.innerHTML = ` ⚠️ r > g Detected!
Capital rate (${(capitalRate * 100).toFixed(1)}%) > Growth rate (${(growthRate * 100).toFixed(1)}%)
Wealth inequality will increase over time `; } else if (capitalRate === growthRate) { warning.style.display = 'block'; warning.className = 'alert alert-warning'; warning.innerHTML = ` ⚖️ r = g
Balanced scenario - moderate inequality growth expected `; } else { warning.style.display = 'block'; warning.className = 'alert alert-success'; warning.innerHTML = ` ✅ r < g
Growth rate exceeds capital rate - inequality may decrease `; } } } /** * Initialize Chart.js instances */ function initializeCharts() { // Wealth Evolution Chart const wealthCtx = document.getElementById('wealthEvolutionChart'); if (wealthCtx) { charts.wealthEvolution = new Chart(wealthCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'Total Wealth', data: [], borderColor: '#007bff', backgroundColor: 'rgba(0, 123, 255, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Wealth Accumulation Over Time' }, legend: { display: false } }, scales: { x: { title: { display: true, text: 'Iteration' } }, y: { title: { display: true, text: 'Total Wealth ($)' }, ticks: { callback: function(value) { return window.MarkovEconomics.utils.formatCurrency(value); } } } }, animation: { duration: 300 } } }); } // Inequality Metrics Chart const inequalityCtx = document.getElementById('inequalityChart'); if (inequalityCtx) { charts.inequality = new Chart(inequalityCtx, { type: 'line', data: { labels: [], datasets: [ { label: 'Gini Coefficient', data: [], borderColor: '#dc3545', backgroundColor: 'rgba(220, 53, 69, 0.1)', yAxisID: 'y', tension: 0.4 }, { label: 'Top 10% Share', data: [], borderColor: '#fd7e14', backgroundColor: 'rgba(253, 126, 20, 0.1)', yAxisID: 'y1', tension: 0.4 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Inequality Metrics' } }, scales: { x: { title: { display: true, text: 'Iteration' } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'Gini Coefficient' }, min: 0, max: 1 }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Top 10% Share (%)' }, grid: { drawOnChartArea: false, }, ticks: { callback: function(value) { return (value * 100).toFixed(0) + '%'; } } } }, animation: { duration: 300 } } }); } // Wealth Distribution Chart const distributionCtx = document.getElementById('distributionChart'); debugLog('Initializing distribution chart', { hasCanvas: !!distributionCtx, canvasId: distributionCtx ? distributionCtx.id : 'not found', chartJsAvailable: typeof Chart !== 'undefined' }); if (distributionCtx) { try { charts.distribution = new Chart(distributionCtx, { type: 'bar', data: { labels: [], datasets: [{ label: 'Number of Agents', data: [], backgroundColor: 'rgba(40, 167, 69, 0.7)', borderColor: '#28a745', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Wealth Distribution' }, legend: { display: false } }, scales: { x: { title: { display: true, text: 'Wealth Range' } }, y: { title: { display: true, text: 'Number of Agents' } } } } }); debugLog('✅ Distribution chart initialized successfully'); } catch (error) { debugLog('❌ Error initializing distribution chart', error); } } else { debugLog('❌ Distribution chart canvas not found!'); } } /** * Initialize event listeners */ function initializeEventListeners() { // Start simulation button const startBtn = document.getElementById('startBtn'); if (startBtn) { startBtn.addEventListener('click', startSimulation); } // Stop simulation button const stopBtn = document.getElementById('stopBtn'); if (stopBtn) { stopBtn.addEventListener('click', stopSimulation); } // Reset button const resetBtn = document.getElementById('resetBtn'); if (resetBtn) { resetBtn.addEventListener('click', resetSimulation); } // Export buttons const exportJsonBtn = document.getElementById('exportJsonBtn'); if (exportJsonBtn) { exportJsonBtn.addEventListener('click', () => exportData('json')); } const exportCsvBtn = document.getElementById('exportCsvBtn'); if (exportCsvBtn) { exportCsvBtn.addEventListener('click', () => exportData('csv')); } } /** * Initialize real-time updates via Socket.IO with fallback polling */ function initializeRealtimeUpdates() { if (!window.MarkovEconomics.socket) { debugLog('Socket.IO not available - real-time updates disabled'); return; } const socket = window.MarkovEconomics.socket; // Monitor connection status socket.on('connect', function() { debugLog('Socket.IO connected successfully'); window.MarkovEconomics.isConnected = true; }); socket.on('disconnect', function() { debugLog('Socket.IO disconnected'); window.MarkovEconomics.isConnected = false; // Start fallback polling if simulation is running if (currentSimulation.isRunning) { debugLog('Starting fallback polling due to disconnection'); startFallbackPolling(); } }); socket.on('connect_error', function(error) { debugLog('Socket.IO connection error', error); window.MarkovEconomics.isConnected = false; // Start fallback polling if (currentSimulation.isRunning) { debugLog('Starting fallback polling due to connection error'); startFallbackPolling(); } }); // WebSocket test handler socket.on('websocket_test', function(data) { debugLog('WebSocket test message received', data); // Test updating the distribution chart with test data if (charts.distribution && data.distribution) { try { charts.distribution.data.labels = data.distribution.labels; charts.distribution.data.datasets[0].data = data.distribution.counts; charts.distribution.update(); debugLog('✅ WebSocket test: Chart updated successfully with test data'); } catch (error) { debugLog('❌ WebSocket test: Chart update failed', error); } } }); // Simulation progress updates socket.on('simulation_progress', function(data) { debugLog('Received real-time progress update'); updateSimulationProgress(data); }); // Simulation completion socket.on('simulation_complete', function(data) { debugLog('Received simulation complete event'); onSimulationComplete(data); }); // Simulation stopped socket.on('simulation_stopped', function(data) { onSimulationStopped(data); }); // Simulation error socket.on('simulation_error', function(data) { onSimulationError(data); }); } /** * Fallback polling mechanism when WebSocket fails */ let fallbackPollingInterval = null; function startFallbackPolling() { if (fallbackPollingInterval) { clearInterval(fallbackPollingInterval); } debugLog('Starting fallback polling mechanism'); fallbackPollingInterval = setInterval(async () => { if (!currentSimulation.isRunning || !currentSimulation.id) { stopFallbackPolling(); return; } try { const response = await window.MarkovEconomics.utils.apiRequest( `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` ); if (response.latest_snapshot) { // Simulate progress update updateSimulationProgress({ iteration: response.latest_snapshot.iteration, total_wealth: response.latest_snapshot.total_wealth, gini_coefficient: response.latest_snapshot.gini_coefficient, capital_share: response.latest_snapshot.capital_share, wealth_concentration_top10: response.latest_snapshot.wealth_concentration_top10, distribution: response.distribution, progress_percentage: (response.latest_snapshot.iteration / currentSimulation.parameters.iterations) * 100 }); } // Check if simulation is complete if (response.latest_snapshot && response.latest_snapshot.iteration >= currentSimulation.parameters.iterations) { debugLog('Simulation completed via fallback polling'); stopFallbackPolling(); onSimulationComplete({ total_snapshots: response.latest_snapshot.iteration }); } } catch (error) { debugLog('Fallback polling error', error); } }, 1000); // Poll every second } function stopFallbackPolling() { if (fallbackPollingInterval) { debugLog('Stopping fallback polling'); clearInterval(fallbackPollingInterval); fallbackPollingInterval = null; } } /** * Start a new simulation */ async function startSimulation() { try { // Get parameters from UI const parameters = getSimulationParameters(); // Validate parameters const errors = window.MarkovEconomics.utils.validateParameters(parameters); if (errors.length > 0) { window.MarkovEconomics.utils.showNotification( 'Parameter validation failed:
' + errors.join('
'), 'danger' ); return; } // Update UI state updateUIState('starting'); // Create simulation const createResponse = await window.MarkovEconomics.utils.apiRequest('/api/simulation', { method: 'POST', body: JSON.stringify(parameters) }); currentSimulation.id = createResponse.simulation_id; currentSimulation.parameters = parameters; debugLog('Simulation created', { id: currentSimulation.id, parameters: currentSimulation.parameters }); // Join simulation room for real-time updates if (window.MarkovEconomics.socket && window.MarkovEconomics.isConnected) { debugLog('Joining simulation room via WebSocket'); window.MarkovEconomics.socket.emit('join_simulation', { simulation_id: currentSimulation.id }); } else { debugLog('WebSocket not available, will use fallback polling'); } // Start simulation await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, { method: 'POST' }); currentSimulation.isRunning = true; updateUIState('running'); // Start fallback polling if WebSocket is not connected if (!window.MarkovEconomics.socket || !window.MarkovEconomics.isConnected) { debugLog('Starting fallback polling for simulation progress'); startFallbackPolling(); } window.MarkovEconomics.utils.showNotification('Simulation started successfully!', 'success'); } catch (error) { console.error('Failed to start simulation:', error); updateUIState('error'); } } /** * Stop the current simulation */ async function stopSimulation() { if (!currentSimulation.id) return; try { await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/stop`, { method: 'POST' }); currentSimulation.isRunning = false; updateUIState('stopped'); // Stop fallback polling if active stopFallbackPolling(); window.MarkovEconomics.utils.showNotification('Simulation stopped', 'info'); } catch (error) { console.error('Failed to stop simulation:', error); } } /** * Reset simulation state */ function resetSimulation() { currentSimulation = { id: null, isRunning: false, parameters: {}, data: { iterations: [], totalWealth: [], giniCoefficients: [], capitalShare: [], top10Share: [], distribution: { labels: [], counts: [] } } }; // Clear charts Object.values(charts).forEach(chart => { if (chart) { chart.data.labels = []; chart.data.datasets.forEach(dataset => { dataset.data = []; }); chart.update(); } }); // Reset metrics updateMetricsDisplay({ total_wealth: 0, gini_coefficient: 0, wealth_concentration_top10: 0, capital_share: 0 }); updateUIState('ready'); window.MarkovEconomics.utils.showNotification('Simulation reset', 'info'); } /** * Get current simulation parameters from UI */ function getSimulationParameters() { return { r_rate: parseFloat(document.getElementById('capitalRate').value) / 100, g_rate: parseFloat(document.getElementById('growthRate').value) / 100, num_agents: parseInt(document.getElementById('numAgents').value), iterations: parseInt(document.getElementById('iterations').value), initial_capital: 1000, initial_consumption: 1000 }; } /** * Update UI state based on simulation status */ function updateUIState(state) { const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); const resetBtn = document.getElementById('resetBtn'); const status = document.getElementById('simulationStatus'); const progress = document.getElementById('progressContainer'); const exportBtns = [ document.getElementById('exportJsonBtn'), document.getElementById('exportCsvBtn') ]; switch (state) { case 'starting': if (startBtn) startBtn.disabled = true; if (stopBtn) stopBtn.disabled = true; if (resetBtn) resetBtn.disabled = true; if (status) { status.textContent = 'Starting...'; status.className = 'simulation-status bg-warning text-dark'; } break; case 'running': if (startBtn) startBtn.disabled = true; if (stopBtn) stopBtn.disabled = false; if (resetBtn) resetBtn.disabled = true; if (status) { status.textContent = 'Running'; status.className = 'simulation-status status-running'; } if (progress) progress.style.display = 'block'; break; case 'complete': if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; if (resetBtn) resetBtn.disabled = false; if (status) { status.textContent = 'Complete'; status.className = 'simulation-status status-complete'; } if (progress) progress.style.display = 'none'; exportBtns.forEach(btn => { if (btn) btn.disabled = false; }); break; case 'stopped': if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; if (resetBtn) resetBtn.disabled = false; if (status) { status.textContent = 'Stopped'; status.className = 'simulation-status bg-warning text-dark'; } if (progress) progress.style.display = 'none'; break; case 'error': if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; if (resetBtn) resetBtn.disabled = false; if (status) { status.textContent = 'Error'; status.className = 'simulation-status status-error'; } if (progress) progress.style.display = 'none'; break; default: // 'ready' if (startBtn) startBtn.disabled = false; if (stopBtn) stopBtn.disabled = true; if (resetBtn) resetBtn.disabled = false; if (status) { status.textContent = 'Ready to start'; status.className = 'simulation-status status-ready'; } if (progress) progress.style.display = 'none'; exportBtns.forEach(btn => { if (btn) btn.disabled = true; }); } } /** * Update simulation progress */ function updateSimulationProgress(data) { // Update progress bar const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); if (progressBar && progressText) { const percentage = data.progress_percentage || 0; progressBar.style.width = percentage + '%'; progressText.textContent = percentage.toFixed(1) + '%'; } // Update charts and metrics if (data.iteration !== undefined) { currentSimulation.data.iterations.push(data.iteration); currentSimulation.data.totalWealth.push(data.total_wealth || 0); currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0); currentSimulation.data.capitalShare.push(data.capital_share || 0); currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0); // Update distribution data if available debugLog('Received simulation progress data', { hasDistribution: !!data.distribution, distribution: data.distribution, socketConnected: window.MarkovEconomics ? window.MarkovEconomics.isConnected : 'unknown' }); if (data.distribution && data.distribution.labels && data.distribution.counts) { debugLog('Updating distribution data', { labelsLength: data.distribution.labels.length, countsLength: data.distribution.counts.length, labels: data.distribution.labels, counts: data.distribution.counts }); currentSimulation.data.distribution.labels = [...data.distribution.labels]; currentSimulation.data.distribution.counts = [...data.distribution.counts]; } else { debugLog('No valid distribution data in progress update'); } updateCharts(); updateMetricsDisplay(data); } } /** * Update charts with new data */ function updateCharts() { // Wealth Evolution Chart if (charts.wealthEvolution) { charts.wealthEvolution.data.labels = currentSimulation.data.iterations; charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth; charts.wealthEvolution.update('none'); } // Inequality Chart if (charts.inequality) { charts.inequality.data.labels = currentSimulation.data.iterations; charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients; charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share; charts.inequality.update('none'); } // Wealth Distribution Chart if (charts.distribution) { const distData = currentSimulation.data.distribution; debugLog('Updating distribution chart with data', { hasLabels: !!(distData.labels && distData.labels.length > 0), hasCounts: !!(distData.counts && distData.counts.length > 0), labelsLength: distData.labels ? distData.labels.length : 0, countsLength: distData.counts ? distData.counts.length : 0, labels: distData.labels, counts: distData.counts, chartExists: !!charts.distribution, canvasElement: document.getElementById('distributionChart') }); if (distData.labels && distData.labels.length > 0 && distData.counts && distData.counts.length > 0) { try { charts.distribution.data.labels = distData.labels; charts.distribution.data.datasets[0].data = distData.counts; charts.distribution.update('none'); debugLog('✅ Distribution chart updated successfully'); } catch (error) { debugLog('❌ Error updating distribution chart', error); } } else { debugLog('❌ Distribution chart not updated - insufficient data'); } } else { debugLog('❌ Distribution chart not found', { chartsObject: charts, distributionElement: document.getElementById('distributionChart') }); } } /** * Update metrics display */ function updateMetricsDisplay(data) { const formatters = window.MarkovEconomics.utils; const totalWealthEl = document.getElementById('totalWealthMetric'); if (totalWealthEl) { totalWealthEl.textContent = formatters.formatCurrency(data.total_wealth || 0); } const giniEl = document.getElementById('giniMetric'); if (giniEl) { giniEl.textContent = (data.gini_coefficient || 0).toFixed(3); } const top10El = document.getElementById('top10Metric'); if (top10El) { top10El.textContent = formatters.formatPercentage(data.wealth_concentration_top10 || 0); } const capitalShareEl = document.getElementById('capitalShareMetric'); if (capitalShareEl) { capitalShareEl.textContent = formatters.formatPercentage(data.capital_share || 0); } } /** * Handle simulation completion */ async function onSimulationComplete(data) { currentSimulation.isRunning = false; updateUIState('complete'); // Fetch complete simulation data and populate charts try { debugLog('Fetching complete simulation data...'); const response = await window.MarkovEconomics.utils.apiRequest( `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` ); debugLog('Complete simulation data received', { hasEvolution: !!response.evolution, hasDistribution: !!response.distribution, distribution: response.distribution }); if (response.evolution) { // Clear and populate with complete data currentSimulation.data.iterations = response.evolution.iterations; currentSimulation.data.totalWealth = response.evolution.total_wealth; currentSimulation.data.giniCoefficients = response.evolution.gini_coefficients; currentSimulation.data.top10Share = response.evolution.top10_shares || []; currentSimulation.data.capitalShare = response.evolution.capital_shares || []; // Update distribution data if (response.distribution && response.distribution.labels && response.distribution.counts) { debugLog('Setting final distribution data', { labelsLength: response.distribution.labels.length, countsLength: response.distribution.counts.length, labels: response.distribution.labels, counts: response.distribution.counts }); currentSimulation.data.distribution.labels = [...response.distribution.labels]; currentSimulation.data.distribution.counts = [...response.distribution.counts]; } else { debugLog('No distribution data in final response'); // Fallback: try to get distribution data from dedicated endpoint try { debugLog('Attempting fallback distribution fetch...'); const distResponse = await window.MarkovEconomics.utils.apiRequest( `/api/simulation/${currentSimulation.id}/distribution?bins=10` ); if (distResponse.histogram && distResponse.histogram.labels && distResponse.histogram.counts) { debugLog('Got distribution data from dedicated endpoint', distResponse.histogram); currentSimulation.data.distribution.labels = [...distResponse.histogram.labels]; currentSimulation.data.distribution.counts = [...distResponse.histogram.counts]; } } catch (distError) { debugLog('Failed to fetch distribution data from dedicated endpoint', distError); } } // Update charts with complete data updateCharts(); // Update final metrics if (response.latest_snapshot) { updateMetricsDisplay(response.latest_snapshot); } } window.MarkovEconomics.utils.showNotification( `Simulation completed! ${data.total_snapshots} data points collected.`, 'success' ); } catch (error) { console.error('Failed to fetch simulation data:', error); window.MarkovEconomics.utils.showNotification( 'Simulation completed but failed to load results', 'warning' ); } } /** * Handle simulation stopped */ function onSimulationStopped(data) { currentSimulation.isRunning = false; updateUIState('stopped'); } /** * Handle simulation error */ function onSimulationError(data) { currentSimulation.isRunning = false; updateUIState('error'); window.MarkovEconomics.utils.showNotification( `Simulation error: ${data.error}`, 'danger' ); } /** * Export simulation data */ async function exportData(format) { if (!currentSimulation.id) { window.MarkovEconomics.utils.showNotification('No simulation data to export', 'warning'); return; } try { const response = await fetch(`/api/simulation/${currentSimulation.id}/export/${format}`); if (!response.ok) { throw new Error(`Export failed: ${response.statusText}`); } if (format === 'csv') { const csvData = await response.text(); downloadFile(csvData, `simulation_${currentSimulation.id.slice(0, 8)}.csv`, 'text/csv'); } else { const jsonData = await response.json(); downloadFile( JSON.stringify(jsonData.data, null, 2), `simulation_${currentSimulation.id.slice(0, 8)}.json`, 'application/json' ); } window.MarkovEconomics.utils.showNotification(`Data exported as ${format.toUpperCase()}`, 'success'); } catch (error) { console.error('Export failed:', error); window.MarkovEconomics.utils.showNotification(`Export failed: ${error.message}`, 'danger'); } } /** * Download file helper */ function downloadFile(content, filename, contentType) { const blob = new Blob([content], { type: contentType }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Initial inequality warning update updateInequalityWarning(); }); // Export to global scope window.initializeSimulation = initializeSimulation;