743 lines
23 KiB
JavaScript
743 lines
23 KiB
JavaScript
/**
|
|
* Simulation Control and Visualization
|
|
*
|
|
* Handles the main simulation interface, real-time charting,
|
|
* and parameter control for the Markov Economics application.
|
|
*/
|
|
|
|
// Simulation state
|
|
let currentSimulation = {
|
|
id: null,
|
|
isRunning: false,
|
|
parameters: {},
|
|
data: {
|
|
iterations: [],
|
|
totalWealth: [],
|
|
giniCoefficients: [],
|
|
capitalShare: [],
|
|
top10Share: []
|
|
}
|
|
};
|
|
|
|
// Chart instances
|
|
let charts = {
|
|
wealthEvolution: null,
|
|
inequality: null,
|
|
distribution: null
|
|
};
|
|
|
|
/**
|
|
* Initialize the simulation interface
|
|
*/
|
|
function initializeSimulation() {
|
|
console.log('Initializing simulation interface...');
|
|
|
|
// Initialize parameter controls
|
|
initializeParameterControls();
|
|
|
|
// Initialize charts
|
|
initializeCharts();
|
|
|
|
// Initialize event listeners
|
|
initializeEventListeners();
|
|
|
|
// Initialize real-time updates
|
|
initializeRealtimeUpdates();
|
|
|
|
console.log('✅ Simulation interface ready');
|
|
}
|
|
|
|
/**
|
|
* Initialize parameter controls with sliders
|
|
*/
|
|
function initializeParameterControls() {
|
|
const controls = [
|
|
{ id: 'capitalRate', valueId: 'capitalRateValue', suffix: '%', scale: 100 },
|
|
{ id: 'growthRate', valueId: 'growthRateValue', suffix: '%', scale: 100 },
|
|
{ id: 'numAgents', valueId: 'numAgentsValue', suffix: '', scale: 1 },
|
|
{ id: 'iterations', valueId: 'iterationsValue', suffix: '', scale: 1 }
|
|
];
|
|
|
|
controls.forEach(control => {
|
|
const slider = document.getElementById(control.id);
|
|
const valueDisplay = document.getElementById(control.valueId);
|
|
|
|
if (slider && valueDisplay) {
|
|
slider.addEventListener('input', function() {
|
|
const value = parseFloat(this.value);
|
|
const displayValue = (value / control.scale).toFixed(control.scale === 100 ? 1 : 0);
|
|
valueDisplay.textContent = displayValue + control.suffix;
|
|
|
|
// Update inequality warning
|
|
updateInequalityWarning();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update inequality warning based on r vs g
|
|
*/
|
|
function updateInequalityWarning() {
|
|
const capitalRate = parseFloat(document.getElementById('capitalRate').value) / 100;
|
|
const growthRate = parseFloat(document.getElementById('growthRate').value) / 100;
|
|
const warning = document.getElementById('inequalityAlert');
|
|
|
|
if (warning) {
|
|
if (capitalRate > growthRate) {
|
|
warning.style.display = 'block';
|
|
warning.className = 'alert alert-danger';
|
|
warning.innerHTML = `
|
|
<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');
|
|
if (distributionCtx) {
|
|
charts.distribution = new Chart(distributionCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'Number of Agents',
|
|
data: [],
|
|
backgroundColor: 'rgba(40, 167, 69, 0.7)',
|
|
borderColor: '#28a745',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Wealth Distribution'
|
|
},
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Wealth Range'
|
|
}
|
|
},
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Number of Agents'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize event listeners
|
|
*/
|
|
function initializeEventListeners() {
|
|
// Start simulation button
|
|
const startBtn = document.getElementById('startBtn');
|
|
if (startBtn) {
|
|
startBtn.addEventListener('click', startSimulation);
|
|
}
|
|
|
|
// Stop simulation button
|
|
const stopBtn = document.getElementById('stopBtn');
|
|
if (stopBtn) {
|
|
stopBtn.addEventListener('click', stopSimulation);
|
|
}
|
|
|
|
// Reset button
|
|
const resetBtn = document.getElementById('resetBtn');
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', resetSimulation);
|
|
}
|
|
|
|
// Export buttons
|
|
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
|
if (exportJsonBtn) {
|
|
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
|
}
|
|
|
|
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
|
if (exportCsvBtn) {
|
|
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize real-time updates via Socket.IO
|
|
*/
|
|
function initializeRealtimeUpdates() {
|
|
if (!window.MarkovEconomics.socket) {
|
|
console.warn('Socket.IO not available - real-time updates disabled');
|
|
return;
|
|
}
|
|
|
|
const socket = window.MarkovEconomics.socket;
|
|
|
|
// Simulation progress updates
|
|
socket.on('simulation_progress', function(data) {
|
|
updateSimulationProgress(data);
|
|
});
|
|
|
|
// Simulation completion
|
|
socket.on('simulation_complete', function(data) {
|
|
onSimulationComplete(data);
|
|
});
|
|
|
|
// Simulation stopped
|
|
socket.on('simulation_stopped', function(data) {
|
|
onSimulationStopped(data);
|
|
});
|
|
|
|
// Simulation error
|
|
socket.on('simulation_error', function(data) {
|
|
onSimulationError(data);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start a new simulation
|
|
*/
|
|
async function startSimulation() {
|
|
try {
|
|
// Get parameters from UI
|
|
const parameters = getSimulationParameters();
|
|
|
|
// Validate parameters
|
|
const errors = window.MarkovEconomics.utils.validateParameters(parameters);
|
|
if (errors.length > 0) {
|
|
window.MarkovEconomics.utils.showNotification(
|
|
'Parameter validation failed:<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;
|
|
|
|
// Join simulation room for real-time updates
|
|
if (window.MarkovEconomics.socket) {
|
|
window.MarkovEconomics.socket.emit('join_simulation', {
|
|
simulation_id: currentSimulation.id
|
|
});
|
|
}
|
|
|
|
// Start simulation
|
|
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/start`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
currentSimulation.isRunning = true;
|
|
updateUIState('running');
|
|
|
|
window.MarkovEconomics.utils.showNotification('Simulation started successfully!', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Failed to start simulation:', error);
|
|
updateUIState('error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop the current simulation
|
|
*/
|
|
async function stopSimulation() {
|
|
if (!currentSimulation.id) return;
|
|
|
|
try {
|
|
await window.MarkovEconomics.utils.apiRequest(`/api/simulation/${currentSimulation.id}/stop`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('stopped');
|
|
|
|
window.MarkovEconomics.utils.showNotification('Simulation stopped', 'info');
|
|
|
|
} catch (error) {
|
|
console.error('Failed to stop simulation:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset simulation state
|
|
*/
|
|
function resetSimulation() {
|
|
currentSimulation = {
|
|
id: null,
|
|
isRunning: false,
|
|
parameters: {},
|
|
data: {
|
|
iterations: [],
|
|
totalWealth: [],
|
|
giniCoefficients: [],
|
|
capitalShare: [],
|
|
top10Share: []
|
|
}
|
|
};
|
|
|
|
// Clear charts
|
|
Object.values(charts).forEach(chart => {
|
|
if (chart) {
|
|
chart.data.labels = [];
|
|
chart.data.datasets.forEach(dataset => {
|
|
dataset.data = [];
|
|
});
|
|
chart.update();
|
|
}
|
|
});
|
|
|
|
// Reset metrics
|
|
updateMetricsDisplay({
|
|
total_wealth: 0,
|
|
gini_coefficient: 0,
|
|
wealth_concentration_top10: 0,
|
|
capital_share: 0
|
|
});
|
|
|
|
updateUIState('ready');
|
|
|
|
window.MarkovEconomics.utils.showNotification('Simulation reset', 'info');
|
|
}
|
|
|
|
/**
|
|
* Get current simulation parameters from UI
|
|
*/
|
|
function getSimulationParameters() {
|
|
return {
|
|
r_rate: parseFloat(document.getElementById('capitalRate').value) / 100,
|
|
g_rate: parseFloat(document.getElementById('growthRate').value) / 100,
|
|
num_agents: parseInt(document.getElementById('numAgents').value),
|
|
iterations: parseInt(document.getElementById('iterations').value),
|
|
initial_capital: 1000,
|
|
initial_consumption: 1000
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Update UI state based on simulation status
|
|
*/
|
|
function updateUIState(state) {
|
|
const startBtn = document.getElementById('startBtn');
|
|
const stopBtn = document.getElementById('stopBtn');
|
|
const resetBtn = document.getElementById('resetBtn');
|
|
const status = document.getElementById('simulationStatus');
|
|
const progress = document.getElementById('progressContainer');
|
|
const exportBtns = [
|
|
document.getElementById('exportJsonBtn'),
|
|
document.getElementById('exportCsvBtn')
|
|
];
|
|
|
|
switch (state) {
|
|
case 'starting':
|
|
if (startBtn) startBtn.disabled = true;
|
|
if (stopBtn) stopBtn.disabled = true;
|
|
if (resetBtn) resetBtn.disabled = true;
|
|
if (status) {
|
|
status.textContent = 'Starting...';
|
|
status.className = 'simulation-status bg-warning text-dark';
|
|
}
|
|
break;
|
|
|
|
case 'running':
|
|
if (startBtn) startBtn.disabled = true;
|
|
if (stopBtn) stopBtn.disabled = false;
|
|
if (resetBtn) resetBtn.disabled = true;
|
|
if (status) {
|
|
status.textContent = 'Running';
|
|
status.className = 'simulation-status status-running';
|
|
}
|
|
if (progress) progress.style.display = 'block';
|
|
break;
|
|
|
|
case 'complete':
|
|
if (startBtn) startBtn.disabled = false;
|
|
if (stopBtn) stopBtn.disabled = true;
|
|
if (resetBtn) resetBtn.disabled = false;
|
|
if (status) {
|
|
status.textContent = 'Complete';
|
|
status.className = 'simulation-status status-complete';
|
|
}
|
|
if (progress) progress.style.display = 'none';
|
|
exportBtns.forEach(btn => {
|
|
if (btn) btn.disabled = false;
|
|
});
|
|
break;
|
|
|
|
case 'stopped':
|
|
if (startBtn) startBtn.disabled = false;
|
|
if (stopBtn) stopBtn.disabled = true;
|
|
if (resetBtn) resetBtn.disabled = false;
|
|
if (status) {
|
|
status.textContent = 'Stopped';
|
|
status.className = 'simulation-status bg-warning text-dark';
|
|
}
|
|
if (progress) progress.style.display = 'none';
|
|
break;
|
|
|
|
case 'error':
|
|
if (startBtn) startBtn.disabled = false;
|
|
if (stopBtn) stopBtn.disabled = true;
|
|
if (resetBtn) resetBtn.disabled = false;
|
|
if (status) {
|
|
status.textContent = 'Error';
|
|
status.className = 'simulation-status status-error';
|
|
}
|
|
if (progress) progress.style.display = 'none';
|
|
break;
|
|
|
|
default: // 'ready'
|
|
if (startBtn) startBtn.disabled = false;
|
|
if (stopBtn) stopBtn.disabled = true;
|
|
if (resetBtn) resetBtn.disabled = false;
|
|
if (status) {
|
|
status.textContent = 'Ready to start';
|
|
status.className = 'simulation-status status-ready';
|
|
}
|
|
if (progress) progress.style.display = 'none';
|
|
exportBtns.forEach(btn => {
|
|
if (btn) btn.disabled = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update simulation progress
|
|
*/
|
|
function updateSimulationProgress(data) {
|
|
// Update progress bar
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressText = document.getElementById('progressText');
|
|
|
|
if (progressBar && progressText) {
|
|
const percentage = data.progress_percentage || 0;
|
|
progressBar.style.width = percentage + '%';
|
|
progressText.textContent = percentage.toFixed(1) + '%';
|
|
}
|
|
|
|
// Update charts and metrics
|
|
if (data.iteration !== undefined) {
|
|
currentSimulation.data.iterations.push(data.iteration);
|
|
currentSimulation.data.totalWealth.push(data.total_wealth || 0);
|
|
currentSimulation.data.giniCoefficients.push(data.gini_coefficient || 0);
|
|
currentSimulation.data.capitalShare.push(data.capital_share || 0);
|
|
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
|
|
|
|
updateCharts();
|
|
updateMetricsDisplay(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update charts with new data
|
|
*/
|
|
function updateCharts() {
|
|
// Wealth Evolution Chart
|
|
if (charts.wealthEvolution) {
|
|
charts.wealthEvolution.data.labels = currentSimulation.data.iterations;
|
|
charts.wealthEvolution.data.datasets[0].data = currentSimulation.data.totalWealth;
|
|
charts.wealthEvolution.update('none');
|
|
}
|
|
|
|
// Inequality Chart
|
|
if (charts.inequality) {
|
|
charts.inequality.data.labels = currentSimulation.data.iterations;
|
|
charts.inequality.data.datasets[0].data = currentSimulation.data.giniCoefficients;
|
|
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
|
|
charts.inequality.update('none');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update metrics display
|
|
*/
|
|
function updateMetricsDisplay(data) {
|
|
const formatters = window.MarkovEconomics.utils;
|
|
|
|
const totalWealthEl = document.getElementById('totalWealthMetric');
|
|
if (totalWealthEl) {
|
|
totalWealthEl.textContent = formatters.formatCurrency(data.total_wealth || 0);
|
|
}
|
|
|
|
const giniEl = document.getElementById('giniMetric');
|
|
if (giniEl) {
|
|
giniEl.textContent = (data.gini_coefficient || 0).toFixed(3);
|
|
}
|
|
|
|
const top10El = document.getElementById('top10Metric');
|
|
if (top10El) {
|
|
top10El.textContent = formatters.formatPercentage(data.wealth_concentration_top10 || 0);
|
|
}
|
|
|
|
const capitalShareEl = document.getElementById('capitalShareMetric');
|
|
if (capitalShareEl) {
|
|
capitalShareEl.textContent = formatters.formatPercentage(data.capital_share || 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle simulation completion
|
|
*/
|
|
function onSimulationComplete(data) {
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('complete');
|
|
|
|
window.MarkovEconomics.utils.showNotification(
|
|
`Simulation completed! ${data.total_snapshots} data points collected.`,
|
|
'success'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle simulation stopped
|
|
*/
|
|
function onSimulationStopped(data) {
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('stopped');
|
|
}
|
|
|
|
/**
|
|
* Handle simulation error
|
|
*/
|
|
function onSimulationError(data) {
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('error');
|
|
|
|
window.MarkovEconomics.utils.showNotification(
|
|
`Simulation error: ${data.error}`,
|
|
'danger'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Export simulation data
|
|
*/
|
|
async function exportData(format) {
|
|
if (!currentSimulation.id) {
|
|
window.MarkovEconomics.utils.showNotification('No simulation data to export', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/simulation/${currentSimulation.id}/export/${format}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Export failed: ${response.statusText}`);
|
|
}
|
|
|
|
if (format === 'csv') {
|
|
const csvData = await response.text();
|
|
downloadFile(csvData, `simulation_${currentSimulation.id.slice(0, 8)}.csv`, 'text/csv');
|
|
} else {
|
|
const jsonData = await response.json();
|
|
downloadFile(
|
|
JSON.stringify(jsonData.data, null, 2),
|
|
`simulation_${currentSimulation.id.slice(0, 8)}.json`,
|
|
'application/json'
|
|
);
|
|
}
|
|
|
|
window.MarkovEconomics.utils.showNotification(`Data exported as ${format.toUpperCase()}`, 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Export failed:', error);
|
|
window.MarkovEconomics.utils.showNotification(`Export failed: ${error.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download file helper
|
|
*/
|
|
function downloadFile(content, filename, contentType) {
|
|
const blob = new Blob([content], { type: contentType });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initial inequality warning update
|
|
updateInequalityWarning();
|
|
});
|
|
|
|
// Export to global scope
|
|
window.initializeSimulation = initializeSimulation; |