Files
capitalism-eats-the-world/app/static/js/simulation.js

1510 lines
52 KiB
JavaScript

/**
* 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 = `
<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: [], // 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:<br>' + errors.join('<br>'),
'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;