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:
2025-08-24 20:10:58 +02:00
parent 27e185c2a6
commit 3a08de3a4a
4 changed files with 193 additions and 9 deletions

View File

@@ -177,8 +177,8 @@ class EconomicSimulation:
gini_coefficient=gini,
wealth_concentration_top10=wealth_conc,
capital_share=capital_share,
average_wealth=np.mean(total_wealth_data),
median_wealth=np.median(total_wealth_data)
average_wealth=float(np.mean(total_wealth_data)),
median_wealth=float(np.median(total_wealth_data))
)
def _calculate_gini_coefficient(self, wealth_data: List[float]) -> float:
@@ -296,6 +296,67 @@ class EconomicSimulation:
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):
"""
Update simulation parameters and restart with new settings.

View File

@@ -111,6 +111,9 @@ def start_simulation(simulation_id: str):
# Emit progress update every 10 iterations or at milestones
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 = {
'simulation_id': simulation_id,
'iteration': snapshot.iteration,
@@ -119,7 +122,11 @@ def start_simulation(simulation_id: str):
'total_wealth': snapshot.total_wealth,
'gini_coefficient': snapshot.gini_coefficient,
'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,
@@ -272,6 +279,19 @@ def get_simulation_data(simulation_id: str):
'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)
except Exception as e:
@@ -279,6 +299,49 @@ def get_simulation_data(simulation_id: str):
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'])
def export_simulation_data(simulation_id: str, format_type: str):
"""

View File

@@ -15,7 +15,11 @@ let currentSimulation = {
totalWealth: [],
giniCoefficients: [],
capitalShare: [],
top10Share: []
top10Share: [],
distribution: {
labels: [],
counts: []
}
}
};
@@ -445,7 +449,11 @@ function resetSimulation() {
totalWealth: [],
giniCoefficients: [],
capitalShare: [],
top10Share: []
top10Share: [],
distribution: {
labels: [],
counts: []
}
}
};
@@ -596,6 +604,12 @@ function updateSimulationProgress(data) {
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 && data.distribution.labels && data.distribution.counts) {
currentSimulation.data.distribution.labels = data.distribution.labels;
currentSimulation.data.distribution.counts = data.distribution.counts;
}
updateCharts();
updateMetricsDisplay(data);
}
@@ -619,6 +633,13 @@ function updateCharts() {
charts.inequality.data.datasets[1].data = currentSimulation.data.top10Share;
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
try {
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) {
@@ -669,6 +690,12 @@ async function onSimulationComplete(data) {
currentSimulation.data.top10Share = response.evolution.top10_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
updateCharts();

View File

@@ -17,8 +17,8 @@ print('Started simulation:', start_resp.json()['status'])
# Wait for completion
time.sleep(3)
# Get evolution data
data_resp = requests.get(f'http://localhost:5000/api/simulation/{sim_id}/data?include_evolution=true')
# Get evolution data with distribution
data_resp = requests.get(f'http://localhost:5000/api/simulation/{sim_id}/data?include_evolution=true&include_distribution=true')
result = data_resp.json()
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' 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:
print('\n✅ Charts should now be populated with data!')
print('\n🎯 All charts should now be populated with data!')
else:
print('\n❌ No data available for charts')