Multiple callbacks to filter data – dash Plotly

Question:

I’m hoping to include multiple callbacks or combine them to filter data. These functions will be used to visualise graphs.

The first callback returns point data if it’s within a designated region. It is assigned to a dropdown bar called area-dropdown. The dropdown bar and callback function returns smaller subsets from the main df. This is accomplished by merging the point data within a specific polygon area.

The additional callback functions are for a scatter mapbox chart and bar chart. They filter unique values in Code and Cat.

At present, I’ve got the callback functions that filter unique values in Code and Cat operational. This is outlined in the 2nd batch of code. If I comment this section out and use the 1st batch of code, the area dropdown callback is functional.

I’m aiming to find a method that combines both these functions together.

import geopandas as gpd
import plotly.express as px
import dash
from dash import dcc, html, Input, Output
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objs as go
import plotly.figure_factory as ff
import geopandas as gpd
from itertools import cycle

# point data
gdf_all = gpd.read_file(gpd.datasets.get_path("naturalearth_cities"))

i = iter(['A', 'B', 'C', 'D'])
gdf_all['Cat'] = gdf_all.index.map(dict(zip(gdf_all.index, cycle(i))))

j = iter(['10-20', '20-30', '30-40', '40-50', '60-70'])
gdf_all['Code'] = gdf_all.index.map(dict(zip(gdf_all.index, cycle(j))))

# polygon data
gdf_poly = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
gdf_poly = gdf_poly.drop('name', axis = 1)

gdf_all['LON'] = gdf_all['geometry'].x
gdf_all['LAT'] = gdf_all['geometry'].y

# subset African continent
Afr_gdf_area = gdf_poly[gdf_poly['continent'] == 'Africa'].reset_index(drop = True)

# subset European continent
Eur_gdf_area = gdf_poly[gdf_poly['continent'] == 'Europe'].reset_index(drop = True)

# function to merge point data within selected polygon area
def merge_withinboundary(gdf1, gdf2):

    # spatial join data within larger boundary
    gdf_out = gpd.sjoin(gdf1, gdf2, predicate = 'within', how = 'inner').reset_index(drop = True)

    return gdf_out

gdf_Africa = merge_withinboundary(gdf_all, Afr_gdf_area)
gdf_Europe = merge_withinboundary(gdf_all, Eur_gdf_area)



external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP]

app = dash.Dash(__name__, external_stylesheets = external_stylesheets)

nav_bar =  html.Div([
     html.P("area-dropdown:"),
     dcc.Dropdown(
       id = 'data', 
       value = 'data', 
       options = [{'value': 'gdf_all', 'label': 'gdf_all'},
             {'value': 'gdf_Africa', 'label': 'gdf_Africa'},
             {'value': 'gdf_Europe', 'label': 'gdf_Europe'}
             ],
       clearable=False
  ),

    html.Label('Code', style = {'paddingTop': '1rem'}),
    dcc.Checklist(
            id = 'Code',
            options = [
                 {'label': '10-20', 'value': '10-20'},
                 {'label': '20-30', 'value': '20-30'},
                 {'label': '30-40', 'value': '30-40'},
                 {'label': '40-50', 'value': '40-50'},
                 {'label': '60-70', 'value': '60-70'},                        
                 ],
            value = ['10-20', '20-30', '30-40', '40-50', '60-70'],
            style = {'display': 'inline', 'margin-right': '50px'}
        ),

    html.Label('Cat', style = {'paddingTop': '1rem'}),
    dcc.Checklist(
            id = 'Cat',
            options = [
                 {'label': 'A', 'value': 'A'},
                 {'label': 'B', 'value': 'B'},
                 {'label': 'C', 'value': 'C'},
                 {'label': 'D', 'value': 'D'},                     
                 ],
            value = ['A', 'B', 'C', 'D'],
            style = {'display': 'inline', 'margin-right': '50px'}
        ),

    html.Label('Spatial Map', style = {'paddingTop': '1rem'}),
    dcc.RadioItems(['Scatter','Hexbin'],'Scatter', 
                       id = 'maps', 
                       #labelStyle= {"margin":"1rem"}, 
                       style = {'display': 'inline', 'margin-right': '50px'}
                       ),

 ], className = "vstack gap-2 h-50")


app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(html.Div(nav_bar), className = 'bg-light', width=2),
        dbc.Col([
            dbc.Row([
                dbc.Col(dcc.Graph(id = 'spatial-chart'))
            ]),
            dbc.Row([
                dbc.Col(dcc.Graph(id = 'bar-chart'))
            ]),
        ], width = 5),
        dbc.Col([
        ], width = 5),
    ])
], fluid = True)


