from dash import Dash, dcc, html, callback, Input, Output,clientside_callback, Patch
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import load_figure_template
import plotly.io as pio
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import dash_ag_grid as dag
# adds templates to plotly.io
load_figure_template(["lux", "lux_dark"])
dfh=pd.read_csv("https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-28/CPI-historical.csv")
dfh2024 = dfh[dfh.Year == 2024].copy(deep=True)
region_labels = {
"AP": "Asia Pacific",
"ECA": "Europe and Central Asia",
"MENA": "Middle East and North Africa",
"SSA": "Sub-Saharan Africa",
"AME": "Americas",
"WE/EU": "Western Europe / European Union"
}
region_keys = list(region_labels.values())
dfh2024['Region_label']= dfh2024.Region.apply(lambda x: region_labels.get(x))
grid_columndefs = [
{'field': 'Country / Territory'},
{'field': 'CPI score',
"headerName": "CPI score 2024",},
{
"field": "cpi-score-graph",
"cellRenderer": "DCC_GraphClickData",
"headerName": "YoY CPI-score",
"maxWidth": 300,
"minWidth": 300,
},
{'field': 'Rank',
"headerName": "Rank 2024"}
]
explanation = dcc.Markdown('''
The Corruption Perceptions Index (CPI) is an index that scores and ranks countries
by their perceived levels of public sector corruption, as assessed by experts
and business executives.The CPI generally defines corruption as an
"abuse of entrusted power for private gain". The index is published annually
by the non-governmental organisation Transparency International since 1995.
Since 2012, the Corruption Perceptions Index has been ranked on a scale from 100
(very clean) to 0 (highly corrupt). (Source: Wikipedia)
''')
color_mode_switch = html.Span(
[
dbc.Label(className="fa fa-moon", html_for="color-mode-switch"),
dbc.Switch( id="color-mode-switch", value=False, className="d-inline-block ms-1", persistence=True),
dbc.Label(className="fa fa-sun", html_for="color-mode-switch"),
]
)
def create_scores_distplot():
# Initialize figure
fig = go.Figure()
# Loop through each region and add a histogram
for region_code, label in region_labels.items():
region_data = dfh2024[dfh2024['Region'] == region_code]['CPI score']
fig.add_trace(go.Histogram(
x=region_data,
name=label,
opacity=0.6,
nbinsx=20,
))
# Update layout
fig.update_layout(
xaxis_title="CPI Score",
yaxis_title="Number of countries",
barmode='overlay', # Overlay histograms
#legend_title="Region",
xaxis=dict(range=[0, 100]),
yaxis=dict(range=[0, 10]),
height=400,
legend=dict(
title='Region',
orientation="v",
)
)
return fig
app = Dash(__name__,suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.LUX, dbc.icons.FONT_AWESOME])
app.layout = dbc.Container([
dbc.Row([
dbc.Col([
html.H1('Corruption perception'),
explanation,
html.P('Click on a bar to see country details, click on a region to show/hide regiondata.', style={'marginTop':'.5rem','backgroundColor':'rgba(118, 163, 163,.3)','padding':'1rem','fontSize':'12px'})
], className="col-12 col-sm-12 col-md-6 order-2 order-md-1"),
dbc.Col(color_mode_switch, style={"textAlign":"right"}, className="col-12 col-sm-12 col-md-6 order-1 order-md-2")
]),
dbc.Row([
dbc.Col([
html.Div(id='click_output'),
html.H3("CPI Score Distribution by Region in 2024"),
dcc.Graph(figure=create_scores_distplot(), id='cpi_histogram'),
], style={'height':'400px'}),
], style={"marginTop":'3rem', 'height':'400px'}),
dbc.Modal(
[
dbc.ModalHeader(dbc.ModalTitle(id='modal-title')),
dbc.ModalBody(id='modal-body'),
],
id='grid-modal',
size='xl',
is_open=False,
),
], style={"marginTop":'2rem'})
@callback(
Output("cpi_histogram", "figure"),
Input("color-mode-switch", "value")
)
def update_figure_template(switch_on):
# When using Patch() to update the figure template, you must use the figure template dict
# from plotly.io and not just the template name
template = pio.templates["lux"] if switch_on else pio.templates["lux_dark"]
patched_figure = Patch()
patched_figure["layout"]["template"] = template
return patched_figure
@callback(
Output("country-grid", "className"),
Input("color-mode-switch", "value")
)
def change_theme(switch_on):
returnclass = "ag-theme-quartz" if switch_on else "ag-theme-quartz-dark"
return returnclass
#click on a bar of the distplot and get back the bin and region value
@callback(
#Output("click_output", "children"),
Output("modal-title", "children"),
Output("modal-body", "children"),
Output("grid-modal", "is_open"),
Input("cpi_histogram", "clickData"),
prevent_initial_call=True,
)
def display_click_info(clickData):
if not clickData:
return "Click on a bar to see region and bin range.", "", "", False
point = clickData["points"][0]
bin_center = point["x"]
region_number = point["curveNumber"]
region_label = region_keys[region_number]
bin_start = bin_center - 2
bin_end = bin_center + 2
dff2024 = dfh2024.loc[
(dfh2024['CPI score'] >= bin_start) &
(dfh2024['CPI score'] <= bin_end) &
(dfh2024['Region_label'] == region_label)
].copy()
dff2024["cpi-score-graph"] = ""
for i, r in dff2024.iterrows():
filterDf = dfh[dfh["Country / Territory"] == r["Country / Territory"]]
ymax = filterDf['CPI score'].max()
xmax = filterDf.loc[filterDf['CPI score'].idxmax(), 'Year']
ymin = filterDf['CPI score'].min()
xmin = filterDf.loc[filterDf['CPI score'].idxmin(), 'Year']
fig = px.line(filterDf, x="Year", y="CPI score")
fig.update_layout(
showlegend=False,
yaxis_visible=False,
yaxis_showticklabels=False,
xaxis_visible=False,
xaxis_showticklabels=False,
margin=dict(l=0, r=0, t=0, b=0)
)
fig.add_scatter(
x=[xmax, xmin],
y=[ymax, ymin],
mode='markers',
marker=dict(color=['green', 'red'], size=7)
)
dff2024.at[i, 'cpi-score-graph'] = fig
grid = dag.AgGrid(
rowData=dff2024.to_dict("records"),
columnDefs=grid_columndefs,
dashGridOptions={"pagination": False},
className="ag-theme-quartz",
id="country-grid",
columnSize="sizeToFit"
)
title = f"{region_label}, CPI score range {int(bin_start)}–{int(bin_end)}"
return title, grid, True
clientside_callback(
"""
(switchOn) => {
document.documentElement.setAttribute('data-bs-theme', switchOn ? 'light' : 'dark');
return window.dash_clientside.no_update
}
""",
Output("color-mode-switch", "id"),
Input("color-mode-switch", "value"),
)
if __name__ == "__main__":
app.run(debug=False)