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