Skip to content

Commit 5ec0ce2

Browse files
Merge pull request #36 from masterismail/distributionalCharts
Distributional charts
2 parents cd2d20a + 88ef712 commit 5ec0ce2

File tree

8 files changed

+445
-0
lines changed

8 files changed

+445
-0
lines changed

policyengine/charts/distributional_impact/__init__.py

Whitespace-only changes.

policyengine/charts/distributional_impact/by_income_decile/__init__.py

Whitespace-only changes.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import plotly.graph_objects as go
2+
from policyengine_core.charts.formatting import *
3+
4+
class ByIncomeDecileAverageChart:
5+
def __init__(self, country: str, data=None):
6+
if data is None:
7+
raise ValueError("Data must be provided")
8+
9+
# Store values as they are (no percentage conversion)
10+
for i in range(1, 12):
11+
setattr(self, f"decile_{i}", data['average'][i])
12+
13+
self.country = country
14+
15+
def _get_color(self, value):
16+
if value is None or value == 0 or value < 0:
17+
return GRAY
18+
return BLUE
19+
20+
def _get_change_direction(self, value):
21+
if value > 0:
22+
return "increase"
23+
elif value < 0:
24+
return "decrease"
25+
else:
26+
return "no change"
27+
28+
def _get_currency_symbol(self):
29+
if self.country.lower() == "us":
30+
return "$"
31+
elif self.country.lower() == "uk":
32+
return "£"
33+
else:
34+
return "$" # Default to USD if country not recognized
35+
36+
def ordinal_suffix(self, n):
37+
"""Return the ordinal suffix for an integer."""
38+
if 10 <= n % 100 <= 20:
39+
suffix = 'th'
40+
else:
41+
suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
42+
return suffix
43+
44+
def generate_chart_data(self):
45+
categories = [str(i) for i in range(1, 12)]
46+
values = [getattr(self, f"decile_{i}") for i in range(1, 12)]
47+
48+
# Filter out categories and values with zero difference
49+
non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0]
50+
51+
if not non_zero_data:
52+
fig = go.Figure()
53+
fig.add_annotation(
54+
x=0.5,
55+
y=0.5,
56+
xref="paper",
57+
yref="paper",
58+
text="No differences to display",
59+
showarrow=False,
60+
font=dict(size=20)
61+
)
62+
fig.update_layout(
63+
title="Absolute change in household income",
64+
xaxis=dict(visible=False),
65+
yaxis=dict(visible=False)
66+
)
67+
return fig
68+
69+
non_zero_categories, non_zero_values = zip(*non_zero_data)
70+
71+
# Get currency symbol based on country
72+
currency_symbol = self._get_currency_symbol()
73+
74+
# Generate hover texts with raw impact values and change direction
75+
hover_texts = [
76+
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}"
77+
for i, val in zip(non_zero_categories, non_zero_values)
78+
]
79+
80+
fig = go.Figure()
81+
82+
values_in_bn = non_zero_values # No need to convert values
83+
colors = [self._get_color(value) for value in non_zero_values]
84+
85+
# Add bar chart with text formatted with currency symbol
86+
fig.add_trace(go.Bar(
87+
x=non_zero_categories,
88+
y=values_in_bn,
89+
marker=dict(color=colors, line=dict(width=1)),
90+
width=0.6,
91+
text=[f"{currency_symbol}{abs(value):,.1f}" for value in non_zero_values], # Display values with currency symbol
92+
textposition='outside',
93+
hovertemplate="<b>Decile %{x}</b><br><br>%{customdata}<extra></extra>", # Hover shows "Decile {x}"
94+
customdata=hover_texts
95+
))
96+
97+
# Update layout to include currency on y-axis
98+
fig.update_layout(
99+
yaxis=dict(
100+
tickformat=",.0f", # No decimal places for the y-axis, add thousands separator
101+
title=f"Absolute Impact on Income ({currency_symbol})"
102+
),
103+
xaxis=dict(
104+
title="Income Decile"
105+
),
106+
hoverlabel=dict(
107+
bgcolor="white",
108+
font=dict(color="black", size=16)
109+
),
110+
title="Absolute Change in Household Income by Decile"
111+
)
112+
113+
format_fig(fig) # Keep the formatting logic from policyengine_core
114+
return fig
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import plotly.graph_objects as go
2+
from policyengine_core.charts.formatting import *
3+
4+
5+
class ByIncomeDecileRelativeChart:
6+
def __init__(self, country: str, data=None):
7+
if data is None:
8+
raise ValueError("Data must be provided")
9+
10+
# Convert the relative values to percentages and store them in attributes for each decile
11+
for i in range(1, 12):
12+
setattr(self, f"decile_{i}", data['relative'][i] * 100)
13+
14+
self.country = country
15+
16+
def _get_color(self, value):
17+
if value is None or value == 0 or value < 0:
18+
return GRAY
19+
return BLUE
20+
21+
def _get_change_direction(self, value):
22+
if value > 0:
23+
return "increase"
24+
elif value < 0:
25+
return "decrease"
26+
else:
27+
return "no change"
28+
29+
def ordinal_suffix(self, n):
30+
"""Return the ordinal suffix for an integer."""
31+
if 10 <= n % 100 <= 20:
32+
suffix = 'th'
33+
else:
34+
suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
35+
return suffix
36+
37+
38+
def generate_chart_data(self):
39+
categories = [str(i) for i in range(1, 12)]
40+
values = [getattr(self, f"decile_{i}") for i in range(1, 12)]
41+
42+
# Filter out categories and values with zero difference
43+
non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0]
44+
45+
if not non_zero_data:
46+
fig = go.Figure()
47+
fig.add_annotation(
48+
x=0.5,
49+
y=0.5,
50+
xref="paper",
51+
yref="paper",
52+
text="No differences to display",
53+
showarrow=False,
54+
font=dict(size=20)
55+
)
56+
fig.update_layout(
57+
title="Relative change in household income",
58+
xaxis=dict(visible=False),
59+
yaxis=dict(visible=False)
60+
)
61+
return fig
62+
63+
non_zero_categories, non_zero_values = zip(*non_zero_data)
64+
65+
# Generate hover texts with formatted impact values and change direction
66+
hover_texts = [
67+
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}%"
68+
for i, val in zip(non_zero_categories, non_zero_values)
69+
]
70+
71+
fig = go.Figure()
72+
73+
values_in_bn = non_zero_values # The values are already in percentages
74+
colors = [self._get_color(value) for value in non_zero_values]
75+
76+
fig.add_trace(go.Bar(
77+
x=non_zero_categories,
78+
y=values_in_bn,
79+
marker=dict(color=colors, line=dict(width=1)),
80+
width=0.6,
81+
text=[f"{value:.1f}%" for value in non_zero_values], # Display values with one decimal place and percentage symbol
82+
textposition='outside',
83+
hovertemplate="<b>Decile %{x}</b><br><br>%{customdata}<extra></extra>", # Hover shows "Decile {x}"
84+
customdata=hover_texts
85+
))
86+
87+
# Update layout to show percentage on y-axis and format figure
88+
fig.update_layout(
89+
yaxis=dict(
90+
tickformat=".1f%", # Format for one decimal place with percentage symbol
91+
ticksuffix="%",
92+
title="Relative Impact on Income (%)"
93+
),
94+
xaxis=dict(
95+
title="Income Decile"
96+
),
97+
hoverlabel=dict(
98+
bgcolor="white",
99+
font=dict(color="black", size=16)
100+
),
101+
title="Relative Change in Household Income by Decile"
102+
)
103+
format_fig(fig) # Keep the formatting logic from policyengine_core
104+
return fig

