|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
import pandas as pd |
|
|
from src.config_manager import get_config |
|
|
|
|
|
def get_sdg_colors(): |
|
|
""" |
|
|
Get SDG colors from configuration or use defaults. |
|
|
""" |
|
|
config = get_config() |
|
|
sdg_colors_config = config.get('visualization.sdg_colors', {}) |
|
|
|
|
|
|
|
|
default_colors = { |
|
|
"1": '#E5243B', |
|
|
"2": '#DDA63A', |
|
|
"3": '#4C9F38', |
|
|
"4": '#C5192D', |
|
|
"5": '#FF3A21', |
|
|
"6": '#26BDE2', |
|
|
"7": '#FCC30B', |
|
|
"8": '#A21942', |
|
|
"9": '#FD6925', |
|
|
"10": '#DD1367', |
|
|
"11": '#FD9D24', |
|
|
"12": '#BF8B2E', |
|
|
"13": '#3F7E44', |
|
|
"14": '#0A97D9', |
|
|
"15": '#56C02B', |
|
|
"16": '#00689D', |
|
|
"17": '#19486A' |
|
|
} |
|
|
|
|
|
|
|
|
colors = sdg_colors_config or default_colors |
|
|
|
|
|
|
|
|
return {int(k): v for k, v in colors.items()} |
|
|
|
|
|
|
|
|
|
|
|
SDG_COLORS = get_sdg_colors() |
|
|
|
|
|
SDG_NAMES = { |
|
|
1: 'No Poverty', |
|
|
2: 'Zero Hunger', |
|
|
3: 'Good Health', |
|
|
4: 'Quality Education', |
|
|
5: 'Gender Equality', |
|
|
6: 'Clean Water', |
|
|
7: 'Clean Energy', |
|
|
8: 'Decent Work', |
|
|
9: 'Industry & Innovation', |
|
|
10: 'Reduced Inequalities', |
|
|
11: 'Sustainable Cities', |
|
|
12: 'Responsible Consumption', |
|
|
13: 'Climate Action', |
|
|
14: 'Life Below Water', |
|
|
15: 'Life on Land', |
|
|
16: 'Peace & Justice', |
|
|
17: 'Partnerships' |
|
|
} |
|
|
|
|
|
def create_world_map(df): |
|
|
""" |
|
|
Create a Choropleth map for the overall SDG index score. |
|
|
""" |
|
|
fig = px.choropleth( |
|
|
df, |
|
|
locations="country", |
|
|
locationmode="country names", |
|
|
color="sdg_index_score", |
|
|
hover_name="country", |
|
|
hover_data={'sdg_index_score': ':.1f'}, |
|
|
color_continuous_scale=[ |
|
|
[0, '#FF6B6B'], |
|
|
[0.25, '#FFE66D'], |
|
|
[0.5, '#4ECDC4'], |
|
|
[0.75, '#45B7D1'], |
|
|
[1.0, '#2ECC71'] |
|
|
], |
|
|
title="π Global SDG Index Progress (Latest Year)", |
|
|
labels={'sdg_index_score': 'SDG Index Score'} |
|
|
) |
|
|
fig.update_layout( |
|
|
geo=dict( |
|
|
showframe=False, |
|
|
showcoastlines=True, |
|
|
coastlinecolor='#ddd', |
|
|
projection_type='equirectangular', |
|
|
bgcolor='rgba(0,0,0,0)', |
|
|
landcolor='#f5f5f5', |
|
|
countrycolor='#fff' |
|
|
), |
|
|
margin=dict(l=0, r=0, b=0, t=50), |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
coloraxis_colorbar=dict( |
|
|
title=dict(text="Score", font=dict(size=14)), |
|
|
tickfont=dict(size=12), |
|
|
len=0.6, |
|
|
thickness=15 |
|
|
), |
|
|
title=dict(font=dict(size=18, color='#0d4f6c')) |
|
|
) |
|
|
return fig |
|
|
|
|
|
def create_radar_chart(df, country, year): |
|
|
""" |
|
|
Create a radar chart for the 17 SDG goals with official colors and labels. |
|
|
""" |
|
|
target_row = df[(df['country'] == country) & (df['year'] == year)] |
|
|
if target_row.empty: |
|
|
return None |
|
|
|
|
|
|
|
|
categories = [f"SDG {i}: {SDG_NAMES[i]}" for i in range(1, 18)] |
|
|
|
|
|
|
|
|
values = [] |
|
|
for i in range(1, 18): |
|
|
col_name = f"goal_{i}_score" |
|
|
if col_name not in target_row.columns: |
|
|
values.append(0.0) |
|
|
continue |
|
|
|
|
|
val = target_row[col_name] |
|
|
if isinstance(val, pd.DataFrame): |
|
|
val = val.iloc[0, 0] |
|
|
elif isinstance(val, pd.Series): |
|
|
val = val.iloc[0] |
|
|
|
|
|
try: |
|
|
val = float(val) |
|
|
if pd.isna(val): val = 0.0 |
|
|
except: |
|
|
val = 0.0 |
|
|
values.append(val) |
|
|
|
|
|
|
|
|
values_closed = values + [values[0]] |
|
|
categories_closed = categories + [categories[0]] |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
|
|
|
fig.add_trace(go.Scatterpolar( |
|
|
r=values_closed, |
|
|
theta=categories_closed, |
|
|
fill='toself', |
|
|
fillcolor='rgba(59, 130, 246, 0.2)', |
|
|
line=dict(color='#3b82f6', width=2), |
|
|
name=f'{country} ({year})', |
|
|
hoverinfo='skip' |
|
|
)) |
|
|
|
|
|
|
|
|
for i, (r, cat) in enumerate(zip(values, categories)): |
|
|
goal_id = i + 1 |
|
|
fig.add_trace(go.Scatterpolar( |
|
|
r=[r], |
|
|
theta=[cat], |
|
|
mode='markers', |
|
|
marker=dict( |
|
|
color=SDG_COLORS.get(goal_id, '#888'), |
|
|
size=12, |
|
|
line=dict(color='white', width=1) |
|
|
), |
|
|
name=f"SDG {goal_id}", |
|
|
hovertemplate=f"<b>{SDG_NAMES[goal_id]}</b><br>Score: {r:.1f}<extra></extra>" |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
polar=dict( |
|
|
radialaxis=dict( |
|
|
visible=True, |
|
|
range=[0, 100], |
|
|
tickfont=dict(size=9), |
|
|
gridcolor='#e2e8f0', |
|
|
angle=0, |
|
|
tickangle=0 |
|
|
), |
|
|
angularaxis=dict( |
|
|
tickfont=dict(size=10, color='#64748b'), |
|
|
gridcolor='#e2e8f0', |
|
|
rotation=90, |
|
|
direction='clockwise' |
|
|
), |
|
|
bgcolor='rgba(255, 255, 255, 0)' |
|
|
), |
|
|
showlegend=False, |
|
|
title=dict( |
|
|
text=f"π― {country} SDG Performance ({year})", |
|
|
font=dict(size=18, color='#1e293b'), |
|
|
x=0.5, |
|
|
y=0.95 |
|
|
), |
|
|
margin=dict(t=80, b=40, l=80, r=80), |
|
|
height=450, |
|
|
paper_bgcolor='rgba(0,0,0,0)' |
|
|
) |
|
|
return fig |
|
|
|
|
|
def create_trend_chart(df_filtered): |
|
|
""" |
|
|
Create a multi-line chart for SDG trends with 2025 styling. |
|
|
""" |
|
|
fig = px.line( |
|
|
df_filtered, |
|
|
x="year", |
|
|
y="sdg_index_score", |
|
|
title="π Overall SDG Index Score Trend (2000-2025)", |
|
|
markers=True, |
|
|
line_shape="spline" |
|
|
) |
|
|
|
|
|
fig.update_traces( |
|
|
line=dict(color='#3b82f6', width=4), |
|
|
marker=dict(size=10, symbol='circle', line=dict(width=2, color='white')), |
|
|
hovertemplate='<b>Year: %{x}</b><br>SDG Index: %{y:.2f}<extra></extra>' |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
xaxis=dict( |
|
|
title=dict(text="Year", font=dict(size=14)), |
|
|
gridcolor='#f1f5f9', |
|
|
dtick=2 |
|
|
), |
|
|
yaxis=dict( |
|
|
title=dict(text="Overall Score", font=dict(size=14)), |
|
|
gridcolor='#f1f5f9', |
|
|
range=[ |
|
|
df_filtered['sdg_index_score'].min() * 0.95 if not df_filtered['sdg_index_score'].dropna().empty else 0, |
|
|
df_filtered['sdg_index_score'].max() * 1.05 if not df_filtered['sdg_index_score'].dropna().empty else 100 |
|
|
] |
|
|
), |
|
|
annotations=[ |
|
|
dict( |
|
|
text="Data: SDSN 2025 | Unit: Score (0-100)", |
|
|
showarrow=False, |
|
|
xref="paper", yref="paper", |
|
|
x=1, y=-0.2, |
|
|
font=dict(size=10, color="gray") |
|
|
) |
|
|
], |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(255, 255, 255, 0.5)', |
|
|
hovermode='x unified', |
|
|
height=450 |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
def create_detailed_trend_chart(df_filtered): |
|
|
""" |
|
|
Create a detailed multi-line chart for individual goals. |
|
|
""" |
|
|
cols = [f"goal_{i}_score" for i in range(1, 18)] |
|
|
|
|
|
|
|
|
df_melted = df_filtered.melt( |
|
|
id_vars=['year'], |
|
|
value_vars=cols, |
|
|
var_name='Goal', |
|
|
value_name='Score' |
|
|
) |
|
|
|
|
|
|
|
|
df_melted['Goal_Num'] = df_melted['Goal'].str.extract(r'goal_(\d+)_score').astype(int) |
|
|
df_melted['Goal_Name'] = df_melted['Goal_Num'].map(lambda x: f"SDG {x}: {SDG_NAMES[x]}") |
|
|
df_melted['Color'] = df_melted['Goal_Num'].map(SDG_COLORS) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
for goal_num in range(1, 18): |
|
|
goal_data = df_melted[df_melted['Goal_Num'] == goal_num] |
|
|
fig.add_trace(go.Scatter( |
|
|
x=goal_data['year'], |
|
|
y=goal_data['Score'], |
|
|
mode='lines+markers', |
|
|
name=f"SDG {goal_num}", |
|
|
line=dict(color=SDG_COLORS[goal_num], width=2), |
|
|
marker=dict(size=6), |
|
|
hovertemplate=f'{SDG_NAMES[goal_num]}<br>Year: %{{x}}<br>Score: %{{y:.1f}}<extra></extra>' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title=dict( |
|
|
text="π Individual SDG Goals Trends", |
|
|
font=dict(size=16, color='#0d4f6c') |
|
|
), |
|
|
xaxis=dict( |
|
|
title=dict(text="Year", font=dict(size=13)), |
|
|
gridcolor='#eee' |
|
|
), |
|
|
yaxis=dict( |
|
|
title=dict(text="Score", font=dict(size=13)), |
|
|
range=[0, 100], |
|
|
gridcolor='#eee' |
|
|
), |
|
|
legend=dict( |
|
|
orientation='h', |
|
|
yanchor='bottom', |
|
|
y=-0.4, |
|
|
xanchor='center', |
|
|
x=0.5, |
|
|
font=dict(size=10) |
|
|
), |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(248, 250, 252, 0.5)', |
|
|
height=500, |
|
|
margin=dict(b=120) |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
|