Production Environment Fixes: - Enhanced SocketIO configuration for proxy compatibility - Added fallback polling mechanism when WebSocket fails - Fixed environment configuration (FLASK_ENV vs FLASK_CONFIG) - Added production-friendly debug logging for distribution chart - Improved connection status monitoring and error handling Proxy-Specific Improvements: - Enhanced CORS and transport settings for SocketIO - Fallback to HTTP polling when WebSocket connections fail - Better error handling and retry mechanisms - Debug logging that works in production mode This should resolve the wealth distribution histogram issue when running behind Nginx proxy in Docker containers.
1003 lines
34 KiB
JavaScript
1003 lines
34 KiB
JavaScript
// 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 = `
|
|
<strong>⚠️ r > g Detected!</strong><br>
|
|
<small>Capital rate (${(capitalRate * 100).toFixed(1)}%) > Growth rate (${(growthRate * 100).toFixed(1)}%)</small><br>
|
|
<small>Wealth inequality will increase over time</small>
|
|
`;
|
|
} else if (capitalRate === growthRate) {
|
|
warning.style.display = 'block';
|
|
warning.className = 'alert alert-warning';
|
|
warning.innerHTML = `
|
|
<strong>⚖️ r = g</strong><br>
|
|
<small>Balanced scenario - moderate inequality growth expected</small>
|
|
`;
|
|
} else {
|
|
warning.style.display = 'block';
|
|
warning.className = 'alert alert-success';
|
|
warning.innerHTML = `
|
|
<strong>✅ r < g</strong><br>
|
|
<small>Growth rate exceeds capital rate - inequality may decrease</small>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
});
|
|
|
|
// 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:<br>' + errors.join('<br>'),
|
|
'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; |