policyengine/charts/distributional_impact/by_wealth_decile/__init__.py

Whitespace-only changes.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import plotly.graph_objects as go
2+
from policyengine_core.charts.formatting import *
3+
4+
class ByWealthDecileAverageChart:
5+
def __init__(self, country: str, data=None):
6+
if data is None:
7+
raise ValueError("Data must be provided")
8+
9+
# Store values as they are (no percentage conversion)
10+
for i in range(1, 12):
11+
setattr(self, f"decile_{i}", data['average'][i])
12+
13+
self.country = country
14+
15+
def _get_color(self, value):
16+
if value is None or value == 0 or value < 0:
17+
return GRAY
18+
return BLUE
19+
20+
def _get_change_direction(self, value):
21+
if value > 0:
22+
return "increase"
23+
elif value < 0:
24+
return "decrease"
25+
else:
26+
return "no change"
27+
28+
def _get_currency_symbol(self):
29+
if self.country.lower() == "us":
30+
return "$"
31+
elif self.country.lower() == "uk":
32+
return "£"
33+
else:
34+
return "$" # Default to USD if country not recognized
35+
36+
def ordinal_suffix(self, n):
37+
"""Return the ordinal suffix for an integer."""
38+
if 10 <= n % 100 <= 20:
39+
suffix = 'th'
40+
else:
41+
suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')
42+
return suffix
43+
44+
def generate_chart_data(self):
45+
categories = [str(i) for i in range(1, 12)]
46+
values = [getattr(self, f"decile_{i}") for i in range(1, 12)]
47+
48+
# Filter out categories and values with zero difference
49+
non_zero_data = [(cat, val) for cat, val in zip(categories, values) if val != 0]
50+
51+
if not non_zero_data:
52+
fig = go.Figure()
53+
fig.add_annotation(
54+
x=0.5,
55+
y=0.5,
56+
xref="paper",
57+
yref="paper",
58+
text="No differences to display",
59+
showarrow=False,
60+
font=dict(size=20)
61+
)
62+
fig.update_layout(
63+
title="Absolute change in household income",
64+
xaxis=dict(visible=False),
65+
yaxis=dict(visible=False)
66+
)
67+
return fig
68+
69+
non_zero_categories, non_zero_values = zip(*non_zero_data)
70+
71+
# Get currency symbol based on country
72+
currency_symbol = self._get_currency_symbol()
73+
74+
# Generate hover texts with raw impact values and change direction
75+
hover_texts = [
76+
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}"
77+
for i, val in zip(non_zero_categories, non_zero_values)
78+
]
79+
80+
fig = go.Figure()
81+
82+
values_in_bn = non_zero_values # No need to convert values
83+
colors = [self._get_color(value) for value in non_zero_values]
84+
85+
# Add bar chart with text formatted with currency symbol
86+
fig.add_trace(go.Bar(
87+
x=non_zero_categories,
88+
y=values_in_bn,
89+
marker=dict(color=colors, line=dict(width=1)),
90+
width=0.6,
91+
text=[f"{currency_symbol}{abs(value):,.1f}" for value in non_zero_values], # Display values with currency symbol
92+
textposition='outside',
93+
hovertemplate="<b>Decile %{x}</b><br><br>%{customdata}<extra></extra>", # Hover shows "Decile {x}"
94+
customdata=hover_texts
95+
))
96+
97+
# Update layout to include currency on y-axis
98+
fig.update_layout(
99+
yaxis=dict(
100+
tickformat=",.0f", # No decimal places for the y-axis, add thousands separator
101+
title=f"Absolute Impact on Wealth ({currency_symbol})"
102+
),
103+
xaxis=dict(
104+
title="Wealth Decile"
105+
),
106+
hoverlabel=dict(
107+
bgcolor="white",
108+
font=dict(color="black", size=16)
109+
),
110+
title="Absolute Change in Household Income by Decile"
111+
)
112+
113+
format_fig(fig) # Keep the formatting logic from policyengine_core
114+
return fig

0 commit comments

Comments
 (0)