diff --git a/policyengine/charts/__init__.py b/policyengine/charts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/__init__.py b/policyengine/charts/distributional_impact/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/by_income_decile/__init__.py b/policyengine/charts/distributional_impact/by_income_decile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/by_income_decile/average.py b/policyengine/charts/distributional_impact/by_income_decile/average.py new file mode 100644 index 0000000..4bda492 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_income_decile/average.py @@ -0,0 +1,114 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class ByIncomeDecileAverageChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Store values as they are (no percentage conversion) + for i in range(1, 12): + setattr(self, f"decile_{i}", data['average'][i]) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def _get_currency_symbol(self): + if self.country.lower() == "us": + return "$" + elif self.country.lower() == "uk": + return "£" + else: + return "$" # Default to USD if country not recognized + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Absolute change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Get currency symbol based on country + currency_symbol = self._get_currency_symbol() + + # Generate hover texts with raw impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {currency_symbol}{abs(val):,.1f}" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # No need to convert values + colors = [self._get_color(value) for value in non_zero_values] + + # Add bar chart with text formatted with currency symbol + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{currency_symbol}{abs(value):,.1f}" for value in non_zero_values], # Display values with currency symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to include currency on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.0f", # No decimal places for the y-axis, add thousands separator + title=f"Absolute Impact on Income ({currency_symbol})" + ), + xaxis=dict( + title="Income Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Absolute Change in Household Income by Decile" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig \ No newline at end of file diff --git a/policyengine/charts/distributional_impact/by_income_decile/relative.py b/policyengine/charts/distributional_impact/by_income_decile/relative.py new file mode 100644 index 0000000..c7795ff --- /dev/null +++ b/policyengine/charts/distributional_impact/by_income_decile/relative.py @@ -0,0 +1,104 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + + +class ByIncomeDecileRelativeChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Convert the relative values to percentages and store them in attributes for each decile + for i in range(1, 12): + setattr(self, f"decile_{i}", data['relative'][i] * 100) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Relative change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Generate hover texts with formatted impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {abs(val):,.1f}%" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # The values are already in percentages + colors = [self._get_color(value) for value in non_zero_values] + + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{value:.1f}%" for value in non_zero_values], # Display values with one decimal place and percentage symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to show percentage on y-axis and format figure + fig.update_layout( + yaxis=dict( + tickformat=".1f%", # Format for one decimal place with percentage symbol + ticksuffix="%", + title="Relative Impact on Income (%)" + ), + xaxis=dict( + title="Income Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Relative Change in Household Income by Decile" + ) + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig \ No newline at end of file diff --git a/policyengine/charts/distributional_impact/by_wealth_decile/__init__.py b/policyengine/charts/distributional_impact/by_wealth_decile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/distributional_impact/by_wealth_decile/average.py b/policyengine/charts/distributional_impact/by_wealth_decile/average.py new file mode 100644 index 0000000..5b0ab43 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_wealth_decile/average.py @@ -0,0 +1,114 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class ByWealthDecileAverageChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Store values as they are (no percentage conversion) + for i in range(1, 12): + setattr(self, f"decile_{i}", data['average'][i]) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def _get_currency_symbol(self): + if self.country.lower() == "us": + return "$" + elif self.country.lower() == "uk": + return "£" + else: + return "$" # Default to USD if country not recognized + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Absolute change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Get currency symbol based on country + currency_symbol = self._get_currency_symbol() + + # Generate hover texts with raw impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {currency_symbol}{abs(val):,.1f}" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # No need to convert values + colors = [self._get_color(value) for value in non_zero_values] + + # Add bar chart with text formatted with currency symbol + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{currency_symbol}{abs(value):,.1f}" for value in non_zero_values], # Display values with currency symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to include currency on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.0f", # No decimal places for the y-axis, add thousands separator + title=f"Absolute Impact on Wealth ({currency_symbol})" + ), + xaxis=dict( + title="Wealth Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Absolute Change in Household Income by Decile" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig \ No newline at end of file diff --git a/policyengine/charts/distributional_impact/by_wealth_decile/relative.py b/policyengine/charts/distributional_impact/by_wealth_decile/relative.py new file mode 100644 index 0000000..5377781 --- /dev/null +++ b/policyengine/charts/distributional_impact/by_wealth_decile/relative.py @@ -0,0 +1,105 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + + +class ByWealthDecileRelativeChart: + def __init__(self, country: str, data=None): + if data is None: + raise ValueError("Data must be provided") + + # Convert the relative values to percentages and store them in attributes for each decile + for i in range(1, 12): + setattr(self, f"decile_{i}", data['relative'][i] * 100) + + self.country = country + + def _get_color(self, value): + if value is None or value == 0 or value < 0: + return GRAY + return BLUE + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + + def generate_chart_data(self): + categories = [str(i) for i in range(1, 12)] + values = [getattr(self, f"decile_{i}") for i in range(1, 12)] + + # Filter out categories and values with zero difference + non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0] + + if not non_zero_data: + fig = go.Figure() + fig.add_annotation( + x=0.5, + y=0.5, + xref="paper", + yref="paper", + text="No differences to display", + showarrow=False, + font=dict(size=20) + ) + fig.update_layout( + title="Relative change in household income", + xaxis=dict(visible=False), + yaxis=dict(visible=False) + ) + return fig + + non_zero_categories, non_zero_values = zip(*non_zero_data) + + # Generate hover texts with formatted impact values and change direction + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the income of households in the {i}{self.ordinal_suffix(int(i))} decile by {abs(val):,.1f}%" + for i, val in zip(non_zero_categories, non_zero_values) + ] + + fig = go.Figure() + + values_in_bn = non_zero_values # The values are already in percentages + colors = [self._get_color(value) for value in non_zero_values] + + fig.add_trace(go.Bar( + x=non_zero_categories, + y=values_in_bn, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{value:.1f}%" for value in non_zero_values], # Display values with one decimal place and percentage symbol + textposition='outside', + hovertemplate="Decile %{x}

