diff --git a/app/models/economic_model.py b/app/models/economic_model.py index ce21d21..71c1e4c 100644 --- a/app/models/economic_model.py +++ b/app/models/economic_model.py @@ -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. diff --git a/app/routes/api.py b/app/routes/api.py index 51e322c..5fbb66c 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -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//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//export/', methods=['GET']) def export_simulation_data(simulation_id: str, format_type: str): """ diff --git a/app/static/js/simulation.js b/app/static/js/simulation.js index a41bdd3..a70dafe 100644 --- a/app/static/js/simulation.js +++ b/app/static/js/simulation.js @@ -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(); diff --git a/test_charts.py b/test_charts.py index 219c008..7ba199e 100644 --- a/test_charts.py +++ b/test_charts.py @@ -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') \ No newline at end of file