df = gdf_all

#================  1st   =======================
# function to return selected df for plotting
#@app.callback(Output('spatial-chart', 'figure'),
#              Output('bar-chart', 'figure'),
#              Input('data', 'value'),
#              prevent_initial_call=True)

# function to return df using smaller areas
#def update_dataset(dropdown_selection):

#    if dropdown_selection == 'gdf_Africa':
#        gdf = gdf_Africa
#        zoom = 2

#    elif dropdown_selection == 'gdf_Europe':
#        gdf = gdf_Europe
#        zoom = 2

#    else:
#        gdf = gdf_all
#        zoom = 0

#    scatter_subset = px.scatter_mapbox(data_frame = gdf, 
#        lat = 'LAT', 
#        lon = 'LON',
#        zoom = zoom,
#        mapbox_style = 'carto-positron', 
#       )
#    count = gdf['name'].value_counts()

#    bar_subset = px.bar(x = count.index, 
#                y = count.values, 
#               color = count.index, 
#                ) 

#    return scatter_subset, bar_subset
#=============================================


#================  2nd   =======================
# function to filter unique Cat/Code for bar chart
@app.callback(
    [Output('bar-chart', 'figure'),
    ],
    [Input('Cat','value'), 
     Input('Code','value'),
     ]
     )     

def date_chart(cat, code):

    dff = df[df['Cat'].isin(cat)]
    dff = dff[dff['Code'].isin(code)]
    count = dff['Cat'].value_counts()

    data = px.bar(x = count.index, 
                 y = count.values,
                 color = count.index, 
                 )

    fig = [go.Figure(data = data)]

    return fig

# function to filter unique Cat/Code for scatter
@app.callback(
    [Output('spatial-chart', 'figure'),
     ],
    [Input('Cat','value'), 
     Input('Code','value'),
     Input("maps", "value"),
     ])     

def scatter_chart(cat, code, maps):

    if maps == 'Scatter':
    
        dff = df[df['Cat'].isin(cat)]
        dff = dff[dff['Code'].isin(code)]

        data = px.scatter_mapbox(data_frame = dff, 
                                   lat = 'LAT', 
                                   lon = 'LON',
                                   color = 'Cat',
                                   opacity = 0.5,
                                   zoom = 1,
                                   mapbox_style = 'carto-positron', 
                                   hover_name = 'Cat',
                                   ) 

        fig = [go.Figure(data = data)]


    elif maps == 'Hexbin':

        dff = df[df['Cat'].isin(cat)]
        dff = dff[dff['Code'].isin(code)]

        data = ff.create_hexbin_mapbox(data_frame = dff, 
                                      lat = "LAT", 
                                      lon = "LON",
                                      nx_hexagon = 100,
                                      min_count = 1,
         )
    

        fig = [go.Figure(data = data)]

    return fig
#=============================================


if __name__ == '__main__':
    app.run_server(debug=True, port = 8051)
Asked By: Chopin

||

Answers:

I combined your callback functions update_dataset, date_chart and scatter_chart into a single callback. This function processes the dropdown selection, both checklists, and the radioitem components and outputs the updated scatter mapbox and bar charts.

import geopandas as gpd
import plotly.express as px
import dash
from dash import dcc, html, Input, Output
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objs as go
import plotly.figure_factory as ff
import geopandas as gpd
from itertools import cycle

# point data
gdf_all = gpd.read_file(gpd.datasets.get_path("naturalearth_cities"))

i = iter(['A', 'B', 'C', 'D'])
gdf_all['Cat'] = gdf_all.index.map(dict(zip(gdf_all.index, cycle(i))))

j = iter(['10-20', '20-30', '30-40', '40-50', '60-70'])
gdf_all['Code'] = gdf_all.index.map(dict(zip(gdf_all.index, cycle(j))))

# polygon data
gdf_poly = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
gdf_poly = gdf_poly.drop('name', axis = 1)

gdf_all['LON'] = gdf_all['geometry'].x
gdf_all['LAT'] = gdf_all['geometry'].y

# subset African continent
Afr_gdf_area = gdf_poly[gdf_poly['continent'] == 'Africa'].reset_index(drop = True)

# subset European continent
Eur_gdf_area = gdf_poly[gdf_poly['continent'] == 'Europe'].reset_index(drop = True)

# function to merge point data within selected polygon area
def merge_withinboundary(gdf1, gdf2):

    # spatial join data within larger boundary
    gdf_out = gpd.sjoin(gdf1, gdf2, predicate = 'within', how = 'inner').reset_index(drop = True)

    return gdf_out