%{customdata}", # Hover shows "Decile {x}" + customdata=hover_texts + )) + + # Update layout to show percentage on y-axis and format figure + fig.update_layout( + yaxis=dict( + tickformat=".1f%", # Format for one decimal place with percentage symbol + ticksuffix="%", + title="Relative Impact on Wealth (%)" + ), + xaxis=dict( + title="Wealth Decile" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Relative Change in Household Income by Decile" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig diff --git a/policyengine/charts/inequality.py b/policyengine/charts/inequality.py new file mode 100644 index 0000000..c206c1d --- /dev/null +++ b/policyengine/charts/inequality.py @@ -0,0 +1,65 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class InequalityImpactChart: + def __init__(self, data=None) -> None: + if data is None: + raise ValueError("Data must be provided") + + # Expecting data to contain baseline, reform, change, and change_percentage for each metric + self.data = data + + def generate_chart_data(self): + # Data for the x-axis labels + metrics = ["Gini index", "Top 1% share", "Top 10% share"] + + # Extract the change percentages, baseline, and reform values for hover text + change_percentages = [self.data[metric]['change_percentage'] for metric in metrics] + baseline_values = [self.data[metric]['baseline'] for metric in metrics] + reform_values = [self.data[metric]['reform'] for metric in metrics] + + # Generate hover text for each metric + hover_texts = [ + f"The reform would increase the {metric} by {change_percentages[i]}% from {baseline_values[i]} to {reform_values[i]}%" + if change_percentages[i] > 0 + else f"The reform would decrease the {metric} by {change_percentages[i]}% from {baseline_values[i]} to {reform_values[i]}%" + for i, metric in enumerate(metrics) + ] + + # Create the bar chart figure + fig = go.Figure() + + # Add a bar trace for the change percentages of each metric + fig.add_trace(go.Bar( + x=metrics, # Labels for each metric + y=change_percentages, # Change percentages for each metric + marker=dict( + color=[BLUE if change_percentages[i] > 0 else GRAY for i in range(len(change_percentages))], # Conditional color for each bar + line=dict(width=1), + ), + text=[f"{percent}%" for percent in change_percentages], # Display percentage as text + textposition='outside', # Position text outside the bars + hovertemplate=f"%{{x}}

