- Fix circular import issue by moving simulation_manager to app/__init__.py - Enhance get_wealth_evolution to include inequality metrics data - Add get_inequality_evolution method for complete chart data - Update API to return top10_shares and capital_shares in evolution data - Modify onSimulationComplete to fetch and populate charts with complete data - Fix simulation threading to properly mark completion state - Add test script to verify chart data generation The charts now properly display simulation results by fetching complete evolution data when simulation completes, resolving the empty diagrams issue.
773 lines
24 KiB
JavaScript
773 lines
24 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
|
|
*/
|
|
async function onSimulationComplete(data) {
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('complete');
|
|
|
|
// Fetch complete simulation data and populate charts
|
|
try {
|
|
const response = await window.MarkovEconomics.utils.apiRequest(
|
|
`/api/simulation/${currentSimulation.id}/data?include_evolution=true`
|
|
);
|
|
|
|
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 charts with complete data
|
|
updateCharts();
|
|
|
|
// Update final metrics
|
|
if (response.latest_snapshot) {
|
|
updateMetricsDisplay(response.latest_snapshot);
|
|
}
|
|
}
|
|
|
|
window.MarkovEconomics.utils.showNotification(
|
|
`Simulation completed! ${data.total_snapshots} data points collected.`,
|
|
'success'
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to fetch simulation data:', error);
|
|
window.MarkovEconomics.utils.showNotification(
|
|
'Simulation completed but failed to load results',
|
|
'warning'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle simulation stopped
|
|
*/
|
|
function onSimulationStopped(data) {
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('stopped');
|
|
}
|
|
|
|
/**
|
|
* Handle simulation error
|
|
*/
|
|
function onSimulationError(data) {
|
|
currentSimulation.isRunning = false;
|
|
updateUIState('error');
|
|
|
|
window.MarkovEconomics.utils.showNotification(
|
|
`Simulation error: ${data.error}`,
|
|
'danger'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Export simulation data
|
|
*/
|
|
async function exportData(format) {
|
|
if (!currentSimulation.id) {
|
|
window.MarkovEconomics.utils.showNotification('No simulation data to export', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/simulation/${currentSimulation.id}/export/${format}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Export failed: ${response.statusText}`);
|
|
}
|
|
|
|
if (format === 'csv') {
|
|
const csvData = await response.text();
|
|
downloadFile(csvData, `simulation_${currentSimulation.id.slice(0, 8)}.csv`, 'text/csv');
|
|
} else {
|
|
const jsonData = await response.json();
|
|
downloadFile(
|
|
JSON.stringify(jsonData.data, null, 2),
|
|
`simulation_${currentSimulation.id.slice(0, 8)}.json`,
|
|
'application/json'
|
|
);
|
|
}
|
|
|
|
window.MarkovEconomics.utils.showNotification(`Data exported as ${format.toUpperCase()}`, 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Export failed:', error);
|
|
window.MarkovEconomics.utils.showNotification(`Export failed: ${error.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download file helper
|
|
*/
|
|
function downloadFile(content, filename, contentType) {
|
|
const blob = new Blob([content], { type: contentType });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Initialize when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initial inequality warning update
|
|
updateInequalityWarning();
|
|
});
|
|
|
|
// Export to global scope
|
|
window.initializeSimulation = initializeSimulation; |