gdf_Africa = merge_withinboundary(gdf_all, Afr_gdf_area)
gdf_Europe = merge_withinboundary(gdf_all, Eur_gdf_area)



external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP]

app = dash.Dash(__name__, external_stylesheets = external_stylesheets)

nav_bar =  html.Div([
     html.P("area-dropdown:"),
     dcc.Dropdown(
       id = 'data', 
       value = 'data', 
       options = [{'value': 'gdf_all', 'label': 'gdf_all'},
             {'value': 'gdf_Africa', 'label': 'gdf_Africa'},
             {'value': 'gdf_Europe', 'label': 'gdf_Europe'}
             ],
       clearable=False
  ),

    html.Label('Code', style = {'paddingTop': '1rem'}),
    dcc.Checklist(
            id = 'Code',
            options = [
                 {'label': '10-20', 'value': '10-20'},
                 {'label': '20-30', 'value': '20-30'},
                 {'label': '30-40', 'value': '30-40'},
                 {'label': '40-50', 'value': '40-50'},
                 {'label': '60-70', 'value': '60-70'},                        
                 ],
            value = ['10-20', '20-30', '30-40', '40-50', '60-70'],
            style = {'display': 'inline', 'margin-right': '50px'}
        ),

    html.Label('Cat', style = {'paddingTop': '1rem'}),
    dcc.Checklist(
            id = 'Cat',
            options = [
                 {'label': 'A', 'value': 'A'},
                 {'label': 'B', 'value': 'B'},
                 {'label': 'C', 'value': 'C'},
                 {'label': 'D', 'value': 'D'},                     
                 ],
            value = ['A', 'B', 'C', 'D'],
            style = {'display': 'inline', 'margin-right': '50px'}
        ),

    html.Label('Spatial Map', style = {'paddingTop': '1rem'}),
    dcc.RadioItems(['Scatter','Hexbin'],'Scatter', 
                       id = 'maps', 
                       #labelStyle= {"margin":"1rem"}, 
                       style = {'display': 'inline', 'margin-right': '50px'}
                       ),

 ], className = "vstack gap-2 h-50")


app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(html.Div(nav_bar), className = 'bg-light', width=2),
        dbc.Col([
            dbc.Row([
                dbc.Col(dcc.Graph(id = 'spatial-chart'))
            ]),
            dbc.Row([
                dbc.Col(dcc.Graph(id = 'bar-chart'))
            ]),
        ], width = 5),
        dbc.Col([
        ], width = 5),
    ])
], fluid = True)

df = gdf_all

@app.callback(
    [Output('bar-chart', 'figure'),
     Output('spatial-chart', 'figure'),
    ],
    [Input('data', 'value'),
     Input('Cat', 'value'), 
     Input('Code','value'),
     Input('maps', 'value'),
     ]
     )     

def update_charts(dropdown_selection, cat, code, maps):

    ## dropdown defines df before the other inputs subset it further
    if dropdown_selection == 'gdf_Africa':
        df = gdf_Africa
        zoom = 2

    elif dropdown_selection == 'gdf_Europe':
        df = gdf_Europe
        zoom = 2

    else:
        df = gdf_all
        zoom = 0

    dff = df[df['Cat'].isin(cat)]
    dff = dff[dff['Code'].isin(code)]
    count = dff['Cat'].value_counts()

    data = px.bar(x = count.index, 
                 y = count.values,
                 color = count.index, 
                 )

    fig_bar = go.Figure(data = data)

    if maps == 'Scatter':
    
        dff = df[df['Cat'].isin(cat)]
        dff = dff[dff['Code'].isin(code)]

        data = px.scatter_mapbox(data_frame = dff, 
                                   lat = 'LAT', 
                                   lon = 'LON',
                                   color = 'Cat',
                                   opacity = 0.5,
                                   zoom = 1,
                                   mapbox_style = 'carto-positron', 
                                   hover_name = 'Cat',
                                   ) 

        fig_mapbox = go.Figure(data = data)


    elif maps == 'Hexbin':

        dff = df[df['Cat'].isin(cat)]
        dff = dff[dff['Code'].isin(code)]

        data = ff.create_hexbin_mapbox(data_frame = dff, 
                                      lat = "LAT", 
                                      lon = "LON",
                                      nx_hexagon = 100,
                                      min_count = 1,
         )
    
        fig_mapbox = go.Figure(data = data)
    
    return fig_bar, fig_mapbox

if __name__ == '__main__':
    app.run_server(debug=True, port = 8051)

enter image description here

Answered By: Derek O