Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distributional charts #36

Merged
merged 5 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added policyengine/charts/__init__.py
Empty file.
Empty file.
Empty file.
114 changes: 114 additions & 0 deletions policyengine/charts/distributional_impact/by_income_decile/average.py
Original file line number Diff line number Diff line change
@@ -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="<b>Decile %{x}</b><br><br>%{customdata}<extra></extra>", # 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
104 changes: 104 additions & 0 deletions policyengine/charts/distributional_impact/by_income_decile/relative.py
Original file line number Diff line number Diff line change
@@ -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="<b>Decile %{x}</b><br><br>%{customdata}<extra></extra>", # 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
Empty file.
114 changes: 114 additions & 0 deletions policyengine/charts/distributional_impact/by_wealth_decile/average.py
Original file line number Diff line number Diff line change
@@ -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="<b>Decile %{x}</b><br><br>%{customdata}<extra></extra>", # 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
Loading
Loading