Py.Cafe

marie-anne/

2025-figurefriday-w28

An idea about visualising CPI scores, not the best idea

DocsPricing
  • assets/
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
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)