/** * 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); } }; /** * Test distribution data generation * Call from browser console: testDistributionData() */ window.testDistributionData = async function() { debugLog('=== DISTRIBUTION DATA TEST ==='); try { // First check if we have a simulation ID const simulationId = currentSimulation.id; if (!simulationId) { debugLog('❌ No active simulation found'); return; } // Fetch distribution data directly from API debugLog('Fetching distribution data from API...'); const response = await fetch(`/api/simulation/${simulationId}/distribution?bins=5`); const data = await response.json(); debugLog('Distribution API response', data); if (data.histogram) { debugLog('Histogram data', { labels: data.histogram.labels, counts: data.histogram.counts, total_agents: data.histogram.total_agents }); // Try to update chart with this data if (charts.distribution && data.histogram.labels && data.histogram.counts) { debugLog('Attempting to update chart with API data...'); try { charts.distribution.data.labels = data.histogram.labels; charts.distribution.data.datasets[0].data = data.histogram.counts; charts.distribution.update(); debugLog('✅ Chart updated successfully with API data'); } catch (error) { debugLog('❌ Error updating chart with API data', error); } } else { debugLog('❌ Cannot update chart - missing chart instance or data'); } } else { debugLog('❌ No histogram data in response'); } } catch (error) { debugLog('❌ Error fetching distribution data', error); } debugLog('=== DISTRIBUTION DATA TEST COMPLETE ==='); }; /** * 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 = false; /** * 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: [], // Initialize with empty arrays datasets: [{ label: 'Number of Agents', data: [], // Initialize with empty array 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' }, beginAtZero: true // Ensure y-axis starts at zero } } } }); 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'); onSimulationProgress(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 { // Use enhanced API request with batching support const response = await enhancedApiRequest( `/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true` ); if (response.latest_snapshot) { // Simulate progress update onSimulationProgress({ 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 using enhanced API request const createResponse = await enhancedApiRequest('/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 using enhanced API request await enhancedApiRequest(`/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 { // Use enhanced API request with batching support await enhancedApiRequest(`/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; }); } } /** * Calculate adaptive update interval based on simulation parameters * Reduces update frequency for larger simulations to improve performance */ function getAdaptiveUpdateInterval() { if (!currentSimulation.parameters.num_agents || !currentSimulation.parameters.iterations) { return 50; // Default to 50ms } const agentCount = currentSimulation.parameters.num_agents; const iterationCount = currentSimulation.parameters.iterations; // Base interval (ms) let interval = 50; // Increase interval for larger simulations if (agentCount > 1000) { interval = Math.min(200, 50 + (agentCount / 100)); } else if (agentCount > 500) { interval = 100; } // Further adjust based on iteration count if (iterationCount > 50000) { interval *= 2; } else if (iterationCount > 10000) { interval *= 1.5; } // Cap at reasonable values return Math.min(500, Math.max(20, interval)); } /** * Handle simulation progress updates */ function onSimulationProgress(data) { // Update progress bar const progressBar = document.getElementById('simulationProgressBar'); const progressText = document.getElementById('progressText'); if (progressBar && progressText && data.progress_percentage !== undefined) { const percentage = Math.min(100, Math.max(0, data.progress_percentage)); progressBar.style.width = percentage + '%'; progressText.textContent = percentage.toFixed(1) + '%'; } // Update charts and metrics only if we have new data if (data.iteration !== undefined) { // Only update time series data occasionally to improve performance const shouldUpdateSeries = currentSimulation.data.iterations.length === 0 || data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 200)) === 0 || data.iteration === (currentSimulation.parameters.iterations - 1); if (shouldUpdateSeries) { 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 if (data.distribution && Array.isArray(data.distribution.labels) && Array.isArray(data.distribution.counts) && data.distribution.labels.length > 0 && data.distribution.counts.length > 0) { // Store the distribution data properly currentSimulation.data.distribution.labels = [...data.distribution.labels]; currentSimulation.data.distribution.counts = [...data.distribution.counts]; } else if (data.iteration % Math.max(1, Math.floor(currentSimulation.parameters.iterations / 50)) === 0) { // Periodically fetch distribution data if not provided in the update // But less frequently for larger simulations fetchDistributionData().then(histogram => { if (histogram && histogram.labels && histogram.counts) { currentSimulation.data.distribution.labels = [...histogram.labels]; currentSimulation.data.distribution.counts = [...histogram.counts]; } }); } // Adaptive throttling based on simulation size const adaptiveInterval = getAdaptiveUpdateInterval(); if (!window.lastChartUpdate || (Date.now() - window.lastChartUpdate) > adaptiveInterval) { updateCharts(); window.lastChartUpdate = Date.now(); } updateMetricsDisplay(data); } } /** * Sample data for chart rendering to improve performance with large datasets * @param {Array} data - Array of data points * @param {number} maxPoints - Maximum number of points to display * @returns {Array} - Sampled data */ function sampleDataForChart(data, maxPoints = 200) { if (!Array.isArray(data) || data.length <= maxPoints) { return data; } const sampled = []; const step = Math.ceil(data.length / maxPoints); for (let i = 0; i < data.length; i += step) { sampled.push(data[i]); } return sampled; } /** * Sample time series data for chart rendering * @param {Object} chartData - Object containing time series data arrays * @param {number} maxPoints - Maximum number of points to display * @returns {Object} - Object with sampled data arrays */ function sampleTimeSeriesData(chartData, maxPoints = 200) { if (!chartData.iterations || chartData.iterations.length <= maxPoints) { return chartData; } const step = Math.ceil(chartData.iterations.length / maxPoints); return { iterations: sampleDataForChart(chartData.iterations, maxPoints), totalWealth: sampleDataForChart(chartData.totalWealth, maxPoints), giniCoefficients: sampleDataForChart(chartData.giniCoefficients, maxPoints), capitalShare: sampleDataForChart(chartData.capitalShare, maxPoints), top10Share: sampleDataForChart(chartData.top10Share, maxPoints) }; } /** * Update charts with new data using sampling optimization */ function updateCharts() { // Determine maximum points based on agent count for better performance const maxChartPoints = currentSimulation.parameters.num_agents > 1000 ? 100 : 200; // Wealth Evolution Chart if (charts.wealthEvolution) { // Sample data for large datasets const sampledData = sampleTimeSeriesData({ iterations: currentSimulation.data.iterations, totalWealth: currentSimulation.data.totalWealth }, maxChartPoints); charts.wealthEvolution.data.labels = sampledData.iterations; charts.wealthEvolution.data.datasets[0].data = sampledData.totalWealth; charts.wealthEvolution.update('none'); } // Inequality Chart if (charts.inequality) { // Sample data for large datasets const sampledData = sampleTimeSeriesData({ iterations: currentSimulation.data.iterations, giniCoefficients: currentSimulation.data.giniCoefficients, top10Share: currentSimulation.data.top10Share }, maxChartPoints); charts.inequality.data.labels = sampledData.iterations; charts.inequality.data.datasets[0].data = sampledData.giniCoefficients; charts.inequality.data.datasets[1].data = sampledData.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') }); // Ensure we have valid data before updating if (distData.labels && distData.labels.length > 0 && distData.counts && distData.counts.length > 0) { try { // Make sure we're working with arrays const labels = Array.isArray(distData.labels) ? distData.labels : []; const counts = Array.isArray(distData.counts) ? distData.counts : []; charts.distribution.data.labels = labels; charts.distribution.data.datasets[0].data = 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...'); // Use enhanced API request with batching support const response = await enhancedApiRequest( `/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...'); // Use enhanced API request with batching support const distResponse = await enhancedApiRequest( `/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); } /** * Calculate optimal bin count for distribution chart based on agent count * @param {number} agentCount - Number of agents in the simulation * @returns {number} - Optimal number of bins */ function getOptimalBinCount(agentCount) { if (agentCount < 50) { return Math.max(5, Math.floor(agentCount / 5)); } else if (agentCount < 200) { return 10; } else if (agentCount < 1000) { return 15; } else if (agentCount < 5000) { return 20; } else { return 25; // Cap at 25 bins for very large simulations } } /** * Fetch distribution data with dynamic bin count using enhanced API requests */ async function fetchDistributionData() { if (!currentSimulation.id) return null; try { // Calculate optimal bin count based on agent count const binCount = getOptimalBinCount(currentSimulation.parameters.num_agents || 100); // Use enhanced API request with batching support const response = await enhancedApiRequest( `/api/simulation/${currentSimulation.id}/distribution?bins=${binCount}` ); return response.histogram; } catch (error) { debugLog('Error fetching distribution data', error); return null; } } /** * Batch API requests to reduce network overhead */ class ApiRequestBatcher { constructor() { this.pendingRequests = []; this.batchTimeout = null; this.maxBatchSize = 5; } /** * Add a request to the batch */ addRequest(url, options = {}) { return new Promise((resolve, reject) => { this.pendingRequests.push({ url, options, resolve, reject }); // If we've reached max batch size, flush immediately if (this.pendingRequests.length >= this.maxBatchSize) { this.flush(); } else if (!this.batchTimeout) { // Otherwise, schedule a flush this.batchTimeout = setTimeout(() => this.flush(), 50); } }); } /** * Flush all pending requests */ async flush() { if (this.batchTimeout) { clearTimeout(this.batchTimeout); this.batchTimeout = null; } if (this.pendingRequests.length === 0) return; // For now, we'll process requests individually since our API doesn't support batching // In a real implementation, this would send a single batched request const requests = [...this.pendingRequests]; this.pendingRequests = []; // Process all requests for (const request of requests) { try { const response = await fetch(request.url, request.options); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); request.resolve(data); } catch (error) { request.reject(error); } } } } // Create a global instance window.apiBatcher = new ApiRequestBatcher(); /** * Enhanced API request function with batching support */ async function enhancedApiRequest(url, options = {}) { // Use batching for certain types of requests const shouldBatch = url.includes('/data') || url.includes('/distribution'); if (shouldBatch) { return window.apiBatcher.addRequest(url, options); } else { // Fall back to regular API request for non-batchable requests return window.MarkovEconomics.utils.apiRequest(url, options); } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Initial inequality warning update updateInequalityWarning(); }); // Export to global scope window.initializeSimulation = initializeSimulation;