sdgToPic / src /viz_engine.py
Song
chore: use git lfs for large data files
b88006b
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 if not in config
default_colors = {
"1": '#E5243B', # No Poverty
"2": '#DDA63A', # Zero Hunger
"3": '#4C9F38', # Good Health and Well-being
"4": '#C5192D', # Quality Education
"5": '#FF3A21', # Gender Equality
"6": '#26BDE2', # Clean Water and Sanitation
"7": '#FCC30B', # Affordable and Clean Energy
"8": '#A21942', # Decent Work and Economic Growth
"9": '#FD6925', # Industry, Innovation and Infrastructure
"10": '#DD1367', # Reduced Inequalities
"11": '#FD9D24', # Sustainable Cities and Communities
"12": '#BF8B2E', # Responsible Consumption and Production
"13": '#3F7E44', # Climate Action
"14": '#0A97D9', # Life Below Water
"15": '#56C02B', # Life on Land
"16": '#00689D', # Peace, Justice and Strong Institutions
"17": '#19486A' # Partnerships for the Goals
}
# Use config colors or fallback to defaults
colors = sdg_colors_config or default_colors
# Convert string keys to integers
return {int(k): v for k, v in colors.items()}
# Get colors from configuration
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
# Use goal names as labels for better clarity
categories = [f"SDG {i}: {SDG_NAMES[i]}" for i in range(1, 18)]
# Robustly extract values as scalars
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)
# Close the radar loop
values_closed = values + [values[0]]
categories_closed = categories + [categories[0]]
fig = go.Figure()
# Add the radar area
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' # Disable hover for area to avoid interference with markers
))
# Add individual markers for each goal with their official colors
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)]
# Melt the dataframe for plotting
df_melted = df_filtered.melt(
id_vars=['year'],
value_vars=cols,
var_name='Goal',
value_name='Score'
)
# Map goal numbers to SDG names and colors
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