Figure Friday 2025 - week 3

join the Figure Friday session on January 24, at noon Eastern Time, to showcase your creation and receive feedback from the community.

“Created by the United Nations Framework Convention on Climate Change, the Green Climate Fund aims to support a paradigm shift in the global response to climate change. It allocates its resources to low-emission and climate-resilient projects and programmes in developing countries.” (UNEP)

In week 3 of Figure Friday, we’ll explore the Climate Fund’s data on these climate-resilient projects and programmes.

The above dataset represents the Countries tab, but feel free to analyze the other three tabs, such as the Fund Activities tab.

Things to consider:

  • can you improve the sample figure below (Choropleth map)?
  • would you like to tell a different data story using a different graph?
  • can you create a Dash app instead?

Sample figure:

Code for sample figure:
import plotly.express as px
import pandas as pd

df = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-3/ODL-Export-Countries.csv')

fig = px.choropleth(
    data_frame=df,
    locations='ISO3',
    color='FA Financing $',
    color_continuous_scale='viridis',
    hover_name='Country Name',
    title='Funded Activities by Country',
    projection='orthographic'
)

fig.update_coloraxes(colorbar_tickprefix='$')
fig.show()

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation and receive feedback from the community.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel.

Data Source:

Thank you to the Green Climate Fund for the data.

3 Likes

For those interested, the Glossary didn’t provide more or any definition on some of the acronyms or columns. Maybe I missed them but here is what I had to dig deeper on.

In the context of the Green Climate Fund (GCF), the “B” in “B.11” stands for Board, specifically referring to a formal meeting of the GCF Board.

The number following the “B” indicates the sequence of the board meeting. So, B.11 signifies the 11th meeting of the GCF Board.

Theme”: the term “Cross-cutting” typically refers to projects or programs that simultaneously address both mitigation (reducing greenhouse gas emissions) and adaptation (building resilience to the impacts of climate change)

ESS:
ESS categories are used to classify the environmental and social risks associated with business activities, projects, or programs. These categories help to identify, analyze, and mitigate potential negative impacts.

Examples of ESS categories

  • Category A

Activities with significant, irreversible, or unprecedented adverse environmental or social risks

  • Category B

Activities with limited adverse environmental or social risks that are site-specific and reversible

  • Category C

Activities with minimal or no adverse environmental or social risks

5 Likes

I practiced the maps, and this is what I ended up with. I chose the greenish color because of the theme.

8 Likes

Very helpful. Thank you for this, @ThomasD21M

1 Like

Nice app, @Ester . I like how you added the dropdown to “Select Funding Rage”. That’s helpful.

I would just modify the title slightly to: Green Climate Fund Activities by Funding and Country.

Here’s a cool idea in case you have more time to work on the app:

When a user clicks a country on the map, a Dash AG Grid is filtered to show all funding activities in that country (using the projects dataset).

2 Likes

@adamschroeder That would be really good, click data and dash-grid are also very good, I’m trying to do it.:rocket:

2 Likes

Hi everyone! I’m excited to share my Green Climate Fund Analysis Dashboard
Check out the full project and code here:

5 Likes

Thanks for creating this app, @feanor_92 . You provide a lot of interesting information. One thing I wasn’t completely sure about was the SIDS and LDC heatmap. For example, what does it mean in the hover that 77 countries are False?

2 Likes

Week 3 data set provides a list of countries associated with each project. I have transformed this data to show a list of projects associated with each country and used this information as hover info on the choropleth map.

Two things I enjoyed and learned from on this project were the data transformation using polars columns of type list and experimenting with different projection types supported by choropleth maps. I ended up using wagner4 projection.

Here is a screenshot of the full world:

Here is a screenshot showing the list of projects for India, where I am now and have written this code.

I will be travelling back to California during the Friday meeting, hope to join with all of you in the following week. Here is the code:

'''
This code transforms a list of countries associated with each project into 
a list of projects associated with each country. The list of each country
projects is used for this hover display.
'''
import plotly.express as px
import polars as pl

#-------------------------------------------------------------------------------
# df_iso maps ISO3 codes to country names, joined later to df_projects
#-------------------------------------------------------------------------------
df_iso = (
    pl.read_csv('ODL-Export-Countries.csv')
    .select(pl.col('ISO3', 'Country Name'))
    .unique('ISO3')
    .rename({'Country Name' : 'Country'})
)

