Initial commit: Markov Economics Simulation App
This commit is contained in:
281
app/static/js/app.js
Normal file
281
app/static/js/app.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Main JavaScript Application for Markov Economics
|
||||
*
|
||||
* Provides utility functions and global application behavior
|
||||
*/
|
||||
|
||||
// Global application state
|
||||
window.MarkovEconomics = {
|
||||
socket: null,
|
||||
currentSimulationId: null,
|
||||
isConnected: false,
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
maxRetries: 3,
|
||||
retryDelay: 1000,
|
||||
updateInterval: 100
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize Socket.IO connection
|
||||
*/
|
||||
function initializeSocket() {
|
||||
if (typeof io !== 'undefined') {
|
||||
window.MarkovEconomics.socket = io();
|
||||
|
||||
window.MarkovEconomics.socket.on('connect', function() {
|
||||
console.log('Connected to server');
|
||||
window.MarkovEconomics.isConnected = true;
|
||||
updateConnectionStatus(true);
|
||||
});
|
||||
|
||||
window.MarkovEconomics.socket.on('disconnect', function() {
|
||||
console.log('Disconnected from server');
|
||||
window.MarkovEconomics.isConnected = false;
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
|
||||
window.MarkovEconomics.socket.on('connect_error', function(error) {
|
||||
console.error('Connection error:', error);
|
||||
updateConnectionStatus(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection status indicator
|
||||
*/
|
||||
function updateConnectionStatus(connected) {
|
||||
// You can add a connection status indicator here if needed
|
||||
if (connected) {
|
||||
console.log('✅ Real-time connection established');
|
||||
} else {
|
||||
console.log('❌ Real-time connection lost');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format numbers for display
|
||||
*/
|
||||
function formatNumber(num, decimals = 2) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if (Math.abs(num) >= 1e9) {
|
||||
return (num / 1e9).toFixed(decimals) + 'B';
|
||||
} else if (Math.abs(num) >= 1e6) {
|
||||
return (num / 1e6).toFixed(decimals) + 'M';
|
||||
} else if (Math.abs(num) >= 1e3) {
|
||||
return (num / 1e3).toFixed(decimals) + 'K';
|
||||
} else {
|
||||
return num.toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency values
|
||||
*/
|
||||
function formatCurrency(amount, decimals = 0) {
|
||||
if (amount === null || amount === undefined || isNaN(amount)) {
|
||||
return '$0';
|
||||
}
|
||||
|
||||
return '$' + formatNumber(amount, decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage values
|
||||
*/
|
||||
function formatPercentage(value, decimals = 1) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return (value * 100).toFixed(decimals) + '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification message
|
||||
*/
|
||||
function showNotification(message, type = 'info', duration = 3000) {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
notification.style.cssText = `
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-remove after duration
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, duration);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading spinner
|
||||
*/
|
||||
function showLoading(element, text = 'Loading...') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<div class="spinner me-2"></div>
|
||||
<span>${text}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading spinner
|
||||
*/
|
||||
function hideLoading(element, originalContent = '') {
|
||||
if (typeof element === 'string') {
|
||||
element = document.getElementById(element);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.innerHTML = originalContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request with error handling
|
||||
*/
|
||||
async function apiRequest(url, options = {}) {
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const response = await fetch(url, finalOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Request failed:', error);
|
||||
showNotification(`Error: ${error.message}`, 'danger');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameter ranges
|
||||
*/
|
||||
function validateParameters(params) {
|
||||
const errors = [];
|
||||
|
||||
if (params.r_rate < 0 || params.r_rate > 0.25) {
|
||||
errors.push('Capital rate must be between 0% and 25%');
|
||||
}
|
||||
|
||||
if (params.g_rate < 0 || params.g_rate > 0.20) {
|
||||
errors.push('Growth rate must be between 0% and 20%');
|
||||
}
|
||||
|
||||
if (params.num_agents < 10 || params.num_agents > 10000) {
|
||||
errors.push('Number of agents must be between 10 and 10,000');
|
||||
}
|
||||
|
||||
if (params.iterations < 100 || params.iterations > 100000) {
|
||||
errors.push('Iterations must be between 100 and 100,000');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for parameter changes
|
||||
*/
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize application
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Socket.IO if available
|
||||
initializeSocket();
|
||||
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Add fade-in animation to cards
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach((card, index) => {
|
||||
card.style.animationDelay = `${index * 0.1}s`;
|
||||
card.classList.add('fade-in');
|
||||
});
|
||||
|
||||
console.log('🚀 Markov Economics application initialized');
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle page visibility changes
|
||||
*/
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
console.log('Page hidden - pausing updates');
|
||||
} else {
|
||||
console.log('Page visible - resuming updates');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Export functions to global scope
|
||||
*/
|
||||
window.MarkovEconomics.utils = {
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatPercentage,
|
||||
showNotification,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
apiRequest,
|
||||
validateParameters,
|
||||
debounce
|
||||
};
|
743
app/static/js/simulation.js
Normal file
743
app/static/js/simulation.js
Normal file
@@ -0,0 +1,743 @@
|
||||
/**
|
||||
* 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;
|
Reference in New Issue
Block a user