/** * 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: [] } }; // 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'); if (distributionCtx) { 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' } } } } }); } } /** * 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 */ function initializeRealtimeUpdates() { if (!window.MarkovEconomics.socket) { console.warn('Socket.IO not available - real-time updates disabled'); return; } const socket = window.MarkovEconomics.socket; // Simulation progress updates socket.on('simulation_progress', function(data) { updateSimulationProgress(data); }); // Simulation completion socket.on('simulation_complete', function(data) { onSimulationComplete(data); }); // Simulation stopped socket.on('simulation_stopped', function(data) { onSimulationStopped(data); }); // Simulation error socket.on('simulation_error', function(data) { onSimulationError(data); }); } /** * 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; // Join simulation room for real-time updates if (window.MarkovEconomics.socket) { window.MarkovEconomics.socket.emit('join_simulation', { simulation_id: currentSimulation.id }); } // Start simulation await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, { method: 'POST' }); currentSimulation.isRunning = true; updateUIState('running'); 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'); 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: [] } }; // 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); 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'); } } /** * 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 */ function onSimulationComplete(data) { currentSimulation.isRunning = false; updateUIState('complete'); window.MarkovEconomics.utils.showNotification( `Simulation completed! ${data.total_snapshots} data points collected.`, 'success' ); } /** * 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;