#-------------------------------------------------------------------------------
# read file with list of projects and associated countries, and tranform to 
# list of countries and associated projects, joined with df_iso for choropleth
#-------------------------------------------------------------------------------
df_projects = (
    pl.read_excel('ODL-Export-projects-1737422266591.xlsx')
    .select(pl.col(['Project Name', 'Countries']))
    .with_columns(pl.col('Project Name').str.slice(0,80)) # limit project len

    # convert countries given as a string into a polars list
    .with_columns(pl.col('Countries').str.split(','))
    .explode('Countries')  # explode is similar to unstack or pandas melt

    # polars list of projects for each country
    .group_by('Countries').agg(pl.col('Project Name')) 
    .with_columns(Project_Count = pl.col('Project Name').list.len())
    .with_columns(Project_Names = pl.col('Project Name').list.join(','))
    .rename({'Countries': 'Country'})

    # replace comma that separates project with carriage return
    .with_columns(pl.col('Project_Names').str.replace_all(',', '<br>'))
    .join(
        df_iso,
        on='Country',
        how='inner'
    )
    .with_columns(pl.col('Country').str.to_uppercase())
    .with_columns(
        Country = pl.col('Country') +
        pl.lit(' - ') +
        pl.col('Project_Count').cast(pl.String) +
        pl.lit(' Project(s)')
    )
    .sort('Project_Count')   # reason for sort is to numerically sort the legend
)

fig = px.choropleth(
    data_frame=df_projects,
    locations='ISO3',
    color='Project_Count',
    color_continuous_scale='viridis',
    hover_name='Country',
    title='List of Projects by Country'.upper(),
    projection='wagner4',
    height=1200, width=1200,
    custom_data=['Country', 'Project_Names'],

)

fig.update_traces(
    hovertemplate="<br>".join([
        "%{customdata[0]}",
        "%{customdata[1]}",
    ])
)
fig.update_layout(
    boxgap=0.25,
    height=500,
    margin=dict(l=10, r=0, b=10, t=100), # , pad=50),
    legend=dict(y=0.5, x=0.85)
)
fig.show()

4 Likes

Thank you for pointing that out! The hover text showing “77 countries are False” refers to the count of countries where the SIDS or LDCs designation is False. I will think how I can clarify this further in the dashboard.

2 Likes

I updated it in pycafe with clickdata and dash ag grid. :slight_smile: I think it’s best to make clickable things elements.

4 Likes

Thanks for sharing the code, @Mike_Purtell . Safe travels back home.

1 Like

awesome work, @Ester . Now users can learn more about each country.

2 Likes

Hi community! I stayed on country data this week, and it seems a pretty straightforward, so I tried with AG Grid just to practice working with tables. Just to remark, AG Grid is gigantic! So many features and attributes made it overwhelming at times, at least to me for now!

I tried a couple of things:
1. Updating the table through external components, such as a RadioButton. (I know that the table could filter on their own). But in this case, to could apply an external filter, it is necessary to add a filter-function to the callback (that’s what made it interesting!)
"isExternalFilterPresent": {"function": "true" if filter_value != 'all_rows' else "false"},

"doesExternalFilterPass": {"function": filter_function[filter_value]}

Thanks @adamschroeder for the video and @Skiks (Sebastien) for throwing me a line.

2. I created two charts using virtualRowData attribute from the table inside a callback (here I need an asterisk *). The asterisk represents the fact that I couldn’t figure out how to switch themes from light to dark using Index ids → work for home!
The two theme charts it’s on purpose to highlight the differences in switching themes!
There is always room for improvement…

AG_Grid_w3_v2

Code
'''Just some imports'''
from dash import Dash, dcc, callback, Input, Output, State, clientside_callback, _dash_renderer, Patch, ALL, html, no_update
from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc
import plotly.express as px
import plotly.io as pio
import pandas as pd
import dash_ag_grid as dag
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")

dmc.add_figure_templates(default="mantine_dark")

df = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-3/ODL-Export-Countries.csv')

# countries_no_program = (df
#                         .loc[df['RP Financing $'].isna() & df['FA Financing $'].isna()]
#                         .sort_values(by='Region'))
# # countries_no_program
# [{'label': val, 'value': val} for val in df['Region'].unique()]
# reg_val = 'Asia-Pacific'
# (df
#  .query("Region in @reg_val")
#  .sort_values(by='# RP', ascending=False)
#  )

df10 = (df
 .query('SIDS == True') # 'Yes' returns only countries classified as Small Island Developing States
 .query('LDCs == True') # 'Yes' returns only countries classified as Least Developed Countries
).groupby('Region', as_index=False).agg({'RP Financing $':'sum',
                                         'FA Financing $':'sum',
                                         '# RP':'count',
                                         '# FA':'count'})

