1510 lines
52 KiB
JavaScript
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; |