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,
|
||||
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.
|
||||
|
@@ -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):
|
||||
"""
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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')
|
Reference in New Issue
Block a user