Fix wealth distribution chart functionality
- Add get_wealth_histogram() method to EconomicModel for histogram data - Add new API endpoint /simulation/<id>/distribution for chart data - Extend main data API with include_distribution parameter - Update real-time progress updates to include distribution data - Fix frontend updateCharts() to handle wealth distribution chart - Add distribution data processing in simulation.js - Update test_charts.py to verify histogram functionality Resolves issue where wealth distribution chart was not updating during simulations.
This commit is contained in:
@@ -177,8 +177,8 @@ class EconomicSimulation:
|
|||||||
gini_coefficient=gini,
|
gini_coefficient=gini,
|
||||||
wealth_concentration_top10=wealth_conc,
|
wealth_concentration_top10=wealth_conc,
|
||||||
capital_share=capital_share,
|
capital_share=capital_share,
|
||||||
average_wealth=np.mean(total_wealth_data),
|
average_wealth=float(np.mean(total_wealth_data)),
|
||||||
median_wealth=np.median(total_wealth_data)
|
median_wealth=float(np.median(total_wealth_data))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _calculate_gini_coefficient(self, wealth_data: List[float]) -> float:
|
def _calculate_gini_coefficient(self, wealth_data: List[float]) -> float:
|
||||||
@@ -296,6 +296,67 @@ class EconomicSimulation:
|
|||||||
|
|
||||||
return distribution
|
return distribution
|
||||||
|
|
||||||
|
def get_wealth_histogram(self, num_bins: int = 10) -> Tuple[List[str], List[int]]:
|
||||||
|
"""
|
||||||
|
Get wealth distribution as histogram data for charting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_bins: Number of bins for the histogram
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (bin_labels, bin_counts) for histogram chart
|
||||||
|
"""
|
||||||
|
if not self.agents:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
# Get current total wealth for all agents (capital + consumption)
|
||||||
|
wealth_values = []
|
||||||
|
for agent in self.agents:
|
||||||
|
if agent.wealth_history and agent.consumption_history:
|
||||||
|
total_wealth = agent.wealth_history[-1] + agent.consumption_history[-1]
|
||||||
|
else:
|
||||||
|
total_wealth = agent.initial_capital + agent.initial_consumption
|
||||||
|
wealth_values.append(total_wealth)
|
||||||
|
|
||||||
|
if not wealth_values:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
# Calculate histogram bins
|
||||||
|
min_wealth = min(wealth_values)
|
||||||
|
max_wealth = max(wealth_values)
|
||||||
|
|
||||||
|
if min_wealth == max_wealth:
|
||||||
|
# All agents have same wealth
|
||||||
|
return [f"${min_wealth:.0f}"], [len(wealth_values)]
|
||||||
|
|
||||||
|
# Create bins
|
||||||
|
bin_width = (max_wealth - min_wealth) / num_bins
|
||||||
|
bins = [min_wealth + i * bin_width for i in range(num_bins + 1)]
|
||||||
|
|
||||||
|
# Count agents in each bin
|
||||||
|
bin_counts = [0] * num_bins
|
||||||
|
bin_labels = []
|
||||||
|
|
||||||
|
for i in range(num_bins):
|
||||||
|
# Create label for this bin
|
||||||
|
bin_start = bins[i]
|
||||||
|
bin_end = bins[i + 1]
|
||||||
|
if i == num_bins - 1: # Last bin includes upper bound
|
||||||
|
bin_labels.append(f"${bin_start:.0f} - ${bin_end:.0f}")
|
||||||
|
else:
|
||||||
|
bin_labels.append(f"${bin_start:.0f} - ${bin_end:.0f}")
|
||||||
|
|
||||||
|
# Count agents in this bin
|
||||||
|
for wealth in wealth_values:
|
||||||
|
if i == num_bins - 1: # Last bin includes upper bound
|
||||||
|
if bin_start <= wealth <= bin_end:
|
||||||
|
bin_counts[i] += 1
|
||||||
|
else:
|
||||||
|
if bin_start <= wealth < bin_end:
|
||||||
|
bin_counts[i] += 1
|
||||||
|
|
||||||
|
return bin_labels, bin_counts
|
||||||
|
|
||||||
def update_parameters(self, new_parameters: SimulationParameters):
|
def update_parameters(self, new_parameters: SimulationParameters):
|
||||||
"""
|
"""
|
||||||
Update simulation parameters and restart with new settings.
|
Update simulation parameters and restart with new settings.
|
||||||
|
@@ -111,6 +111,9 @@ def start_simulation(simulation_id: str):
|
|||||||
|
|
||||||
# Emit progress update every 10 iterations or at milestones
|
# Emit progress update every 10 iterations or at milestones
|
||||||
if i % 10 == 0 or i == total_iterations - 1:
|
if i % 10 == 0 or i == total_iterations - 1:
|
||||||
|
# Get distribution data for real-time chart updates
|
||||||
|
bin_labels, bin_counts = simulation.get_wealth_histogram(10)
|
||||||
|
|
||||||
progress_data = {
|
progress_data = {
|
||||||
'simulation_id': simulation_id,
|
'simulation_id': simulation_id,
|
||||||
'iteration': snapshot.iteration,
|
'iteration': snapshot.iteration,
|
||||||
@@ -119,7 +122,11 @@ def start_simulation(simulation_id: str):
|
|||||||
'total_wealth': snapshot.total_wealth,
|
'total_wealth': snapshot.total_wealth,
|
||||||
'gini_coefficient': snapshot.gini_coefficient,
|
'gini_coefficient': snapshot.gini_coefficient,
|
||||||
'capital_share': snapshot.capital_share,
|
'capital_share': snapshot.capital_share,
|
||||||
'wealth_concentration_top10': snapshot.wealth_concentration_top10
|
'wealth_concentration_top10': snapshot.wealth_concentration_top10,
|
||||||
|
'distribution': {
|
||||||
|
'labels': bin_labels,
|
||||||
|
'counts': bin_counts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socketio.emit('simulation_progress', progress_data,
|
socketio.emit('simulation_progress', progress_data,
|
||||||
@@ -272,6 +279,19 @@ def get_simulation_data(simulation_id: str):
|
|||||||
'capital_shares': capital_over_time
|
'capital_shares': capital_over_time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Include current wealth distribution histogram if requested
|
||||||
|
if request.args.get('include_distribution', '').lower() == 'true':
|
||||||
|
num_bins = request.args.get('bins', 10, type=int)
|
||||||
|
if num_bins < 1 or num_bins > 50:
|
||||||
|
num_bins = 10
|
||||||
|
|
||||||
|
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
|
||||||
|
response_data['distribution'] = {
|
||||||
|
'labels': bin_labels,
|
||||||
|
'counts': bin_counts,
|
||||||
|
'bins': num_bins
|
||||||
|
}
|
||||||
|
|
||||||
return jsonify(response_data)
|
return jsonify(response_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -279,6 +299,49 @@ def get_simulation_data(simulation_id: str):
|
|||||||
return jsonify({'error': 'Internal server error'}), 500
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/simulation/<simulation_id>/distribution', methods=['GET'])
|
||||||
|
def get_wealth_distribution(simulation_id: str):
|
||||||
|
"""
|
||||||
|
Get current wealth distribution histogram for charting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
simulation_id: Unique simulation identifier
|
||||||
|
|
||||||
|
Query Parameters:
|
||||||
|
bins: Number of histogram bins (default: 10)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON response with histogram data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
simulation = simulation_manager.get_simulation(simulation_id)
|
||||||
|
|
||||||
|
if not simulation:
|
||||||
|
return jsonify({'error': 'Simulation not found'}), 404
|
||||||
|
|
||||||
|
# Get number of bins from query parameter
|
||||||
|
num_bins = request.args.get('bins', 10, type=int)
|
||||||
|
if num_bins < 1 or num_bins > 50:
|
||||||
|
num_bins = 10 # Default to 10 bins
|
||||||
|
|
||||||
|
# Get histogram data
|
||||||
|
bin_labels, bin_counts = simulation.get_wealth_histogram(num_bins)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'simulation_id': simulation_id,
|
||||||
|
'histogram': {
|
||||||
|
'labels': bin_labels,
|
||||||
|
'counts': bin_counts,
|
||||||
|
'total_agents': len(simulation.agents),
|
||||||
|
'bins': num_bins
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error getting wealth distribution: {str(e)}")
|
||||||
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/simulation/<simulation_id>/export/<format_type>', methods=['GET'])
|
@api_bp.route('/simulation/<simulation_id>/export/<format_type>', methods=['GET'])
|
||||||
def export_simulation_data(simulation_id: str, format_type: str):
|
def export_simulation_data(simulation_id: str, format_type: str):
|
||||||
"""
|
"""
|
||||||
|
@@ -15,7 +15,11 @@ let currentSimulation = {
|
|||||||
totalWealth: [],
|
totalWealth: [],
|
||||||
giniCoefficients: [],
|
giniCoefficients: [],
|
||||||
capitalShare: [],
|
capitalShare: [],
|
||||||
top10Share: []
|
top10Share: [],
|
||||||
|
distribution: {
|
||||||
|
labels: [],
|
||||||
|
counts: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -445,7 +449,11 @@ function resetSimulation() {
|
|||||||
totalWealth: [],
|
totalWealth: [],
|
||||||
giniCoefficients: [],
|
giniCoefficients: [],
|
||||||
capitalShare: [],
|
capitalShare: [],
|
||||||
top10Share: []
|
top10Share: [],
|
||||||
|
distribution: {
|
||||||
|
labels: [],
|
||||||
|
counts: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -596,6 +604,12 @@ function updateSimulationProgress(data) {
|
|||||||
currentSimulation.data.capitalShare.push(data.capital_share || 0);
|
currentSimulation.data.capitalShare.push(data.capital_share || 0);
|
||||||
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
|
currentSimulation.data.top10Share.push(data.wealth_concentration_top10 || 0);
|
||||||
|
|
||||||
|
// Update distribution data if available
|
||||||
|
if (data.distribution && data.distribution.labels && data.distribution.counts) {
|
||||||
|
currentSimulation.data.distribution.labels = data.distribution.labels;
|
||||||
|
currentSimulation.data.distribution.counts = data.distribution.counts;
|
||||||
|
}
|
||||||
|
|
||||||
updateCharts();
|
updateCharts();
|
||||||
updateMetricsDisplay(data);
|
updateMetricsDisplay(data);
|
||||||
}
|
}
|
||||||
@@ -619,6 +633,13 @@ function updateCharts() {
|
|||||||
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
|
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
|
||||||
charts.inequality.update('none');
|
charts.inequality.update('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wealth Distribution Chart
|
||||||
|
if (charts.distribution && currentSimulation.data.distribution.labels.length > 0) {
|
||||||
|
charts.distribution.data.labels = currentSimulation.data.distribution.labels;
|
||||||
|
charts.distribution.data.datasets[0].data = currentSimulation.data.distribution.counts;
|
||||||
|
charts.distribution.update('none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -658,7 +679,7 @@ async function onSimulationComplete(data) {
|
|||||||
// Fetch complete simulation data and populate charts
|
// Fetch complete simulation data and populate charts
|
||||||
try {
|
try {
|
||||||
const response = await window.MarkovEconomics.utils.apiRequest(
|
const response = await window.MarkovEconomics.utils.apiRequest(
|
||||||
`/api/simulation/${currentSimulation.id}/data?include_evolution=true`
|
`/api/simulation/${currentSimulation.id}/data?include_evolution=true&include_distribution=true`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.evolution) {
|
if (response.evolution) {
|
||||||
@@ -669,6 +690,12 @@ async function onSimulationComplete(data) {
|
|||||||
currentSimulation.data.top10Share = response.evolution.top10_shares || [];
|
currentSimulation.data.top10Share = response.evolution.top10_shares || [];
|
||||||
currentSimulation.data.capitalShare = response.evolution.capital_shares || [];
|
currentSimulation.data.capitalShare = response.evolution.capital_shares || [];
|
||||||
|
|
||||||
|
// Update distribution data
|
||||||
|
if (response.distribution) {
|
||||||
|
currentSimulation.data.distribution.labels = response.distribution.labels;
|
||||||
|
currentSimulation.data.distribution.counts = response.distribution.counts;
|
||||||
|
}
|
||||||
|
|
||||||
// Update charts with complete data
|
// Update charts with complete data
|
||||||
updateCharts();
|
updateCharts();
|
||||||
|
|
||||||
|
@@ -17,8 +17,8 @@ print('Started simulation:', start_resp.json()['status'])
|
|||||||
# Wait for completion
|
# Wait for completion
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
# Get evolution data
|
# Get evolution data with distribution
|
||||||
data_resp = requests.get(f'http://localhost:5000/api/simulation/{sim_id}/data?include_evolution=true')
|
data_resp = requests.get(f'http://localhost:5000/api/simulation/{sim_id}/data?include_evolution=true&include_distribution=true')
|
||||||
result = data_resp.json()
|
result = data_resp.json()
|
||||||
|
|
||||||
print('\nFinal metrics:')
|
print('\nFinal metrics:')
|
||||||
@@ -28,7 +28,40 @@ print(f' Final Total Wealth: ${result["evolution"]["total_wealth"][-1]:.2f}')
|
|||||||
print(f' Top 10% data points: {len(result["evolution"].get("top10_shares", []))}')
|
print(f' Top 10% data points: {len(result["evolution"].get("top10_shares", []))}')
|
||||||
print(f' Capital share data points: {len(result["evolution"].get("capital_shares", []))}')
|
print(f' Capital share data points: {len(result["evolution"].get("capital_shares", []))}')
|
||||||
|
|
||||||
|
# Test wealth distribution
|
||||||
|
if 'distribution' in result:
|
||||||
|
dist_data = result['distribution']
|
||||||
|
print(f'\nWealth Distribution:')
|
||||||
|
print(f' Bins: {dist_data.get("bins", 0)}')
|
||||||
|
print(f' Labels: {len(dist_data.get("labels", []))}')
|
||||||
|
print(f' Counts: {len(dist_data.get("counts", []))}')
|
||||||
|
|
||||||
|
if dist_data.get('labels') and dist_data.get('counts'):
|
||||||
|
print('\n Distribution Histogram:')
|
||||||
|
for label, count in zip(dist_data['labels'], dist_data['counts']):
|
||||||
|
bar = '█' * max(1, count // 2) # Visual bar representation
|
||||||
|
print(f' {label:<20} {count:>3} agents {bar}')
|
||||||
|
print('\n✅ Wealth distribution chart should now work!')
|
||||||
|
else:
|
||||||
|
print('\n❌ No distribution data available')
|
||||||
|
else:
|
||||||
|
print('\n❌ No distribution data in response')
|
||||||
|
|
||||||
|
# Test dedicated distribution endpoint
|
||||||
|
print('\nTesting dedicated distribution endpoint...')
|
||||||
|
dist_resp = requests.get(f'http://localhost:5000/api/simulation/{sim_id}/distribution?bins=8')
|
||||||
|
if dist_resp.status_code == 200:
|
||||||
|
dist_result = dist_resp.json()
|
||||||
|
hist_data = dist_result.get('histogram', {})
|
||||||
|
print(f' Total agents: {hist_data.get("total_agents", 0)}')
|
||||||
|
print(f' Bins: {hist_data.get("bins", 0)}')
|
||||||
|
print(f' Labels count: {len(hist_data.get("labels", []))}')
|
||||||
|
print(f' Counts sum: {sum(hist_data.get("counts", []))}')
|
||||||
|
print('\n✅ Distribution endpoint working!')
|
||||||
|
else:
|
||||||
|
print(f'\n❌ Distribution endpoint failed: {dist_resp.status_code}')
|
||||||
|
|
||||||
if len(result["evolution"]["gini_coefficients"]) > 0:
|
if len(result["evolution"]["gini_coefficients"]) > 0:
|
||||||
print('\n✅ Charts should now be populated with data!')
|
print('\n🎯 All charts should now be populated with data!')
|
||||||
else:
|
else:
|
||||||
print('\n❌ No data available for charts')
|
print('\n❌ No data available for charts')
|
Reference in New Issue
Block a user