%{{customdata}}", + customdata=hover_texts # Hover text for each bar + )) + + # Update layout for the chart + fig.update_layout( + yaxis=dict( + tickformat=".1f", # Show y-values with one decimal place + ticksuffix="%", + title="Relative change" # Add percentage symbol + ), + hoverlabel=dict( + bgcolor="white", # Background color of the hover label + font=dict( + color="black", # Text color of the hover label + size=16, # Font size + ), + ), + title="Impact of Reform on Inequality Metrics" # Add a title to the chart + ) + + format_fig(fig) + + return fig \ No newline at end of file diff --git a/policyengine/charts/poverty/__init__.py b/policyengine/charts/poverty/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/poverty/deep/__init__.py b/policyengine/charts/poverty/deep/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/poverty/deep/by_age.py b/policyengine/charts/poverty/deep/by_age.py new file mode 100644 index 0000000..fabe5c6 --- /dev/null +++ b/policyengine/charts/poverty/deep/by_age.py @@ -0,0 +1,107 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class DeepPovertyByAgeChart: + def __init__(self,country:str, data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="%{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Child': { +# 'Baseline': 0.32427219591395395, +# 'Reform': 0.33392168532001054, +# 'Change': 3.0 +# }, +# 'Adult': { +# 'Baseline': 0.17427822561729264, +# 'Reform': 0.17757158627182623, +# 'Change': 1.9 +# }, +# 'Senior': { +# 'Baseline': 0.12817646500651358, +# 'Reform': 0.1370685860340031, +# 'Change': 6.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/charts/poverty/deep/by_gender.py b/policyengine/charts/poverty/deep/by_gender.py new file mode 100644 index 0000000..40e1a61 --- /dev/null +++ b/policyengine/charts/poverty/deep/by_gender.py @@ -0,0 +1,103 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class DeepPovertyByGenderChart: + def __init__(self, country:str,data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="Category %{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", + ticksuffix = "%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Male': { +# 'Baseline': 0.18412623468617267, +# 'Reform': 0.18932591339284738, +# 'Change': 2.8 +# }, +# 'Female': { +# 'Baseline': 0.21377616483057263, +# 'Reform': 0.22004590877186603, +# 'Change': 2.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/charts/poverty/regular/__init__.py b/policyengine/charts/poverty/regular/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/policyengine/charts/poverty/regular/by_age.py b/policyengine/charts/poverty/regular/by_age.py new file mode 100644 index 0000000..e60333b --- /dev/null +++ b/policyengine/charts/poverty/regular/by_age.py @@ -0,0 +1,107 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class RegularPovertyByAgeChart: + def __init__(self,country:str, data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="%{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Child': { +# 'Baseline': 0.32427219591395395, +# 'Reform': 0.33392168532001054, +# 'Change': 3.0 +# }, +# 'Adult': { +# 'Baseline': 0.17427822561729264, +# 'Reform': 0.17757158627182623, +# 'Change': 1.9 +# }, +# 'Senior': { +# 'Baseline': 0.12817646500651358, +# 'Reform': 0.1370685860340031, +# 'Change': 6.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/charts/poverty/regular/by_gender.py b/policyengine/charts/poverty/regular/by_gender.py new file mode 100644 index 0000000..a393c25 --- /dev/null +++ b/policyengine/charts/poverty/regular/by_gender.py @@ -0,0 +1,103 @@ +import plotly.graph_objects as go +from policyengine_core.charts.formatting import * + +class RegularPovertyByGenderChart: + def __init__(self, country:str,data=None): + if data is None: + raise ValueError("Data must be provided") + + self.data = data + + def _get_color(self, value): + # All bars should be gray + return GRAY + + def _get_change_direction(self, value): + if value > 0: + return "increase" + elif value < 0: + return "decrease" + else: + return "no change" + + def ordinal_suffix(self, n): + """Return the ordinal suffix for an integer.""" + if 10 <= n % 100 <= 20: + suffix = 'th' + else: + suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th') + return suffix + + def generate_chart_data(self): + categories = list(self.data.keys()) + values = [self.data[cat]['change'] for cat in categories] + baselines = [self.data[cat]['baseline'] * 100 for cat in categories] + reforms = [self.data[cat]['reform'] * 100 for cat in categories] + + # Generate hover texts with baseline, reform, and percentage change + hover_texts = [ + f"This reform would {self._get_change_direction(val)} the percentage of {category.lower()} in poverty by {abs(val):.1f}% from {baseline:.1f}% to {reform:.1f}%" + for category, val, baseline, reform in zip(categories, values, baselines, reforms) + ] + + fig = go.Figure() + + values_in_pct = values # Use percentage values + colors = [self._get_color(value) for value in values] + + # Add bar chart with percentage values + fig.add_trace(go.Bar( + x=categories, + y=values_in_pct, + marker=dict(color=colors, line=dict(width=1)), + width=0.6, + text=[f"{abs(value):.1f}%" for value in values], # Display values as percentages + textposition='outside', + hovertemplate="Category %{x}

%{customdata}", # Hover shows category + customdata=hover_texts + )) + + # Update layout to reflect percentage values on y-axis + fig.update_layout( + yaxis=dict( + tickformat=",.1f%%", + ticksuffix = "%", # Format y-axis as percentages with one decimal place + title="Percentage Change in Poverty" + ), + xaxis=dict( + title="Category" + ), + hoverlabel=dict( + bgcolor="white", + font=dict(color="black", size=16) + ), + title="Change in Poverty Percentage by Category" + ) + + format_fig(fig) # Keep the formatting logic from policyengine_core + return fig + + +# # Example data +# data = { +# 'Male': { +# 'Baseline': 0.18412623468617267, +# 'Reform': 0.18932591339284738, +# 'Change': 2.8 +# }, +# 'Female': { +# 'Baseline': 0.21377616483057263, +# 'Reform': 0.22004590877186603, +# 'Change': 2.9 +# }, +# 'All': { +# 'Baseline': 0.19913534734369268, +# 'Reform': 0.20487670454940832, +# 'Change': 2.9 +# } +# } + +# # Generate chart for all categories +# chart = OverallChart(data=data) +# fig = chart.generate_chart_data() +# fig.show() diff --git a/policyengine/economic_impact/economic_impact.py b/policyengine/economic_impact/economic_impact.py index d185047..6e0d47c 100644 --- a/policyengine/economic_impact/economic_impact.py +++ b/policyengine/economic_impact/economic_impact.py @@ -76,7 +76,18 @@ from .winners_and_losers.by_wealth_decile.by_wealth_decile import ByWealthDecile -from typing import Dict +from typing import Dict, Type, Union + +from policyengine.charts.inequality import InequalityImpactChart +from policyengine.charts.poverty.regular.by_age import RegularPovertyByAgeChart +from policyengine.charts.poverty.deep.by_age import DeepPovertyByAgeChart +from policyengine.charts.poverty.regular.by_gender import RegularPovertyByGenderChart +from policyengine.charts.poverty.deep.by_gender import DeepPovertyByGenderChart +from policyengine.charts.distributional_impact.by_income_decile.average import ByIncomeDecileAverageChart +from policyengine.charts.distributional_impact.by_income_decile.relative import ByIncomeDecileRelativeChart +from policyengine.charts.distributional_impact.by_wealth_decile.average import ByWealthDecileAverageChart +from policyengine.charts.distributional_impact.by_wealth_decile.relative import ByWealthDecileRelativeChart + class EconomicImpact: """ @@ -170,6 +181,51 @@ def __init__(self, reform: dict, country: str, dataset: str = None) -> None: } + + self.chart_generators: Dict[str, Type] = { + "inequality": InequalityImpactChart, + "poverty/regular/by_age": RegularPovertyByAgeChart, + "poverty/regular/by_gender": RegularPovertyByGenderChart, + "poverty/deep/by_age": DeepPovertyByAgeChart, + "poverty/deep/by_gender": DeepPovertyByGenderChart, + "distributional/by_income/average": ByIncomeDecileAverageChart, + "distributional/by_income/relative": ByIncomeDecileRelativeChart, + "distributional/by_wealth/average": ByWealthDecileAverageChart, + "distributional/by_wealth/relative": ByWealthDecileRelativeChart + } + + self.composite_metrics: Dict[str, Dict[str, str]] = { + "inequality": { + "Gini index": "inequality/gini", + "Top 1% share": "inequality/top_1_pct_share", + "Top 10% share": "inequality/top_10_pct_share", + }, + "poverty/regular/by_age": { + "Child": "poverty/regular/child", + "Adult": "poverty/regular/adult", + "Senior":"poverty/regular/senior", + "All": "poverty/regular/age/all" + }, + "poverty/regular/by_gender": { + "Male": "poverty/regular/male", + "Female": "poverty/regular/female", + "All": "poverty/regular/gender/all" + }, + "poverty/deep/by_age": { + "Child": "poverty/deep/child", + "Adult": "poverty/deep/adult", + "Senior":"poverty/deep/senior", + "All": "poverty/deep/age/all" + }, + "poverty/deep/by_gender": { + "Male": "poverty/deep/male", + "Female": "poverty/deep/female", + "All": "poverty/deep/gender/all" + } + } + + self.metric_results: Dict[str, any] = {} + def _get_simulation_class(self) -> type: """ Get the appropriate Microsimulation class based on the country code. @@ -203,4 +259,42 @@ def calculate(self, metric: str) -> dict: """ if metric not in self.metric_calculators: raise ValueError(f"Unknown metric: {metric}") - return self.metric_calculators[metric].calculate() + + if metric not in self.metric_results: + result = self.metric_calculators[metric].calculate() + self.metric_results[metric] = result + + return self.metric_results[metric] + + def _calculate_composite_metric(self, metric: str) -> dict: + if metric not in self.composite_metrics: + raise ValueError(f"Unknown composite metric: {metric}") + + composite_data = {} + for key, sub_metric in self.composite_metrics[metric].items(): + composite_data[key] = self.calculate(sub_metric) + + return composite_data + + def chart(self, metric: str) -> dict: + if metric in self.composite_metrics: + data = self._calculate_composite_metric(metric) + elif metric in self.chart_generators: + data = self.calculate(metric) + else: + raise ValueError(f"Unknown metric for charting: {metric}") + + chart_generator = self.chart_generators.get(metric) + if not chart_generator: + raise ValueError(f"No chart generator found for metric: {metric}") + + return chart_generator(self.country,data=data).generate_chart_data() + + def add_metric(self, metric: str, calculator: object, chart_generator: Type = None): + self.metric_calculators[metric] = calculator + if chart_generator: + self.chart_generators[metric] = chart_generator + + def add_composite_metric(self, name: str, components: Dict[str, str], chart_generator: Type): + self.composite_metrics[name] = components + self.chart_generators[name] = chart_generator \ No newline at end of file