df10_melted = (df10
               .melt(id_vars='Region',
                     value_vars=['RP Financing $','FA Financing $'],
                     var_name='RP-FA',
                     value_name='$Financing'
                     )
)

fig1 = px.histogram(df10_melted,
       x='Region',
       y='$Financing',
       color='RP-FA',
       histfunc='sum',
       barmode='group', # ['stack', 'group', 'overlay', 'relative']
       template='simple_white'
       )
# fig1

df11_melted = (df10
               .melt(id_vars='Region',
                     value_vars=['# RP','# FA'],
                     var_name='RP_FA',
                     value_name='# Projects'
                     )
)
# df11_melted
fig2 = px.histogram(df11_melted,
       x='Region',
       y='# Projects',
       color='RP_FA',
    #    histfunc='sum',
       barmode='group', # ['stack', 'group', 'overlay', 'relative']
       template='simple_white'
       )
# fig2


## ****APP****
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
app = Dash(__name__, external_stylesheets=dmc.styles.ALL)

data = [["SIDS", '10'], ["LDCs", '01'], ["Both True", '11'], ["Both False", '00'], ["None Filter", 'all_rows']]
radio_group = dmc.RadioGroup(
    dmc.Group([dmc.Radio(l, value=k) for l, k in data], my=10),
    id="radiogroup_simple",
    deselectable=False,
    value='all_rows',
    label="Add filter (SIDS and/or LDCs)",
    size="md",
    mb=10,
)

filter_function = {
    '10': "params.data.SIDS == true && params.data.LDCs == false",
    '01': "params.data.SIDS == false && params.data.LDCs == true",
    '11': "params.data.SIDS == true && params.data.LDCs == true",
    '00': "params.data.SIDS == false && params.data.LDCs == false",
    'all_rows': "true"
}

table = dag.AgGrid(
    id='table_1',
    rowData=df.to_dict("records"),
    columnDefs= [{"field": col} for col in df.columns.to_list()],
    columnSize="autoSize",
    # defaultColDef={"filter": True},
    dashGridOptions={'pagination':True,  # "animateRows": False, "rowSelection":'single'
                     'paginationPageSize':15,
                     "animateRows": False, #"rowSelection":'single'
                     },
    # className="ag-theme-alpine-dark"
)

## ****Graph Cards****
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# def make_graph_card(fig, index):
#     return dmc.GridCol(
#         dmc.Card(dcc.Graph(figure=fig, id={"index": index}),
#                  withBorder=True,
#                  my='xs',
#                  shadow="sm",
#                  radius="md",),
#         span={"base": 12, "md": 6}
#     )
# graphs = dmc.Grid(
#     [
#         make_graph_card(fig1, "fig1"),
#         make_graph_card(fig2, "fig2"),
#     ],
#     gutter="xl",
#     style={"height": 800}
# )

graphs = dmc.Grid(
    [
        dmc.GridCol(
            dmc.Card(
                dcc.Graph(
                    id='fig1_id',#{"index": "fig1_id"},#
                    figure={},
                    ),
                    withBorder=True,
                    my='xs',
                    shadow="sm",
                    radius="md"
                    ),
                    span={"base": 12, "md": 6},
                    ),
        dmc.GridCol(
            dmc.Card(
                dcc.Graph(
                    id='fig2_id',#{"index": "fig2_id"},
                    figure={},
                    ),
                    withBorder=True,
                    my='xs',
                    shadow="sm",
                    radius="md"
                    ),
                    span={"base": 12, "md": 6},
                    )
    ],
    gutter="xl",
    style={"height": 800}
)

theme_toggle = dmc.Switch(
    offLabel=DashIconify(icon="radix-icons:sun", width=15, color=dmc.DEFAULT_THEME["colors"]["yellow"][8]),
    onLabel=DashIconify(icon="radix-icons:moon", width=15, color=dmc.DEFAULT_THEME["colors"]["yellow"][6]),
    id="color-scheme-toggle",
    persistence=True,
    color="grey",
)

# ****LAYOUT****
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
layout = dmc.Container([
    dmc.Group([
        dmc.Title("Funded Activities Dashboard Navigator", mt="md", order=1),
        theme_toggle,
    ], justify="space-between"),
    dmc.Box([
        dmc.Title("Money and projects delivered by Region", my='md', order=2),
    ]),
    radio_group,
    table,
    dmc.Box([
        dcc.Markdown("_**SIDS**, 'Yes' returns only readiness grants targeting Small Island Developing States_"),
        dcc.Markdown("_**LDCs**, 'Yes' returns only readiness grants targeting Least Developed Countries_")
    ],mt='10px', ps='10px'), # bd="1px solid", 
    graphs,
], fluid=True)

app.layout = dmc.MantineProvider(layout)

# ****Switching Light to dark****
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
clientside_callback(
    """ 
    (switchOn) => {
       document.documentElement.setAttribute('data-mantine-color-scheme', switchOn ? 'dark' : 'light');  
       return window.dash_clientside.no_update
    }
    """,
    Output("color-scheme-toggle", "id"),
    Input("color-scheme-toggle", "checked"),
)

# ****Callback to switch AGGrid themes from light to dark mode****
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@callback(
    Output("table_1", "className"),
    Input("color-scheme-toggle", "checked"),
)
def update_grid_theme(switch_on):
    if switch_on:
        return "ag-theme-alpine-dark"
    return "ag-theme-alpine"


# ****Callback to switching themes figures from light to dark mode****
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# @callback(
#     Output({"index": ALL}, "figure")#, allow_duplicate=True),#Output({"type": "graph", "index": ALL}, "figure", allow_duplicate=True),
#     Input("color-scheme-toggle", "checked"),
#     State({"index": ALL}, "id"),#State({"type": "graph", "index": ALL}, "id"),
#     prevent_initial_call=True
# )
# def update_figure_template(switch_on, ids):
#     print(ids)
#     template = pio.templates["mantine_dark"] if switch_on else pio.templates["mantine_light"]
#     patched_figures = []
#     for i in ids:
#         patched_fig = Patch()
#         patched_fig["layout"]["template"] = template
#         patched_figures.append(patched_fig)
#     return patched_figures

# Filter table AGG based on RadioButton
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@callback(
    Output("table_1", "dashGridOptions", allow_duplicate=True),
    Input("radiogroup_simple", "value"),
    prevent_initial_call=True,
)
def update_external_filter(filter_value):
    # print(filter_value)
    return {
        # if filter_value is not 'everyone', then we will start filtering
        "isExternalFilterPresent": {"function": "true" if filter_value != 'all_rows' else "false"},
        "doesExternalFilterPass": {"function": filter_function[filter_value]}
    }

## Updating figures based on virtualRowData
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@callback(
    Output('fig1_id', 'figure'),
    Output('fig2_id', 'figure'),
    Input('table_1', 'virtualRowData'),
    Input("color-scheme-toggle", "checked"),
    pervent_initial_call=True
)
def update_fig_based_AGG(vdata, switch_on):

    template = pio.templates["mantine_dark"] if switch_on else pio.templates["mantine_light"]

    if vdata == None:
        raise PreventUpdate
    else:
        dff = pd.DataFrame(vdata)
        dff0 = (dff
                .groupby('Region', as_index=False)
                .agg({'RP Financing $':'sum',
                    'FA Financing $':'sum',
                    '# RP':'count',
                    '# FA':'count'})
                )
        dff1 = (dff0
                .melt(id_vars='Region',
                    value_vars=['RP Financing $','FA Financing $'],
                    var_name='RP-FA',
                    value_name='$Financing')
        )
        fig10 = px.histogram(dff1,
                            x='Region',
                            y='$Financing',
                            color='RP-FA',
                            histfunc='sum',
                            barmode='group', # ['stack', 'group', 'overlay', 'relative']
                            template=template,
                            title="Total <b>'USD$'</b> of Readiness Programmes and Funded Activities<br> by Region.",
                            labels={'Region': ''}
        ).update_layout(legend_title_text='').update_yaxes(title_text='')
        dff2 = (dff0
                 .melt(id_vars='Region',
                       value_vars=['# RP','# FA'],
                       var_name='RP_FA',
                       value_name='# Projects'
                 )
        )
        fig20 = px.histogram(dff2,
                            x='Region',
                            y='# Projects',
                            color='RP_FA',
                            # histfunc='sum',
                            barmode='group', # ['stack', 'group', 'overlay', 'relative']
                            template='simple_white',
                            title="Number <b>'#'</b> of Readiness Programmes and Funded Activities<br> by Region.",
                            labels={'Region': ''}
       ).update_layout(legend_title_text='').update_yaxes(title_text='')
        return fig10, fig20


if __name__ == "__main__":
    app.run(debug=True, jupyter_mode='external', port=8085)
6 Likes

One can learn a lot from your code, @JuanG . Thank you for sharing.

I see you create your histograms twice: once at the top, globally and once in the final callback.
Could you reduce the amount of code by creating the histograms only inside the callback, changing pervent_initial_call=False?

1 Like